mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-23 01:44:26 +08:00
Introduce HostLaunchPlan and refine launch flow
Add HostLaunchPlan/HostLaunchPlanBuilder to encapsulate host path, package root, working dir, forwarded args and env; add unit tests for builder. Refactor LauncherFlowCoordinator to use HostLaunchPlan when starting hosts, improve IPC handling and startup logic (shorter soft/hard timeouts, more frequent reconnects and shell status polling, activation recovery via existing host). Move argument formatting and environment setup into the plan, include package/working/args metadata in start attempts. Update Commands to prefer ProcessPath for launcher base directory. App and Program: start single-instance activation listener earlier and harden ActivateMainWindow to handle shell initialization state and return richer activation status codes. SingleInstanceService: signal listener readiness (ManualResetEventSlim) and wait briefly when starting, and dispose it. Various logging and minor error handling improvements.
This commit is contained in:
@@ -85,6 +85,7 @@ public partial class App : Application
|
||||
private LoadingStateReporter? _loadingStateReporter;
|
||||
private bool _singleInstanceReleased;
|
||||
private int _forcedExitScheduled;
|
||||
private volatile bool _desktopShellInitializationStarted;
|
||||
private bool _mainWindowOpened;
|
||||
private bool _trayInitialized;
|
||||
private readonly object _launcherProgressLock = new();
|
||||
@@ -184,6 +185,7 @@ public partial class App : Application
|
||||
RegisterUiUnhandledExceptionGuard();
|
||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||
InitializePublicIpc();
|
||||
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
|
||||
_ = InitializeLauncherIpcAsync();
|
||||
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
||||
|
||||
@@ -324,6 +326,7 @@ public partial class App : Application
|
||||
|
||||
private void InitializeDesktopShell()
|
||||
{
|
||||
_desktopShellInitializationStarted = true;
|
||||
_desktopShellHost ??= new DesktopShellHost(
|
||||
InitializePluginRuntime,
|
||||
InitializeTrayIcon,
|
||||
@@ -801,10 +804,16 @@ public partial class App : Application
|
||||
Resources["AppFontFamily"] = fontFamily;
|
||||
}
|
||||
|
||||
private void ActivateMainWindow()
|
||||
internal void ActivateMainWindow()
|
||||
{
|
||||
AppLogger.Info("SingleInstance", $"Activation callback received. Pid={Environment.ProcessId}.");
|
||||
|
||||
if (!_desktopShellInitializationStarted && _mainWindow is null)
|
||||
{
|
||||
AppLogger.Info("SingleInstance", "Activation acknowledged while desktop shell is still initializing.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var restored = Dispatcher.UIThread.CheckAccess()
|
||||
@@ -815,7 +824,8 @@ public partial class App : Application
|
||||
|
||||
if (!restored)
|
||||
{
|
||||
throw new InvalidOperationException("Main window restore failed in activation callback.");
|
||||
AppLogger.Warn("SingleInstance", "Activation callback could not restore the main window yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Info("SingleInstance", "Activation callback completed successfully.");
|
||||
@@ -823,7 +833,6 @@ public partial class App : Application
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("SingleInstance", "Activation callback failed while restoring the desktop shell.", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1758,6 +1767,15 @@ public partial class App : Application
|
||||
|
||||
internal PublicShellActivationResult TryActivateMainWindowWithStatusFromExternalIpc(string source)
|
||||
{
|
||||
if (!_desktopShellInitializationStarted && _mainWindow is null)
|
||||
{
|
||||
return new PublicShellActivationResult(
|
||||
false,
|
||||
"startup_pending",
|
||||
"Desktop process is running, but the shell has not started yet.",
|
||||
GetPublicShellStatus());
|
||||
}
|
||||
|
||||
var restored = RestoreOrCreateMainWindowCore(showSingleInstanceNotice: false, source);
|
||||
var status = GetPublicShellStatus();
|
||||
if (restored)
|
||||
@@ -1770,12 +1788,17 @@ public partial class App : Application
|
||||
return new PublicShellActivationResult(false, "shutdown_in_progress", "Desktop is shutting down.", status);
|
||||
}
|
||||
|
||||
var code = status.PublicIpcReady && (!status.MainWindowOpened || !status.DesktopVisible)
|
||||
? "shell_not_ready"
|
||||
: "activation_failed";
|
||||
var message = code == "shell_not_ready"
|
||||
? "Desktop process is running, but the shell is not ready for activation yet."
|
||||
: "Desktop window activation failed.";
|
||||
var code = status.PublicIpcReady && (!status.MainWindowCreated || !status.MainWindowOpened)
|
||||
? "startup_pending"
|
||||
: status.PublicIpcReady && !status.DesktopVisible
|
||||
? "shell_not_ready"
|
||||
: "activation_failed";
|
||||
var message = code switch
|
||||
{
|
||||
"startup_pending" => "Desktop process is running, but the shell is still creating the main window.",
|
||||
"shell_not_ready" => "Desktop process is running, but the shell is not ready for activation yet.",
|
||||
_ => "Desktop window activation failed."
|
||||
};
|
||||
return new PublicShellActivationResult(false, code, message, status);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,16 @@ public sealed class Program
|
||||
StartupRenderMode = renderMode;
|
||||
AppLogger.Info("Startup", $"Resolved render mode '{renderMode}'.");
|
||||
App.CurrentSingleInstanceService = singleInstance;
|
||||
singleInstance.StartActivationListener(() =>
|
||||
{
|
||||
if (Avalonia.Application.Current is App app)
|
||||
{
|
||||
app.ActivateMainWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Info("SingleInstance", "Activation acknowledged before Avalonia App was ready.");
|
||||
});
|
||||
BuildAvaloniaApp(renderMode).StartWithClassicDesktopLifetime(args);
|
||||
AppLogger.Info("Startup", "Application exited normally.");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class SingleInstanceService : IDisposable
|
||||
private readonly Mutex _mutex;
|
||||
private readonly string _pipeName;
|
||||
private readonly CancellationTokenSource _listenCts = new();
|
||||
private readonly ManualResetEventSlim _listenerReady = new(false);
|
||||
private bool _ownsMutex;
|
||||
private bool _disposed;
|
||||
private Task? _listenTask;
|
||||
@@ -64,6 +65,7 @@ public sealed class SingleInstanceService : IDisposable
|
||||
"SingleInstance",
|
||||
$"Starting activation listener. Pipe='{_pipeName}'; Pid={Environment.ProcessId}; OwnsMutex={_ownsMutex}.");
|
||||
_listenTask = Task.Run(() => ListenForActivationAsync(onActivationRequested, _listenCts.Token));
|
||||
_listenerReady.Wait(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
public bool TryNotifyPrimaryInstance(TimeSpan timeout)
|
||||
@@ -142,6 +144,7 @@ public sealed class SingleInstanceService : IDisposable
|
||||
}
|
||||
|
||||
_listenCts.Dispose();
|
||||
_listenerReady.Dispose();
|
||||
if (_ownsMutex)
|
||||
{
|
||||
try
|
||||
@@ -170,6 +173,7 @@ public sealed class SingleInstanceService : IDisposable
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
_listenerReady.Set();
|
||||
await server.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
var buffer = new byte[1];
|
||||
var readBytes = await server.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user