feat.airapp剥离启动器

This commit is contained in:
lincube
2026-05-31 19:41:10 +08:00
parent 21e970c5b6
commit c351a8e7f3
78 changed files with 1957 additions and 1250 deletions

View File

@@ -0,0 +1,27 @@
using dotnetCampus.Ipc.CompilerServices.Attributes;
namespace LanMountainDesktop.Shared.IPC.Abstractions.Services;
[IpcPublic(IgnoresIpcException = true)]
public interface IAirAppRuntimeControlService
{
Task<AirAppRuntimeControlResult> AttachHostAsync(int hostProcessId);
Task<AirAppRuntimeStatus> GetStatusAsync();
}
public sealed record AirAppRuntimeControlResult(
bool Accepted,
string Code,
string Message,
AirAppRuntimeStatus Status);
public sealed record AirAppRuntimeStatus(
int ProcessId,
int LauncherProcessId,
int HostProcessId,
bool LauncherProcessAlive,
bool HostProcessAlive,
bool HasLiveAirApps,
DateTimeOffset StartedAtUtc,
DateTimeOffset UpdatedAtUtc);

View File

@@ -0,0 +1,64 @@
using System.Text.Json;
namespace LanMountainDesktop.Shared.IPC;
public static class AirAppRuntimeDataRootResolver
{
private const string LauncherDataFolderName = ".Launcher";
private const string ConfigFileName = "data-location.config.json";
private const string DesktopFolderName = "Desktop";
public static string ResolveDataRoot(string? appRoot)
{
var defaultSystemDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
if (string.IsNullOrWhiteSpace(appRoot))
{
return defaultSystemDataPath;
}
var normalizedAppRoot = Path.GetFullPath(appRoot);
var configPath = Path.Combine(normalizedAppRoot, LauncherDataFolderName, ConfigFileName);
if (!File.Exists(configPath))
{
return defaultSystemDataPath;
}
try
{
using var document = JsonDocument.Parse(File.ReadAllText(configPath));
var root = document.RootElement;
var mode = GetString(root, "dataLocationMode");
if (string.Equals(mode, "Portable", StringComparison.OrdinalIgnoreCase))
{
return Path.GetFullPath(
GetString(root, "portableDataPath")
?? Path.Combine(normalizedAppRoot, DesktopFolderName));
}
return Path.GetFullPath(GetString(root, "systemDataPath") ?? defaultSystemDataPath);
}
catch
{
return defaultSystemDataPath;
}
}
private static string? GetString(JsonElement element, string propertyName)
{
foreach (var property in element.EnumerateObject())
{
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase) &&
property.Value.ValueKind is JsonValueKind.String)
{
var value = property.Value.GetString();
return string.IsNullOrWhiteSpace(value) ? null : value;
}
}
return null;
}
}

View File

@@ -0,0 +1,77 @@
namespace LanMountainDesktop.Shared.IPC;
public static class AirAppRuntimePathResolver
{
private const string WindowsExecutableName = "LanMountainDesktop.AirAppRuntime.exe";
private const string UnixExecutableName = "LanMountainDesktop.AirAppRuntime";
private const string DllName = "LanMountainDesktop.AirAppRuntime.dll";
private static string ExecutableName => OperatingSystem.IsWindows()
? WindowsExecutableName
: UnixExecutableName;
public static string? ResolveExecutablePath(string? appRoot = null, string? hostBaseDirectory = null)
{
return EnumerateCandidates(appRoot, hostBaseDirectory)
.Select(Path.GetFullPath)
.Distinct(StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(File.Exists);
}
public static IEnumerable<string> EnumerateCandidates(string? appRoot = null, string? hostBaseDirectory = null)
{
foreach (var root in EnumerateRoots(appRoot, hostBaseDirectory))
{
yield return Path.Combine(root, ExecutableName);
yield return Path.Combine(root, DllName);
yield return Path.Combine(root, "AirAppRuntime", ExecutableName);
yield return Path.Combine(root, "AirAppRuntime", DllName);
}
var current = new DirectoryInfo(AppContext.BaseDirectory);
for (var depth = 0; depth < 8 && current is not null; depth++, current = current.Parent)
{
yield return Path.Combine(
current.FullName,
"LanMountainDesktop.AirAppRuntime",
"bin",
#if DEBUG
"Debug",
#else
"Release",
#endif
"net10.0",
ExecutableName);
yield return Path.Combine(
current.FullName,
"LanMountainDesktop.AirAppRuntime",
"bin",
#if DEBUG
"Debug",
#else
"Release",
#endif
"net10.0",
DllName);
}
}
private static IEnumerable<string> EnumerateRoots(string? appRoot, string? hostBaseDirectory)
{
if (!string.IsNullOrWhiteSpace(appRoot))
{
yield return Path.GetFullPath(appRoot);
}
if (!string.IsNullOrWhiteSpace(hostBaseDirectory))
{
var hostDirectory = Path.GetFullPath(hostBaseDirectory);
yield return hostDirectory;
yield return Path.GetFullPath(Path.Combine(hostDirectory, ".."));
}
yield return AppContext.BaseDirectory;
yield return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, ".."));
}
}

