mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
feat.airapp剥离启动器
This commit is contained in:
@@ -1,90 +0,0 @@
|
||||
namespace LanMountainDesktop.Launcher.AirApp;
|
||||
|
||||
internal sealed class AirAppHostLocator
|
||||
{
|
||||
private const string WindowsExecutableName = "LanMountainDesktop.AirAppHost.exe";
|
||||
private const string DllName = "LanMountainDesktop.AirAppHost.dll";
|
||||
|
||||
public string Resolve(string? packageRoot, string? hostPath = null)
|
||||
{
|
||||
foreach (var candidate in EnumerateCandidates(packageRoot, hostPath))
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Unable to find LanMountainDesktop.AirAppHost output.");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateCandidates(string? packageRoot, string? hostPath)
|
||||
{
|
||||
foreach (var root in EnumerateRoots(packageRoot, hostPath))
|
||||
{
|
||||
yield return Path.Combine(root, "AirAppHost", WindowsExecutableName);
|
||||
yield return Path.Combine(root, "AirAppHost", DllName);
|
||||
yield return Path.Combine(root, WindowsExecutableName);
|
||||
yield return Path.Combine(root, DllName);
|
||||
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
foreach (var deploymentDirectory in Directory.GetDirectories(root, "app-*", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
yield return Path.Combine(deploymentDirectory, "AirAppHost", WindowsExecutableName);
|
||||
yield return Path.Combine(deploymentDirectory, "AirAppHost", DllName);
|
||||
yield return Path.Combine(deploymentDirectory, WindowsExecutableName);
|
||||
yield return Path.Combine(deploymentDirectory, 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.AirAppHost",
|
||||
"bin",
|
||||
#if DEBUG
|
||||
"Debug",
|
||||
#else
|
||||
"Release",
|
||||
#endif
|
||||
"net10.0",
|
||||
WindowsExecutableName);
|
||||
|
||||
yield return Path.Combine(
|
||||
current.FullName,
|
||||
"LanMountainDesktop.AirAppHost",
|
||||
"bin",
|
||||
#if DEBUG
|
||||
"Debug",
|
||||
#else
|
||||
"Release",
|
||||
#endif
|
||||
"net10.0",
|
||||
DllName);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateRoots(string? packageRoot, string? hostPath)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(packageRoot))
|
||||
{
|
||||
yield return Path.GetFullPath(packageRoot);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hostPath))
|
||||
{
|
||||
var hostDirectory = Path.GetDirectoryName(Path.GetFullPath(hostPath));
|
||||
if (!string.IsNullOrWhiteSpace(hostDirectory))
|
||||
{
|
||||
yield return hostDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
yield return AppContext.BaseDirectory;
|
||||
yield return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, ".."));
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
namespace LanMountainDesktop.Launcher.AirApp;
|
||||
|
||||
internal static class AirAppInstanceKey
|
||||
{
|
||||
public static string Build(string appId, string? sourceComponentId, string? sourcePlacementId)
|
||||
{
|
||||
var normalizedAppId = Normalize(appId, "unknown");
|
||||
if (string.Equals(normalizedAppId, "world-clock", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{normalizedAppId}:clock-suite:global";
|
||||
}
|
||||
|
||||
var normalizedComponentId = Normalize(sourceComponentId, "none");
|
||||
var normalizedPlacementId = Normalize(sourcePlacementId, "none");
|
||||
return $"{normalizedAppId}:{normalizedComponentId}:{normalizedPlacementId}";
|
||||
}
|
||||
|
||||
private static string Normalize(string? value, string fallback)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? fallback
|
||||
: value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
namespace LanMountainDesktop.Launcher.AirApp;
|
||||
|
||||
internal interface IAirAppProcessStarter
|
||||
{
|
||||
Process? Start(string appId, string sessionId, string instanceKey, string? sourceComponentId, string? sourcePlacementId);
|
||||
}
|
||||
|
||||
internal sealed class AirAppProcessStarter : IAirAppProcessStarter
|
||||
{
|
||||
private readonly AirAppHostLocator _locator;
|
||||
private readonly Func<string?> _packageRootProvider;
|
||||
private readonly Func<string?> _hostPathProvider;
|
||||
private readonly Func<string?> _dataRootProvider;
|
||||
private readonly DotNetRuntimeProbeOptions? _runtimeProbeOptions;
|
||||
|
||||
public AirAppProcessStarter(
|
||||
AirAppHostLocator locator,
|
||||
Func<string?> packageRootProvider,
|
||||
Func<string?> hostPathProvider,
|
||||
Func<string?> dataRootProvider,
|
||||
DotNetRuntimeProbeOptions? runtimeProbeOptions = null)
|
||||
{
|
||||
_locator = locator;
|
||||
_packageRootProvider = packageRootProvider;
|
||||
_hostPathProvider = hostPathProvider;
|
||||
_dataRootProvider = dataRootProvider;
|
||||
_runtimeProbeOptions = runtimeProbeOptions;
|
||||
}
|
||||
|
||||
public Process? Start(
|
||||
string appId,
|
||||
string sessionId,
|
||||
string instanceKey,
|
||||
string? sourceComponentId,
|
||||
string? sourcePlacementId)
|
||||
{
|
||||
var hostPath = _locator.Resolve(_packageRootProvider(), _hostPathProvider());
|
||||
var startInfo = CreateStartInfo(hostPath, _runtimeProbeOptions);
|
||||
|
||||
AddArgument(startInfo, "--app-id", appId);
|
||||
AddArgument(startInfo, "--session-id", sessionId);
|
||||
AddArgument(startInfo, "--instance-key", instanceKey);
|
||||
AddArgument(startInfo, "--launcher-pipe", LanMountainDesktop.Shared.IPC.IpcConstants.AirAppLifecyclePipeName);
|
||||
var dataRoot = _dataRootProvider();
|
||||
if (!string.IsNullOrWhiteSpace(dataRoot))
|
||||
{
|
||||
AddArgument(startInfo, "--data-root", Path.GetFullPath(dataRoot));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sourceComponentId))
|
||||
{
|
||||
AddArgument(startInfo, "--source-component-id", sourceComponentId.Trim());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sourcePlacementId))
|
||||
{
|
||||
AddArgument(startInfo, "--source-placement-id", sourcePlacementId.Trim());
|
||||
}
|
||||
|
||||
Logger.Info(
|
||||
$"Starting AirAppHost. AppId='{appId}'; InstanceKey='{instanceKey}'; HostPath='{hostPath}'; DataRoot='{dataRoot ?? string.Empty}'.");
|
||||
var process = Process.Start(startInfo);
|
||||
if (process is not null)
|
||||
{
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Info(
|
||||
$"AirAppHost exited. AppId='{appId}'; InstanceKey='{instanceKey}'; ProcessId={process.Id}; ExitCode={process.ExitCode}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to log AirAppHost exit: {ex.Message}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
internal static ProcessStartInfo CreateStartInfo(
|
||||
string hostPath,
|
||||
DotNetRuntimeProbeOptions? runtimeProbeOptions = null)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Path.GetDirectoryName(hostPath) ?? AppContext.BaseDirectory
|
||||
};
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (string.Equals(Path.GetExtension(hostPath), ".exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (DotNetRuntimeProbe.IsFrameworkDependentWindowsApp(hostPath))
|
||||
{
|
||||
var executableRuntime = DotNetRuntimeProbe.Probe(runtimeProbeOptions);
|
||||
if (!executableRuntime.IsAvailable)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unable to start AirAppHost because the architecture-matched .NET 10 runtime was not found. " +
|
||||
executableRuntime.Message);
|
||||
}
|
||||
}
|
||||
|
||||
startInfo.FileName = hostPath;
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
var runtime = DotNetRuntimeProbe.Probe(runtimeProbeOptions);
|
||||
if (!runtime.IsAvailable || string.IsNullOrWhiteSpace(runtime.DotNetHostPath))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unable to start AirAppHost because the architecture-matched .NET 10 runtime was not found. " +
|
||||
runtime.Message);
|
||||
}
|
||||
|
||||
startInfo.FileName = runtime.DotNetHostPath;
|
||||
startInfo.ArgumentList.Add(hostPath);
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
startInfo.FileName = "dotnet";
|
||||
startInfo.ArgumentList.Add(hostPath);
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
|
||||
private static void AddArgument(ProcessStartInfo startInfo, string name, string value)
|
||||
{
|
||||
startInfo.ArgumentList.Add(name);
|
||||
startInfo.ArgumentList.Add(value);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.AirApp;
|
||||
|
||||
internal sealed class LauncherAirAppLifecycleIpcHost : IDisposable
|
||||
{
|
||||
private readonly PublicIpcHostService _host;
|
||||
|
||||
public LauncherAirAppLifecycleIpcHost(LauncherAirAppLifecycleService lifecycleService)
|
||||
{
|
||||
LifecycleService = lifecycleService;
|
||||
_host = new PublicIpcHostService(IpcConstants.AirAppLifecyclePipeName);
|
||||
_host.RegisterPublicService<IAirAppLifecycleService>(lifecycleService);
|
||||
}
|
||||
|
||||
public LauncherAirAppLifecycleService LifecycleService { get; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_host.Start();
|
||||
Logger.Info($"Air APP lifecycle IPC started. Pipe='{IpcConstants.AirAppLifecyclePipeName}'.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_host.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.AirApp;
|
||||
|
||||
internal sealed class LauncherAirAppLifecycleService : IAirAppLifecycleService
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly IAirAppProcessStarter _processStarter;
|
||||
private readonly Dictionary<string, ManagedAirAppInstance> _instances = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public LauncherAirAppLifecycleService(IAirAppProcessStarter processStarter)
|
||||
{
|
||||
_processStarter = processStarter;
|
||||
}
|
||||
|
||||
public Task<AirAppOperationResult> OpenAsync(AirAppOpenRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
var appId = Normalize(request.AppId, "unknown");
|
||||
var instanceKey = AirAppInstanceKey.Build(appId, request.SourceComponentId, request.SourcePlacementId);
|
||||
Logger.Info(
|
||||
$"Air APP open requested. AppId='{appId}'; InstanceKey='{instanceKey}'; RequesterProcessId={request.RequesterProcessId}.");
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
CleanupExitedInstances();
|
||||
|
||||
if (_instances.TryGetValue(instanceKey, out var existing) && IsProcessAlive(existing.ProcessId))
|
||||
{
|
||||
TryActivateProcess(existing.ProcessId);
|
||||
existing.Touch();
|
||||
return Task.FromResult(BuildResult(true, "activated_existing", "Activated existing Air APP instance.", existing));
|
||||
}
|
||||
|
||||
var sessionId = Guid.NewGuid().ToString("N");
|
||||
try
|
||||
{
|
||||
var process = _processStarter.Start(
|
||||
appId,
|
||||
sessionId,
|
||||
instanceKey,
|
||||
request.SourceComponentId,
|
||||
request.SourcePlacementId);
|
||||
if (process is null)
|
||||
{
|
||||
return Task.FromResult(BuildResult(false, "start_failed", "AirAppHost process was not created.", null));
|
||||
}
|
||||
|
||||
var instance = new ManagedAirAppInstance(
|
||||
instanceKey,
|
||||
appId,
|
||||
sessionId,
|
||||
process.Id,
|
||||
$"{appId} - Air APP",
|
||||
request.SourceComponentId,
|
||||
request.SourcePlacementId);
|
||||
_instances[instanceKey] = instance;
|
||||
Logger.Info($"Started Air APP. AppId='{appId}'; InstanceKey='{instanceKey}'; ProcessId={process.Id}.");
|
||||
return Task.FromResult(BuildResult(true, "started", "Started Air APP instance.", instance));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to start Air APP '{appId}': {ex.Message}");
|
||||
return Task.FromResult(BuildResult(false, "start_failed", ex.Message, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AirAppOperationResult> ActivateAsync(string instanceKey)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
CleanupExitedInstances();
|
||||
if (!_instances.TryGetValue(instanceKey, out var instance))
|
||||
{
|
||||
return Task.FromResult(BuildResult(false, "not_found", "Air APP instance was not found.", null));
|
||||
}
|
||||
|
||||
var accepted = TryActivateProcess(instance.ProcessId);
|
||||
instance.Touch();
|
||||
return Task.FromResult(BuildResult(
|
||||
accepted,
|
||||
accepted ? "activated" : "activation_failed",
|
||||
accepted ? "Air APP instance activated." : "Failed to activate Air APP instance.",
|
||||
instance));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AirAppOperationResult> CloseAsync(string instanceKey)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
CleanupExitedInstances();
|
||||
if (!_instances.TryGetValue(instanceKey, out var instance))
|
||||
{
|
||||
return Task.FromResult(BuildResult(false, "not_found", "Air APP instance was not found.", null));
|
||||
}
|
||||
|
||||
var accepted = TryCloseProcess(instance.ProcessId);
|
||||
instance.Touch();
|
||||
return Task.FromResult(BuildResult(
|
||||
accepted,
|
||||
accepted ? "close_requested" : "close_failed",
|
||||
accepted ? "Air APP close requested." : "Failed to request Air APP close.",
|
||||
instance));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AirAppInstanceInfo[]> GetInstancesAsync()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
CleanupExitedInstances();
|
||||
return Task.FromResult(_instances.Values.Select(static instance => instance.ToInfo()).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AirAppOperationResult> RegisterAsync(AirAppRegistrationRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
lock (_gate)
|
||||
{
|
||||
var instanceKey = string.IsNullOrWhiteSpace(request.InstanceKey)
|
||||
? AirAppInstanceKey.Build(request.AppId, request.SourceComponentId, request.SourcePlacementId)
|
||||
: request.InstanceKey.Trim();
|
||||
var instance = new ManagedAirAppInstance(
|
||||
instanceKey,
|
||||
Normalize(request.AppId, "unknown"),
|
||||
Normalize(request.SessionId, Guid.NewGuid().ToString("N")),
|
||||
request.ProcessId,
|
||||
Normalize(request.WindowTitle, $"{request.AppId} - Air APP"),
|
||||
request.SourceComponentId,
|
||||
request.SourcePlacementId);
|
||||
_instances[instanceKey] = instance;
|
||||
Logger.Info($"Registered Air APP. AppId='{instance.AppId}'; InstanceKey='{instanceKey}'; ProcessId={instance.ProcessId}.");
|
||||
return Task.FromResult(BuildResult(true, "registered", "Air APP instance registered.", instance));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AirAppOperationResult> UnregisterAsync(string instanceKey, int processId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_instances.TryGetValue(instanceKey, out var instance) &&
|
||||
(processId <= 0 || instance.ProcessId == processId))
|
||||
{
|
||||
_instances.Remove(instanceKey);
|
||||
Logger.Info($"Unregistered Air APP. InstanceKey='{instanceKey}'; ProcessId={processId}.");
|
||||
return Task.FromResult(BuildResult(true, "unregistered", "Air APP instance unregistered.", instance));
|
||||
}
|
||||
|
||||
return Task.FromResult(BuildResult(false, "not_found", "Air APP instance was not found.", null));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasLiveAirApps()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
CleanupExitedInstances();
|
||||
return _instances.Values.Any(static instance => IsProcessAlive(instance.ProcessId));
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupExitedInstances()
|
||||
{
|
||||
var exitedKeys = _instances
|
||||
.Where(static pair => !IsProcessAlive(pair.Value.ProcessId))
|
||||
.Select(static pair => pair.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in exitedKeys)
|
||||
{
|
||||
_instances.Remove(key);
|
||||
Logger.Info($"Pruned exited Air APP instance. InstanceKey='{key}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static AirAppOperationResult BuildResult(
|
||||
bool accepted,
|
||||
string code,
|
||||
string message,
|
||||
ManagedAirAppInstance? instance)
|
||||
{
|
||||
return new AirAppOperationResult(accepted, code, message, instance?.ToInfo());
|
||||
}
|
||||
|
||||
private static bool TryActivateProcess(int processId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = Process.GetProcessById(processId);
|
||||
if (process.HasExited)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
process.Refresh();
|
||||
var handle = process.MainWindowHandle;
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_ = ShowWindow(handle, SW_SHOWNORMAL);
|
||||
_ = SetForegroundWindow(handle);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryCloseProcess(int processId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = Process.GetProcessById(processId);
|
||||
if (process.HasExited)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return process.CloseMainWindow();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsProcessAlive(int processId)
|
||||
{
|
||||
if (processId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var process = Process.GetProcessById(processId);
|
||||
return !process.HasExited;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Normalize(string? value, string fallback)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? fallback
|
||||
: value.Trim();
|
||||
}
|
||||
|
||||
private const int SW_SHOWNORMAL = 1;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
private sealed class ManagedAirAppInstance
|
||||
{
|
||||
private readonly DateTimeOffset _startedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
public ManagedAirAppInstance(
|
||||
string instanceKey,
|
||||
string appId,
|
||||
string sessionId,
|
||||
int processId,
|
||||
string windowTitle,
|
||||
string? sourceComponentId,
|
||||
string? sourcePlacementId)
|
||||
{
|
||||
InstanceKey = instanceKey;
|
||||
AppId = appId;
|
||||
SessionId = sessionId;
|
||||
ProcessId = processId;
|
||||
WindowTitle = windowTitle;
|
||||
SourceComponentId = sourceComponentId;
|
||||
SourcePlacementId = sourcePlacementId;
|
||||
UpdatedAtUtc = _startedAtUtc;
|
||||
}
|
||||
|
||||
public string InstanceKey { get; }
|
||||
|
||||
public string AppId { get; }
|
||||
|
||||
public string SessionId { get; }
|
||||
|
||||
public int ProcessId { get; }
|
||||
|
||||
public string WindowTitle { get; }
|
||||
|
||||
public string? SourceComponentId { get; }
|
||||
|
||||
public string? SourcePlacementId { get; }
|
||||
|
||||
public DateTimeOffset UpdatedAtUtc { get; private set; }
|
||||
|
||||
public void Touch()
|
||||
{
|
||||
UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public AirAppInstanceInfo ToInfo()
|
||||
{
|
||||
return new AirAppInstanceInfo(
|
||||
InstanceKey,
|
||||
AppId,
|
||||
SessionId,
|
||||
ProcessId,
|
||||
WindowTitle,
|
||||
SourceComponentId,
|
||||
SourcePlacementId,
|
||||
IsProcessAlive(ProcessId),
|
||||
_startedAtUtc,
|
||||
UpdatedAtUtc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,13 +60,6 @@ public partial class App : Application
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.IsAirAppBrokerCommand)
|
||||
{
|
||||
_ = AirAppBrokerEntryHandler.RunAsync(desktop, context);
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.IsDebugMode && !context.IsPreviewCommand)
|
||||
{
|
||||
Logger.Info("Debug mode active; showing DevDebugWindow instead of normal launch flow.");
|
||||
|
||||
@@ -11,15 +11,7 @@ namespace LanMountainDesktop.Launcher;
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true)]
|
||||
[JsonSerializable(typeof(SignedFileMap))]
|
||||
[JsonSerializable(typeof(UpdateFileEntry))]
|
||||
[JsonSerializable(typeof(PlondsUpdateMetadata))]
|
||||
[JsonSerializable(typeof(PlondsFileMap))]
|
||||
[JsonSerializable(typeof(PlondsComponentEntry))]
|
||||
[JsonSerializable(typeof(PlondsFileEntry))]
|
||||
[JsonSerializable(typeof(PlondsHashDescriptor))]
|
||||
[JsonSerializable(typeof(SnapshotMetadata))]
|
||||
[JsonSerializable(typeof(InstallCheckpoint))]
|
||||
[JsonSerializable(typeof(AppVersionInfo))]
|
||||
[JsonSerializable(typeof(StartupProgressMessage))]
|
||||
[JsonSerializable(typeof(LauncherCoordinatorRequest))]
|
||||
@@ -37,11 +29,11 @@ namespace LanMountainDesktop.Launcher;
|
||||
[JsonSerializable(typeof(StartupAttemptRecord))]
|
||||
[JsonSerializable(typeof(PrivacyConfig))]
|
||||
[JsonSerializable(typeof(PrivacyAgreementState))]
|
||||
[JsonSerializable(typeof(LanMountainDesktop.Shared.Contracts.Update.InstallProgressReport))]
|
||||
[JsonSerializable(typeof(LanMountainDesktop.Shared.Contracts.Update.InstallCompleteReport))]
|
||||
[JsonSerializable(typeof(AirAppOpenRequest))]
|
||||
[JsonSerializable(typeof(AirAppRegistrationRequest))]
|
||||
[JsonSerializable(typeof(AirAppInstanceInfo))]
|
||||
[JsonSerializable(typeof(AirAppOperationResult))]
|
||||
[JsonSerializable(typeof(AirAppInstanceInfo[]))]
|
||||
[JsonSerializable(typeof(AirAppRuntimeControlResult))]
|
||||
[JsonSerializable(typeof(AirAppRuntimeStatus))]
|
||||
internal sealed partial class AppJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -4,14 +4,11 @@ namespace LanMountainDesktop.Launcher;
|
||||
|
||||
internal sealed class CommandContext
|
||||
{
|
||||
public const string AirAppBrokerCommand = "air-app-broker";
|
||||
|
||||
private const string LaunchSourceOptionName = "launch-source";
|
||||
|
||||
private static readonly string[] GuiCommands =
|
||||
[
|
||||
"launch",
|
||||
AirAppBrokerCommand,
|
||||
"preview-splash",
|
||||
"preview-error",
|
||||
"preview-update",
|
||||
@@ -62,15 +59,11 @@ internal sealed class CommandContext
|
||||
public bool IsPreviewCommand =>
|
||||
Command.StartsWith("preview-", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public bool IsAirAppBrokerCommand =>
|
||||
string.Equals(Command, AirAppBrokerCommand, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public bool IsGuiCommand =>
|
||||
GuiCommands.Contains(Command, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public bool IsMaintenanceCommand =>
|
||||
string.Equals(LaunchSource, "plugin-install", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(Command, "update", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(Command, "plugin", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public string? ExplicitAppRoot => GetOption("app-root");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
global using LanMountainDesktop.Launcher.AirApp;
|
||||
global using LanMountainDesktop.Launcher.Deployment;
|
||||
global using LanMountainDesktop.Launcher.Infrastructure;
|
||||
global using LanMountainDesktop.Launcher.Ipc;
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
using System.Buffers;
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Shared.Contracts.Update;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Ipc;
|
||||
|
||||
internal interface IUpdateProgressReporter
|
||||
{
|
||||
void ReportProgress(InstallProgressReport report);
|
||||
void ReportComplete(InstallCompleteReport report);
|
||||
}
|
||||
|
||||
internal sealed class LauncherUpdateProgressIpcServer : IUpdateProgressReporter, IDisposable
|
||||
{
|
||||
private const int LengthPrefixSize = 4;
|
||||
|
||||
private readonly string _pipeName;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private NamedPipeServerStream? _pipe;
|
||||
private Task? _listenTask;
|
||||
private volatile bool _clientConnected;
|
||||
|
||||
public LauncherUpdateProgressIpcServer(int launcherPid)
|
||||
{
|
||||
_pipeName = $"LanMountainDesktop_Update_{launcherPid}";
|
||||
}
|
||||
|
||||
public string PipeName => _pipeName;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_listenTask = Task.Run(AcceptConnectionAsync, _cts.Token);
|
||||
}
|
||||
|
||||
private async Task AcceptConnectionAsync()
|
||||
{
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_pipe = new NamedPipeServerStream(
|
||||
_pipeName,
|
||||
PipeDirection.Out,
|
||||
1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
await _pipe.WaitForConnectionAsync(_cts.Token).ConfigureAwait(false);
|
||||
_clientConnected = true;
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Update progress IPC listen error: {ex.Message}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(200, _cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ReportProgress(InstallProgressReport report)
|
||||
{
|
||||
if (!_clientConnected || _pipe is null || !_pipe.IsConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WriteMessage(_pipe, JsonSerializer.Serialize(report, AppJsonContext.Default.InstallProgressReport));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to report progress via IPC: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void ReportComplete(InstallCompleteReport report)
|
||||
{
|
||||
if (!_clientConnected || _pipe is null || !_pipe.IsConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WriteMessage(_pipe, JsonSerializer.Serialize(report, AppJsonContext.Default.InstallCompleteReport));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to report completion via IPC: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteMessage(Stream stream, string json)
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes(json);
|
||||
var lengthPrefix = BitConverter.GetBytes(payload.Length);
|
||||
stream.Write(lengthPrefix, 0, LengthPrefixSize);
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
stream.Flush();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
try
|
||||
{
|
||||
_pipe?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_listenTask?.Wait(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
<Target Name="CopyPublicKeyToLauncherDir" AfterTargets="Build">
|
||||
<PropertyGroup>
|
||||
<PublicKeySource>$(MSBuildProjectDirectory)\Assets\public-key.pem</PublicKeySource>
|
||||
<PublicKeyDestDir>$(OutDir).launcher\update</PublicKeyDestDir>
|
||||
<PublicKeyDestDir>$(OutDir).Launcher\update</PublicKeyDestDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(PublicKeyDestDir)" />
|
||||
<Copy SourceFiles="$(PublicKeySource)" DestinationFolder="$(PublicKeyDestDir)" SkipUnchangedFiles="true" />
|
||||
@@ -55,7 +55,7 @@
|
||||
<Target Name="CopyPublicKeyToPublishDir" AfterTargets="Publish">
|
||||
<PropertyGroup>
|
||||
<PublishedKeySource>$(MSBuildProjectDirectory)\Assets\public-key.pem</PublishedKeySource>
|
||||
<PublishedKeyDestDir>$(PublishDir).launcher\update</PublishedKeyDestDir>
|
||||
<PublishedKeyDestDir>$(PublishDir).Launcher\update</PublishedKeyDestDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(PublishedKeyDestDir)" />
|
||||
<Copy SourceFiles="$(PublishedKeySource)" DestinationFolder="$(PublishedKeyDestDir)" SkipUnchangedFiles="true" />
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace LanMountainDesktop.Launcher.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 更新频道
|
||||
/// </summary>
|
||||
public enum UpdateChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// 正式版 - 只检查 prerelease=false 的版本
|
||||
/// </summary>
|
||||
Stable,
|
||||
|
||||
/// <summary>
|
||||
/// 预览版 - 检查所有版本(包括 prerelease=true)
|
||||
/// </summary>
|
||||
Preview
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace LanMountainDesktop.Launcher.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 更新检查结果
|
||||
/// </summary>
|
||||
public sealed class UpdateCheckResult
|
||||
{
|
||||
public bool HasUpdate { get; init; }
|
||||
public string? LatestVersion { get; init; }
|
||||
public string? CurrentVersion { get; init; }
|
||||
public ReleaseInfo? Release { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
@@ -1,29 +1,5 @@
|
||||
namespace LanMountainDesktop.Launcher.Models;
|
||||
|
||||
internal sealed class SignedFileMap
|
||||
{
|
||||
public string? FromVersion { get; set; }
|
||||
|
||||
public string? ToVersion { get; set; }
|
||||
|
||||
public string? Platform { get; set; }
|
||||
|
||||
public string? Arch { get; set; }
|
||||
|
||||
public List<UpdateFileEntry> Files { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class UpdateFileEntry
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
public string? ArchivePath { get; set; }
|
||||
|
||||
public string Action { get; set; } = "replace";
|
||||
|
||||
public string? Sha256 { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class SnapshotMetadata
|
||||
{
|
||||
public string SnapshotId { get; set; } = string.Empty;
|
||||
@@ -40,124 +16,3 @@ internal sealed class SnapshotMetadata
|
||||
|
||||
public string Status { get; set; } = "pending";
|
||||
}
|
||||
|
||||
internal sealed class InstallCheckpoint
|
||||
{
|
||||
public string SnapshotId { get; set; } = string.Empty;
|
||||
|
||||
public string SourceVersion { get; set; } = string.Empty;
|
||||
|
||||
public string? TargetVersion { get; set; }
|
||||
|
||||
public string? SourceDirectory { get; set; }
|
||||
|
||||
public string TargetDirectory { get; set; } = string.Empty;
|
||||
|
||||
public bool IsInitialDeployment { get; set; }
|
||||
|
||||
public int AppliedCount { get; set; }
|
||||
|
||||
public int VerifiedCount { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class UpdateApplyResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
public string? FromVersion { get; init; }
|
||||
|
||||
public string? ToVersion { get; init; }
|
||||
|
||||
public string? RolledBackTo { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class PlondsUpdateMetadata
|
||||
{
|
||||
public string? DistributionId { get; set; }
|
||||
|
||||
public string? Channel { get; set; }
|
||||
|
||||
public string? SubChannel { get; set; }
|
||||
|
||||
public string? FromVersion { get; set; }
|
||||
|
||||
public string? ToVersion { get; set; }
|
||||
|
||||
public string? FileMapPath { get; set; }
|
||||
|
||||
public string? FileMapSignaturePath { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class PlondsFileMap
|
||||
{
|
||||
public string? DistributionId { get; set; }
|
||||
|
||||
public string? FromVersion { get; set; }
|
||||
|
||||
public string? ToVersion { get; set; }
|
||||
|
||||
public string? Version { get; set; }
|
||||
|
||||
public string? Platform { get; set; }
|
||||
|
||||
public string? Arch { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; } = [];
|
||||
|
||||
public List<PlondsComponentEntry> Components { get; set; } = [];
|
||||
|
||||
public List<PlondsFileEntry> Files { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class PlondsComponentEntry
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string? Version { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; } = [];
|
||||
|
||||
public List<PlondsFileEntry> Files { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class PlondsFileEntry
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
public string? Action { get; set; } = "replace";
|
||||
|
||||
public string? Url { get; set; }
|
||||
|
||||
public string? ObjectUrl { get; set; }
|
||||
|
||||
public string? ObjectPath { get; set; }
|
||||
|
||||
public string? ObjectKey { get; set; }
|
||||
|
||||
public string? ArchivePath { get; set; }
|
||||
|
||||
public string? Sha256 { get; set; }
|
||||
|
||||
public string? Sha512 { get; set; }
|
||||
|
||||
public string? Sha512Base64 { get; set; }
|
||||
|
||||
public byte[]? Sha512Bytes { get; set; }
|
||||
|
||||
public PlondsHashDescriptor? Hash { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class PlondsHashDescriptor
|
||||
{
|
||||
public string? Algorithm { get; set; }
|
||||
|
||||
public string? Value { get; set; }
|
||||
|
||||
public byte[]? Bytes { get; set; }
|
||||
}
|
||||
|
||||
@@ -57,14 +57,6 @@
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Launcher (Update Check)": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "update check",
|
||||
"workingDirectory": "$(SolutionDir)",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Launcher (Plugin Install)": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "plugin install <path-to-plugin.laapp>",
|
||||
|
||||
83
LanMountainDesktop.Launcher/Shell/AirAppRuntimeBridge.cs
Normal file
83
LanMountainDesktop.Launcher/Shell/AirAppRuntimeBridge.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Shell;
|
||||
|
||||
internal sealed class AirAppRuntimeBridge
|
||||
{
|
||||
private const int ConnectAttempts = 8;
|
||||
|
||||
private readonly string _appRoot;
|
||||
private readonly string? _dataRoot;
|
||||
|
||||
public AirAppRuntimeBridge(string appRoot, string? dataRoot)
|
||||
{
|
||||
_appRoot = appRoot;
|
||||
_dataRoot = dataRoot;
|
||||
}
|
||||
|
||||
public async Task EnsureStartedAsync()
|
||||
{
|
||||
if (await TryGetStatusAsync().ConfigureAwait(false) is not null)
|
||||
{
|
||||
Logger.Info("AirApp Runtime is already available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var process = AirAppRuntimeProcessStarter.Start(new AirAppRuntimeStartRequest(
|
||||
_appRoot,
|
||||
Environment.ProcessId,
|
||||
0,
|
||||
_dataRoot));
|
||||
Logger.Info($"AirApp Runtime start requested. Pid={(process is null ? -1 : process.Id)}; AppRoot='{_appRoot}'.");
|
||||
|
||||
for (var attempt = 1; attempt <= ConnectAttempts; attempt++)
|
||||
{
|
||||
if (await TryGetStatusAsync().ConfigureAwait(false) is not null)
|
||||
{
|
||||
Logger.Info("AirApp Runtime IPC is ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.Warn("AirApp Runtime did not become ready after pre-start; Host fallback remains available.");
|
||||
}
|
||||
|
||||
public async Task AttachHostAsync(int hostProcessId)
|
||||
{
|
||||
if (hostProcessId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var client = new LanMountainDesktopIpcClient();
|
||||
await client.ConnectAsync(IpcConstants.AirAppRuntimePipeName).ConfigureAwait(false);
|
||||
var proxy = client.CreateProxy<IAirAppRuntimeControlService>();
|
||||
var result = await proxy.AttachHostAsync(hostProcessId).ConfigureAwait(false);
|
||||
Logger.Info($"AirApp Runtime host attach completed. Accepted={result.Accepted}; Code='{result.Code}'; HostPid={hostProcessId}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to attach Host to AirApp Runtime: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<AirAppRuntimeStatus?> TryGetStatusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new LanMountainDesktopIpcClient();
|
||||
await client.ConnectAsync(IpcConstants.AirAppRuntimePipeName).ConfigureAwait(false);
|
||||
var proxy = client.CreateProxy<IAirAppRuntimeControlService>();
|
||||
return await proxy.GetStatusAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Launcher.Views;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Shell.EntryHandlers;
|
||||
@@ -30,52 +28,3 @@ internal static class LaunchEntryHandler
|
||||
SplashWindow splashWindow) =>
|
||||
LauncherCompositionRoot.RunOrchestratorWithSplashAsync(desktop, context, splashWindow);
|
||||
}
|
||||
|
||||
internal static class AirAppBrokerEntryHandler
|
||||
{
|
||||
public static async Task RunAsync(IClassicDesktopStyleApplicationLifetime desktop, CommandContext context)
|
||||
{
|
||||
var appRoot = Commands.ResolveAppRoot(context);
|
||||
var requesterPid = context.GetIntOption("requester-pid", 0);
|
||||
var dataLocationResolver = new DataLocationResolver(appRoot);
|
||||
Logger.Info($"Air APP broker starting. AppRoot='{appRoot}'; RequesterPid={requesterPid}.");
|
||||
|
||||
using var airAppIpcHost = new LauncherAirAppLifecycleIpcHost(
|
||||
new LauncherAirAppLifecycleService(
|
||||
new AirAppProcessStarter(
|
||||
new AirAppHostLocator(),
|
||||
() => appRoot,
|
||||
() => null,
|
||||
() => dataLocationResolver.ResolveDataRoot())));
|
||||
airAppIpcHost.Start();
|
||||
|
||||
while (ShouldKeepAlive(requesterPid, airAppIpcHost.LifecycleService))
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.Info("Air APP broker exiting.");
|
||||
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0), DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
internal static bool ShouldKeepAirAppBrokerAlive(int requesterPid, LauncherAirAppLifecycleService lifecycleService)
|
||||
{
|
||||
if (requesterPid <= 0)
|
||||
{
|
||||
return lifecycleService.HasLiveAirApps();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var process = System.Diagnostics.Process.GetProcessById(requesterPid);
|
||||
return !process.HasExited || lifecycleService.HasLiveAirApps();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return lifecycleService.HasLiveAirApps();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldKeepAlive(int requesterPid, LauncherAirAppLifecycleService lifecycleService) =>
|
||||
ShouldKeepAirAppBrokerAlive(requesterPid, lifecycleService);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ internal static class LauncherGuiCoordinator
|
||||
var startupAttemptRegistry = new StartupAttemptRegistry();
|
||||
var coordinatorPipeName = LauncherCoordinatorIpcServer.CreatePipeName();
|
||||
var successPolicy = LauncherOrchestrator.ResolveSuccessPolicyKey(context);
|
||||
var airAppRuntimeBridge = new AirAppRuntimeBridge(appRoot, dataLocationResolver.ResolveDataRoot());
|
||||
await airAppRuntimeBridge.EnsureStartedAsync().ConfigureAwait(false);
|
||||
|
||||
if (!startupAttemptRegistry.TryReserveCoordinator(
|
||||
context.LaunchSource,
|
||||
successPolicy,
|
||||
@@ -44,15 +47,6 @@ internal static class LauncherGuiCoordinator
|
||||
return;
|
||||
}
|
||||
|
||||
using var airAppIpcHost = new LauncherAirAppLifecycleIpcHost(
|
||||
new LauncherAirAppLifecycleService(
|
||||
new AirAppProcessStarter(
|
||||
new AirAppHostLocator(),
|
||||
() => appRoot,
|
||||
() => null,
|
||||
() => dataLocationResolver.ResolveDataRoot())));
|
||||
airAppIpcHost.Start();
|
||||
|
||||
using var coordinatorServer = new LauncherCoordinatorIpcServer(
|
||||
coordinatorPipeName,
|
||||
BuildCoordinatorStatusFromAttempt(reservedAttempt),
|
||||
@@ -129,7 +123,8 @@ internal static class LauncherGuiCoordinator
|
||||
if (result.Success)
|
||||
{
|
||||
var hostPid = ResolveManagedHostPid(result, startupAttemptRegistry.GetOwnedAttempt()?.HostPid ?? 0);
|
||||
await WaitForManagedProcessesToExitAsync(hostPid, airAppIpcHost.LifecycleService).ConfigureAwait(false);
|
||||
await airAppRuntimeBridge.AttachHostAsync(hostPid).ConfigureAwait(false);
|
||||
await WaitForHostProcessToExitAsync(hostPid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
|
||||
@@ -173,17 +168,15 @@ internal static class LauncherGuiCoordinator
|
||||
return fallbackHostPid;
|
||||
}
|
||||
|
||||
private static async Task WaitForManagedProcessesToExitAsync(
|
||||
int hostPid,
|
||||
LauncherAirAppLifecycleService airAppLifecycleService)
|
||||
private static async Task WaitForHostProcessToExitAsync(int hostPid)
|
||||
{
|
||||
Logger.Info($"Launcher entering managed background lifetime. HostPid={hostPid}.");
|
||||
while (TryGetLiveProcess(hostPid) || airAppLifecycleService.HasLiveAirApps())
|
||||
Logger.Info($"Launcher entering host background lifetime. HostPid={hostPid}.");
|
||||
while (TryGetLiveProcess(hostPid))
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.Info("Launcher managed background lifetime completed; no host or Air APP process remains.");
|
||||
Logger.Info("Launcher host background lifetime completed; host process is gone.");
|
||||
}
|
||||
|
||||
private static async Task<LauncherResult> AttachToExistingCoordinatorAsync(
|
||||
|
||||
Reference in New Issue
Block a user