mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-23 09:54:25 +08:00
Add launcher debug settings, recovery & version fixes
Introduce a persistent LauncherDebugSettingsStore and wire it into ErrorWindow and SplashWindow so dev-mode and custom host path can be saved/loaded. Harden DeploymentLocator/FlexibleHostLocator to safely normalize and validate saved debug paths and log warnings for malformed values. Add a WaitingForShell startup state and recoverable-activation logic across App and LauncherFlowCoordinator (with registry updates) so Launcher can attach to an in-progress desktop shell rather than failing. Clean up ErrorDebugWindow UI/flow (WasAccepted flag, localization fixes, event wiring) and improve splash version population. Improve AppVersionProvider to trim surrounding quotes, robustly parse version.json via JsonDocument and read string properties; add unit tests for AppVersionProvider, DeploymentLocator and LauncherDebugSettingsStore. Also quote Exec commands in the csproj and harden scripts/Generate-VersionFile.ps1 (argument normalization, LiteralPath, error handling).
This commit is contained in:
@@ -228,6 +228,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
{
|
||||
ipcConnected = true;
|
||||
shellStatus = existingActivation.Status;
|
||||
var recoverableActivationFailure = IsRecoverableActivationFailure(existingActivation);
|
||||
lastStage = existingActivation.Accepted
|
||||
? StartupStage.ActivationRedirected
|
||||
: StartupStage.ActivationFailed;
|
||||
@@ -236,6 +237,10 @@ internal sealed class LauncherFlowCoordinator
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedSucceeded(lastStage, lastStageMessage);
|
||||
}
|
||||
else if (recoverableActivationFailure)
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedWaitingForShell(lastStageMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedFailed(lastStage, lastStageMessage);
|
||||
@@ -244,14 +249,20 @@ internal sealed class LauncherFlowCoordinator
|
||||
PublishCoordinatorStatus(
|
||||
hostProcessAliveOverride: true,
|
||||
completed: true,
|
||||
succeeded: existingActivation.Accepted);
|
||||
succeeded: existingActivation.Accepted || recoverableActivationFailure);
|
||||
windowsClosingByCoordinator = true;
|
||||
await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false);
|
||||
return BuildResult(
|
||||
success: existingActivation.Accepted,
|
||||
success: existingActivation.Accepted || recoverableActivationFailure,
|
||||
stage: "launch",
|
||||
code: existingActivation.Accepted ? "existing_host_activated" : "existing_host_activation_failed",
|
||||
message: existingActivation.Message,
|
||||
code: existingActivation.Accepted
|
||||
? "existing_host_activated"
|
||||
: recoverableActivationFailure
|
||||
? "existing_host_startup_pending"
|
||||
: "existing_host_activation_failed",
|
||||
message: recoverableActivationFailure
|
||||
? "Existing desktop process is still starting; Launcher will not start another process."
|
||||
: existingActivation.Message,
|
||||
details: MergeDetails(
|
||||
launcherContextDetails,
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -438,6 +449,11 @@ internal sealed class LauncherFlowCoordinator
|
||||
ipcConnected = true;
|
||||
_startupAttemptRegistry.MarkOwnedIpcConnected();
|
||||
shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false);
|
||||
if (shellStatus is { DesktopVisible: false })
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedWaitingForShell("Host public IPC is ready; waiting for desktop shell.");
|
||||
}
|
||||
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: true);
|
||||
}
|
||||
|
||||
@@ -464,6 +480,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
var softTimeoutAt = startedAt + StartupSoftTimeout;
|
||||
var hardTimeoutAt = startedAt + StartupHardTimeout;
|
||||
var nextReconnectAttemptAt = DateTimeOffset.UtcNow.AddSeconds(5);
|
||||
var activationRetryAttempted = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -482,10 +499,35 @@ internal sealed class LauncherFlowCoordinator
|
||||
details: ComposeLaunchDetails(!launchOutcome.Process.HasExited));
|
||||
}
|
||||
|
||||
if (activationFailedTcs.Task.IsCompleted && string.IsNullOrWhiteSpace(activationFailureReason))
|
||||
if (activationFailedTcs.Task.IsCompleted && !activationRetryAttempted)
|
||||
{
|
||||
activationRetryAttempted = true;
|
||||
activationFailureReason = await activationFailedTcs.Task.ConfigureAwait(false);
|
||||
Logger.Warn($"Activation failure received before startup success. Reason='{activationFailureReason}'.");
|
||||
var retryOutcome = await RetryActivationAfterEarlyFailureAsync().ConfigureAwait(false);
|
||||
if (retryOutcome is not null)
|
||||
{
|
||||
windowsClosingByCoordinator = true;
|
||||
if (retryOutcome.Success)
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedSucceeded(lastStage, retryOutcome.Message);
|
||||
PublishCoordinatorStatus(
|
||||
hostProcessAliveOverride: !launchOutcome.Process.HasExited,
|
||||
completed: true,
|
||||
succeeded: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason);
|
||||
PublishCoordinatorStatus(
|
||||
hostProcessAliveOverride: !launchOutcome.Process.HasExited,
|
||||
completed: true,
|
||||
succeeded: false);
|
||||
}
|
||||
|
||||
await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false);
|
||||
return WithAdditionalDetails(retryOutcome, ComposeLaunchDetails(!launchOutcome.Process.HasExited, recoveryActivationAttempted: true));
|
||||
}
|
||||
}
|
||||
|
||||
if (processExitTask.IsCompleted)
|
||||
@@ -512,6 +554,36 @@ internal sealed class LauncherFlowCoordinator
|
||||
}));
|
||||
}
|
||||
|
||||
if (!activationRetryAttempted &&
|
||||
exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired)
|
||||
{
|
||||
activationRetryAttempted = true;
|
||||
var retryOutcome = await RetryActivationAfterEarlyFailureAsync().ConfigureAwait(false);
|
||||
if (retryOutcome is not null)
|
||||
{
|
||||
if (retryOutcome.Success)
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedSucceeded(lastStage, retryOutcome.Message);
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason);
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: false);
|
||||
}
|
||||
|
||||
await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false);
|
||||
return WithAdditionalDetails(
|
||||
retryOutcome,
|
||||
MergeDetails(
|
||||
ComposeLaunchDetails(hostProcessAlive: false, recoveryActivationAttempted: true),
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["exitCode"] = exitCode.ToString()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
_startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason);
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: false);
|
||||
await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false);
|
||||
@@ -543,6 +615,11 @@ internal sealed class LauncherFlowCoordinator
|
||||
ipcConnected = true;
|
||||
_startupAttemptRegistry.MarkOwnedIpcConnected();
|
||||
shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false);
|
||||
if (shellStatus is { DesktopVisible: false })
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedWaitingForShell("Host public IPC reconnected; waiting for desktop shell.");
|
||||
}
|
||||
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: true);
|
||||
}
|
||||
|
||||
@@ -602,6 +679,11 @@ internal sealed class LauncherFlowCoordinator
|
||||
ipcConnected = true;
|
||||
_startupAttemptRegistry.MarkOwnedIpcConnected();
|
||||
shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false);
|
||||
if (shellStatus is { DesktopVisible: false })
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedWaitingForShell("Host public IPC is ready; waiting for desktop shell.");
|
||||
}
|
||||
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: true);
|
||||
}
|
||||
}
|
||||
@@ -632,6 +714,23 @@ internal sealed class LauncherFlowCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
if (connected && !launchOutcome.Process.HasExited)
|
||||
{
|
||||
windowsClosingByCoordinator = true;
|
||||
_startupAttemptRegistry.MarkOwnedWaitingForShell("Host process is still running after the launcher wait window.");
|
||||
shellStatus = await TryGetPublicShellStatusAsync(ipcClient).ConfigureAwait(false);
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: false, succeeded: false);
|
||||
await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false);
|
||||
return BuildResult(
|
||||
success: true,
|
||||
stage: "launch",
|
||||
code: "startup_pending",
|
||||
message: "Host process is still running; Launcher will not start another process while the desktop shell finishes startup.",
|
||||
details: ComposeLaunchDetails(
|
||||
hostProcessAlive: true,
|
||||
recoveryActivationAttempted));
|
||||
}
|
||||
|
||||
windowsClosingByCoordinator = true;
|
||||
_startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason);
|
||||
PublishCoordinatorStatus(!launchOutcome.Process.HasExited, completed: true, succeeded: false);
|
||||
@@ -1369,6 +1468,25 @@ internal sealed class LauncherFlowCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRecoverableActivationFailure(PublicShellActivationResult activation)
|
||||
{
|
||||
if (activation.Accepted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(activation.Code, "shutdown_in_progress", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return activation.Status.PublicIpcReady &&
|
||||
(!activation.Status.MainWindowOpened ||
|
||||
!activation.Status.DesktopVisible ||
|
||||
string.Equals(activation.Code, "shell_not_ready", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(activation.Code, "startup_pending", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static async Task<PublicShellStatus?> TryGetPublicShellStatusAsync(
|
||||
LanMountainDesktopIpcClient ipcClient)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user