View File

@@ -0,0 +1,101 @@
using System.Diagnostics;
using System.Globalization;
namespace LanMountainDesktop.Shared.IPC;
public sealed record AirAppRuntimeStartRequest(
string? AppRoot,
int LauncherProcessId,
int RequesterProcessId,
string? DataRoot);
public static class AirAppRuntimeProcessStarter
{
public static Process? Start(AirAppRuntimeStartRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var runtimePath = AirAppRuntimePathResolver.ResolveExecutablePath(
request.AppRoot,
AppContext.BaseDirectory);
if (string.IsNullOrWhiteSpace(runtimePath))
{
return null;
}
var startInfo = CreateStartInfo(runtimePath);
AddOptionalArgument(startInfo, "--app-root", request.AppRoot);
AddOptionalArgument(startInfo, "--data-root", request.DataRoot);
AddIntArgument(startInfo, "--launcher-pid", request.LauncherProcessId);
AddIntArgument(startInfo, "--requester-pid", request.RequesterProcessId);
return Process.Start(startInfo);
}
public static ProcessStartInfo CreateStartInfo(string runtimePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(runtimePath);
var fullPath = Path.GetFullPath(runtimePath);
var startInfo = new ProcessStartInfo
{
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory
};
var extension = Path.GetExtension(fullPath);
if (OperatingSystem.IsWindows() &&
string.Equals(extension, ".dll", StringComparison.OrdinalIgnoreCase))
{
startInfo.FileName = ResolveDotNetHostPath();
startInfo.ArgumentList.Add(fullPath);
return startInfo;
}
if (!OperatingSystem.IsWindows() &&
string.Equals(extension, ".dll", StringComparison.OrdinalIgnoreCase))
{
startInfo.FileName = "dotnet";
startInfo.ArgumentList.Add(fullPath);
return startInfo;
}
startInfo.FileName = fullPath;
return startInfo;
}
private static string ResolveDotNetHostPath()
{
var programFiles = Environment.GetEnvironmentVariable("ProgramW6432") ??
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var programFilesCandidate = Path.Combine(programFiles, "dotnet", "dotnet.exe");
if (File.Exists(programFilesCandidate))
{
return programFilesCandidate;
}
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var perUserCandidate = Path.Combine(localAppData, "dotnet", "dotnet.exe");
return File.Exists(perUserCandidate) ? perUserCandidate : "dotnet";
}
private static void AddOptionalArgument(ProcessStartInfo startInfo, string name, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
startInfo.ArgumentList.Add(name);
startInfo.ArgumentList.Add(Path.GetFullPath(value));
}
private static void AddIntArgument(ProcessStartInfo startInfo, string name, int value)
{
if (value <= 0)
{
return;
}
startInfo.ArgumentList.Add(name);
startInfo.ArgumentList.Add(value.ToString(CultureInfo.InvariantCulture));
}
}

View File

@@ -6,7 +6,10 @@ public static class IpcConstants
public const string ProtocolVersion = "external-ipc-public-api.v1";
public const string AirAppLifecyclePipeName = "LanMountainDesktop.Launcher.AirApp.v1";
public const string AirAppRuntimePipeName = "LanMountainDesktop.AirAppRuntime.v1";
[Obsolete("Use AirAppRuntimePipeName. The lifecycle service is now hosted by LanMountainDesktop.AirAppRuntime.")]
public const string AirAppLifecyclePipeName = AirAppRuntimePipeName;
public const string AirAppLifecycleProtocolVersion = "air-app-lifecycle.v1";