mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Add launcher coordinator IPC and startup reservation
Introduce a launcher coordinator to reserve startup ownership and prevent duplicate host launches. Adds a NamedPipe-based IPC server/client (LauncherCoordinatorIpcServer/Client), coordinator messages/models, and PublicShellStatus/activation types for richer shell reporting. Enhances StartupAttemptRecord and StartupAttemptRegistry to track coordinator pid/pipe, heartbeat, reserved-before-host-start, and public IPC status, plus new reservation/heartbeat APIs and takeover logic. Wire coordinator into App and LauncherFlowCoordinator to attach secondary launchers, publish coordinator status, probe existing hosts, and include more detailed launch result details. Also adds unit tests and docs describing coordinator and startup visuals behavior.
This commit is contained in:
@@ -71,6 +71,7 @@ public partial class App : Application
|
||||
private ShutdownIntent _shutdownIntent;
|
||||
|
||||
private DesktopTrayService? _desktopTrayService;
|
||||
private DispatcherTimer? _shellRecoveryTimer;
|
||||
private PluginRuntimeService? _pluginRuntimeService;
|
||||
private MainWindow? _mainWindow;
|
||||
private TransparentOverlayWindow? _transparentOverlayWindow;
|
||||
@@ -478,6 +479,7 @@ public partial class App : Application
|
||||
private void InitializeTrayIcon()
|
||||
{
|
||||
EnsureDesktopTrayService();
|
||||
_desktopTrayService?.StartWatchdog();
|
||||
_trayInitialized = _desktopTrayService?.EnsureReady("Startup") == true;
|
||||
if (_trayInitialized)
|
||||
{
|
||||
@@ -525,14 +527,67 @@ public partial class App : Application
|
||||
OnTrayRestartClick,
|
||||
OnTrayExitClick);
|
||||
_desktopTrayService.StateChanged += OnTrayAvailabilityStateChanged;
|
||||
_desktopTrayService.StartWatchdog();
|
||||
EnsureShellRecoveryWatchdog();
|
||||
}
|
||||
|
||||
private void EnsureShellRecoveryWatchdog()
|
||||
{
|
||||
_shellRecoveryTimer ??= new DispatcherTimer(
|
||||
TimeSpan.FromSeconds(10),
|
||||
DispatcherPriority.Background,
|
||||
OnShellRecoveryWatchdogTick);
|
||||
|
||||
if (!_shellRecoveryTimer.IsEnabled)
|
||||
{
|
||||
_shellRecoveryTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void StopShellRecoveryWatchdog()
|
||||
{
|
||||
if (_shellRecoveryTimer?.IsEnabled == true)
|
||||
{
|
||||
_shellRecoveryTimer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShellRecoveryWatchdogTick(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
if (_shutdownIntent != ShutdownIntent.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureTrayReady("ShellRecoveryWatchdog");
|
||||
|
||||
if (!ShouldShowMainWindowInTaskbar())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_desktopShellState != DesktopShellState.ForegroundDesktop)
|
||||
{
|
||||
EnsureTaskbarEntry("ShellRecoveryWatchdog");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_mainWindow is not null && _mainWindow.IsVisible && !_mainWindow.ShowInTaskbar)
|
||||
{
|
||||
_mainWindow.ShowInTaskbar = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnsureTrayReady(string reason)
|
||||
{
|
||||
EnsureDesktopTrayService();
|
||||
var wasReady = _trayInitialized;
|
||||
var ready = _desktopTrayService?.EnsureReady(reason) == true;
|
||||
_trayInitialized = ready;
|
||||
if (ready)
|
||||
if (ready && !wasReady)
|
||||
{
|
||||
ReportStartupProgress(StartupStage.TrayReady, 75, "Tray ready.");
|
||||
}
|
||||
@@ -544,9 +599,25 @@ public partial class App : Application
|
||||
{
|
||||
_trayInitialized = state == TrayAvailabilityState.Ready;
|
||||
|
||||
if (state == TrayAvailabilityState.Failed && _desktopShellState == DesktopShellState.TrayOnly)
|
||||
if (state != TrayAvailabilityState.Failed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_desktopShellState == DesktopShellState.TrayOnly)
|
||||
{
|
||||
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayAvailabilityFailed");
|
||||
return;
|
||||
}
|
||||
|
||||
var foregroundVisible = _mainWindow?.IsVisible == true &&
|
||||
_mainWindow.WindowState != WindowState.Minimized;
|
||||
var taskbarUsable = BuildPublicTaskbarStatus().IsUsable;
|
||||
if (!foregroundVisible &&
|
||||
!taskbarUsable &&
|
||||
(_desktopTrayService?.ConsecutiveRecoveryFailures ?? 0) >= 3)
|
||||
{
|
||||
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayAvailabilityRepeatedFailure");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,7 +807,6 @@ public partial class App : Application
|
||||
mainWindow.PlayEnterAnimation();
|
||||
}, DispatcherPriority.Background);
|
||||
|
||||
_desktopTrayService?.StopWatchdog();
|
||||
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
|
||||
AppLogger.Info(
|
||||
"DesktopShell",
|
||||
@@ -864,6 +934,23 @@ public partial class App : Application
|
||||
{
|
||||
RefreshFusedDesktopMenuItemVisibility();
|
||||
}
|
||||
|
||||
var showInTaskbarChanged =
|
||||
refreshAll ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ShowInTaskbar), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (showInTaskbarChanged)
|
||||
{
|
||||
EnsureTrayReady("SettingsChanged");
|
||||
if (ShouldShowMainWindowInTaskbar())
|
||||
{
|
||||
EnsureTaskbarEntry("SettingsChanged");
|
||||
}
|
||||
else if (_mainWindow is not null && _mainWindow.IsVisible)
|
||||
{
|
||||
_mainWindow.ShowInTaskbar = false;
|
||||
}
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
@@ -980,6 +1067,7 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
_exitCleanupCompleted = true;
|
||||
StopShellRecoveryWatchdog();
|
||||
_settingsFacade.Settings.Changed -= OnSettingsChanged;
|
||||
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
|
||||
|
||||
@@ -1158,7 +1246,6 @@ public partial class App : Application
|
||||
case RestartPresentationMode.Minimized:
|
||||
mainWindow.ShowInTaskbar = true;
|
||||
mainWindow.WindowState = WindowState.Minimized;
|
||||
_desktopTrayService?.StopWatchdog();
|
||||
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, "StartupRestartPresentation");
|
||||
ReportStartupProgressSync(StartupStage.BackgroundReady, 95, "Background ready.");
|
||||
return true;
|
||||
@@ -1300,6 +1387,24 @@ public partial class App : Application
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ShouldShowMainWindowInTaskbar())
|
||||
{
|
||||
EnsureTrayReady($"TaskbarBackground:{source}");
|
||||
mainWindow.ShowInTaskbar = true;
|
||||
if (!mainWindow.IsVisible)
|
||||
{
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
mainWindow.WindowState = WindowState.Minimized;
|
||||
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, source);
|
||||
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready via taskbar.");
|
||||
AppLogger.Info(
|
||||
"DesktopShell",
|
||||
$"Main window minimized to taskbar because taskbar entry is enabled. Source='{source}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureTrayReady($"HideToTray:{source}"))
|
||||
{
|
||||
RecoverFromTrayUnavailable(mainWindow, source);
|
||||
@@ -1308,7 +1413,6 @@ public partial class App : Application
|
||||
|
||||
mainWindow.ShowInTaskbar = false;
|
||||
mainWindow.Hide();
|
||||
_desktopTrayService?.StartWatchdog();
|
||||
SetDesktopShellState(DesktopShellState.TrayOnly, source);
|
||||
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready.");
|
||||
AppLogger.Info(
|
||||
@@ -1345,7 +1449,6 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
mainWindow.WindowState = WindowState.Minimized;
|
||||
_desktopTrayService?.StopWatchdog();
|
||||
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, $"TrayFallbackTaskbar:{source}");
|
||||
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready via taskbar fallback.");
|
||||
return;
|
||||
@@ -1373,7 +1476,6 @@ public partial class App : Application
|
||||
mainWindow.Activate();
|
||||
mainWindow.Topmost = true;
|
||||
mainWindow.Topmost = false;
|
||||
_desktopTrayService?.StopWatchdog();
|
||||
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"TrayFallbackForeground:{source}");
|
||||
ReportStartupProgress(StartupStage.DesktopVisible, 100, "Desktop restored because tray was unavailable.");
|
||||
}
|
||||
@@ -1383,6 +1485,48 @@ public partial class App : Application
|
||||
return _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ShowInTaskbar;
|
||||
}
|
||||
|
||||
private bool EnsureTaskbarEntry(string source)
|
||||
{
|
||||
if (!ShouldShowMainWindowInTaskbar())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
AppLogger.Warn("DesktopShell", $"Taskbar repair skipped because desktop lifetime is unavailable. Source='{source}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mainWindow = GetOrCreateMainWindow(desktop, $"TaskbarRepair:{source}");
|
||||
mainWindow.ShowInTaskbar = true;
|
||||
|
||||
if (!mainWindow.IsVisible)
|
||||
{
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
if (_desktopShellState != DesktopShellState.ForegroundDesktop)
|
||||
{
|
||||
mainWindow.WindowState = WindowState.Minimized;
|
||||
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, $"TaskbarRepair:{source}");
|
||||
ReportStartupProgress(StartupStage.BackgroundReady, 95, "Background ready via taskbar repair.");
|
||||
}
|
||||
|
||||
AppLogger.Info(
|
||||
"DesktopShell",
|
||||
$"Taskbar entry ensured. Source='{source}'; IsVisible={mainWindow.IsVisible}; ShowInTaskbar={mainWindow.ShowInTaskbar}; WindowState='{mainWindow.WindowState}'.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("DesktopShell", $"Failed to ensure taskbar entry. Source='{source}'.", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDesktopShellState(DesktopShellState state, string source)
|
||||
{
|
||||
if (_desktopShellState == state)
|
||||
@@ -1434,7 +1578,72 @@ public partial class App : Application
|
||||
|
||||
internal bool TryActivateMainWindowFromExternalIpc(string source)
|
||||
{
|
||||
return RestoreOrCreateMainWindowCore(showSingleInstanceNotice: false, source);
|
||||
return TryActivateMainWindowWithStatusFromExternalIpc(source).Accepted;
|
||||
}
|
||||
|
||||
internal PublicShellActivationResult TryActivateMainWindowWithStatusFromExternalIpc(string source)
|
||||
{
|
||||
var restored = RestoreOrCreateMainWindowCore(showSingleInstanceNotice: false, source);
|
||||
var status = GetPublicShellStatus();
|
||||
return restored
|
||||
? new PublicShellActivationResult(true, "activated", "Desktop window activation was requested.", status)
|
||||
: new PublicShellActivationResult(false, "activation_failed", "Desktop window activation failed.", status);
|
||||
}
|
||||
|
||||
internal PublicTrayStatus EnsureTrayReadyFromExternalIpc(string source)
|
||||
{
|
||||
EnsureTrayReady($"ExternalIpc:{source}");
|
||||
return BuildPublicTrayStatus();
|
||||
}
|
||||
|
||||
internal PublicTaskbarStatus EnsureTaskbarEntryFromExternalIpc(string source)
|
||||
{
|
||||
EnsureTaskbarEntry($"ExternalIpc:{source}");
|
||||
return BuildPublicTaskbarStatus();
|
||||
}
|
||||
|
||||
internal PublicShellStatus GetPublicShellStatus()
|
||||
{
|
||||
return new PublicShellStatus(
|
||||
Environment.ProcessId,
|
||||
_startupAt,
|
||||
_launchSource,
|
||||
_desktopShellState.ToString(),
|
||||
_mainWindow is not null && !_mainWindowClosed,
|
||||
_mainWindow?.IsVisible == true,
|
||||
_mainWindowOpened,
|
||||
_mainWindow?.IsVisible == true && _mainWindow.WindowState != WindowState.Minimized,
|
||||
_publicIpcHostService is not null,
|
||||
BuildPublicTrayStatus(),
|
||||
BuildPublicTaskbarStatus());
|
||||
}
|
||||
|
||||
private PublicTrayStatus BuildPublicTrayStatus()
|
||||
{
|
||||
return new PublicTrayStatus(
|
||||
_desktopTrayService?.State.ToString() ?? TrayAvailabilityState.Unavailable.ToString(),
|
||||
_desktopTrayService?.IsReady == true,
|
||||
_desktopTrayService?.HasIcon == true,
|
||||
_desktopTrayService?.HasMenu == true,
|
||||
_desktopTrayService?.IsVisible == true,
|
||||
_desktopTrayService?.ConsecutiveRecoveryFailures ?? 0);
|
||||
}
|
||||
|
||||
private PublicTaskbarStatus BuildPublicTaskbarStatus()
|
||||
{
|
||||
var requested = ShouldShowMainWindowInTaskbar();
|
||||
var mainWindowExists = _mainWindow is not null && !_mainWindowClosed;
|
||||
var showInTaskbar = _mainWindow?.ShowInTaskbar == true;
|
||||
var visible = _mainWindow?.IsVisible == true;
|
||||
var minimized = _mainWindow?.WindowState == WindowState.Minimized;
|
||||
|
||||
return new PublicTaskbarStatus(
|
||||
requested,
|
||||
mainWindowExists,
|
||||
showInTaskbar,
|
||||
visible,
|
||||
minimized,
|
||||
requested && mainWindowExists && showInTaskbar && visible);
|
||||
}
|
||||
|
||||
private void InitializePublicIpc()
|
||||
|
||||
@@ -34,6 +34,7 @@ internal sealed class DesktopTrayService : IDisposable
|
||||
private NativeMenuItem? _restartMenuItem;
|
||||
private NativeMenuItem? _exitMenuItem;
|
||||
private int _consecutiveRecoveryFailures;
|
||||
private bool _disposed;
|
||||
|
||||
public DesktopTrayService(
|
||||
Application application,
|
||||
@@ -63,6 +64,14 @@ internal sealed class DesktopTrayService : IDisposable
|
||||
|
||||
public bool IsReady => State == TrayAvailabilityState.Ready;
|
||||
|
||||
public bool HasIcon => _trayIcon?.Icon is not null;
|
||||
|
||||
public bool HasMenu => _trayIcon?.Menu is not null;
|
||||
|
||||
public bool IsVisible => _trayIcon?.IsVisible == true;
|
||||
|
||||
public int ConsecutiveRecoveryFailures => _consecutiveRecoveryFailures;
|
||||
|
||||
public event Action<TrayAvailabilityState>? StateChanged;
|
||||
|
||||
public bool EnsureReady(string reason)
|
||||
@@ -105,6 +114,7 @@ internal sealed class DesktopTrayService : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
StopWatchdog();
|
||||
|
||||
try
|
||||
@@ -126,7 +136,7 @@ internal sealed class DesktopTrayService : IDisposable
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
if (State == TrayAvailabilityState.Unavailable || State == TrayAvailabilityState.Failed)
|
||||
if (_disposed || State == TrayAvailabilityState.Unavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -256,6 +266,11 @@ internal sealed class DesktopTrayService : IDisposable
|
||||
{
|
||||
if (State == state)
|
||||
{
|
||||
if (state == TrayAvailabilityState.Failed)
|
||||
{
|
||||
StateChanged?.Invoke(state);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,15 @@ namespace LanMountainDesktop.Services.ExternalIpc;
|
||||
|
||||
internal sealed class PublicShellControlService : IPublicShellControlService
|
||||
{
|
||||
public Task<PublicShellStatus> GetShellStatusAsync()
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
return (Application.Current as App)?.GetPublicShellStatus()
|
||||
?? CreateUnavailableStatus();
|
||||
}).GetTask();
|
||||
}
|
||||
|
||||
public Task<bool> ActivateMainWindowAsync()
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
@@ -15,6 +24,37 @@ internal sealed class PublicShellControlService : IPublicShellControlService
|
||||
}).GetTask();
|
||||
}
|
||||
|
||||
public Task<PublicShellActivationResult> ActivateMainWindowWithStatusAsync()
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
return (Application.Current as App)?.TryActivateMainWindowWithStatusFromExternalIpc("PublicIpc")
|
||||
?? new PublicShellActivationResult(
|
||||
false,
|
||||
"app_unavailable",
|
||||
"Application instance is not available.",
|
||||
CreateUnavailableStatus());
|
||||
}).GetTask();
|
||||
}
|
||||
|
||||
public Task<PublicTrayStatus> EnsureTrayReadyAsync()
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
return (Application.Current as App)?.EnsureTrayReadyFromExternalIpc("PublicIpc")
|
||||
?? new PublicTrayStatus("Unavailable", false, false, false, false, 0);
|
||||
}).GetTask();
|
||||
}
|
||||
|
||||
public Task<PublicTaskbarStatus> EnsureTaskbarEntryAsync()
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
return (Application.Current as App)?.EnsureTaskbarEntryFromExternalIpc("PublicIpc")
|
||||
?? new PublicTaskbarStatus(false, false, false, false, false, false);
|
||||
}).GetTask();
|
||||
}
|
||||
|
||||
public Task<bool> OpenSettingsAsync(string? pageTag = null)
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
@@ -44,4 +84,20 @@ internal sealed class PublicShellControlService : IPublicShellControlService
|
||||
Source: "PublicIpc",
|
||||
Reason: "External IPC requested exit.")) == true);
|
||||
}
|
||||
|
||||
private static PublicShellStatus CreateUnavailableStatus()
|
||||
{
|
||||
return new PublicShellStatus(
|
||||
Environment.ProcessId,
|
||||
DateTimeOffset.UtcNow,
|
||||
"unknown",
|
||||
"Unavailable",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
new PublicTrayStatus("Unavailable", false, false, false, false, 0),
|
||||
new PublicTaskbarStatus(false, false, false, false, false, false));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user