mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-23 01:44:26 +08:00
changed.更了好多
This commit is contained in:
@@ -16,6 +16,7 @@ public static class HostAppSettingsOobeMerger
|
||||
public const string EnableFusedDesktopKey = "EnableFusedDesktop";
|
||||
public const string EnableThreeFingerSwipeKey = "EnableThreeFingerSwipe";
|
||||
public const string AutoStartWithWindowsKey = "AutoStartWithWindows";
|
||||
public const string MultiInstanceLaunchBehaviorKey = "MultiInstanceLaunchBehavior";
|
||||
|
||||
public static string GetSettingsFilePath(string dataRoot) =>
|
||||
Path.Combine(Path.GetFullPath(dataRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), "settings.json");
|
||||
@@ -54,6 +55,30 @@ public static class HostAppSettingsOobeMerger
|
||||
}
|
||||
}
|
||||
|
||||
public static MultiInstanceLaunchBehavior LoadMultiInstanceLaunchBehavior(string settingsPath)
|
||||
{
|
||||
if (!File.Exists(settingsPath))
|
||||
{
|
||||
return MultiInstanceLaunchBehavior.NotifyAndOpenDesktop;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var root = JsonNode.Parse(File.ReadAllText(settingsPath))?.AsObject();
|
||||
if (root is null)
|
||||
{
|
||||
return MultiInstanceLaunchBehavior.NotifyAndOpenDesktop;
|
||||
}
|
||||
|
||||
return ReadMultiInstanceLaunchBehavior(root);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"HostAppSettingsOobeMerger: failed to read multi-instance behavior from '{settingsPath}'. {ex.Message}");
|
||||
return MultiInstanceLaunchBehavior.NotifyAndOpenDesktop;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MergeStartupPresentation(string settingsPath, HostAppSettingsStartupChoices choices)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(settingsPath);
|
||||
@@ -109,6 +134,31 @@ public static class HostAppSettingsOobeMerger
|
||||
_ => defaultValue
|
||||
};
|
||||
}
|
||||
|
||||
private static MultiInstanceLaunchBehavior ReadMultiInstanceLaunchBehavior(JsonObject root)
|
||||
{
|
||||
if (!root.TryGetPropertyValue(MultiInstanceLaunchBehaviorKey, out var node) || node is null)
|
||||
{
|
||||
return MultiInstanceLaunchBehavior.NotifyAndOpenDesktop;
|
||||
}
|
||||
|
||||
if (node is JsonValue value)
|
||||
{
|
||||
if (value.TryGetValue<string>(out var text) &&
|
||||
Enum.TryParse<MultiInstanceLaunchBehavior>(text, ignoreCase: true, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
if (value.TryGetValue<int>(out var numeric) &&
|
||||
Enum.IsDefined(typeof(MultiInstanceLaunchBehavior), numeric))
|
||||
{
|
||||
return (MultiInstanceLaunchBehavior)numeric;
|
||||
}
|
||||
}
|
||||
|
||||
return MultiInstanceLaunchBehavior.NotifyAndOpenDesktop;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct HostAppSettingsStartupDefaults(
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
using System.Buffers;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Services.Ipc;
|
||||
|
||||
/// <summary>
|
||||
/// Launcher IPC 服务端 - 接收主程序的启动进度报告
|
||||
/// 采用持久连接 + 长度前缀协议,支持客户端在同一连接上多次发送消息。
|
||||
/// 跨平台实现:Windows 使用命名管道,Linux/macOS 使用 Unix 域套接字
|
||||
/// </summary>
|
||||
public class LauncherIpcServer : IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Action<StartupProgressMessage> _onProgress;
|
||||
private Task? _listenTask;
|
||||
private NamedPipeServerStream? _currentPipe;
|
||||
|
||||
/// <summary>
|
||||
/// 协议:每条消息以 4 字节小端 int32 长度前缀开头,后跟 UTF-8 JSON 正文。
|
||||
/// 这在 Windows Message 模式和 unix Byte 模式下均能可靠工作。
|
||||
/// </summary>
|
||||
private const int LengthPrefixSize = 4;
|
||||
|
||||
private const int BackoffBaseMs = 200;
|
||||
private const int BackoffMaxMs = 5000;
|
||||
private const int BackoffJitterMs = 100;
|
||||
|
||||
public LauncherIpcServer(Action<StartupProgressMessage> onProgress)
|
||||
{
|
||||
_onProgress = onProgress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动 IPC 服务端监听
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_listenTask = Task.Run(ListenLoopAsync, _cts.Token);
|
||||
}
|
||||
|
||||
private async Task ListenLoopAsync()
|
||||
{
|
||||
var consecutiveErrors = 0;
|
||||
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
NamedPipeServerStream? pipe = null;
|
||||
try
|
||||
{
|
||||
pipe = new NamedPipeServerStream(
|
||||
LauncherIpcConstants.PipeName,
|
||||
PipeDirection.In,
|
||||
1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
_currentPipe = pipe;
|
||||
await pipe.WaitForConnectionAsync(_cts.Token);
|
||||
|
||||
consecutiveErrors = 0;
|
||||
|
||||
await ReadMessagesFromConnectionAsync(pipe, _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
consecutiveErrors = 0;
|
||||
continue;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
consecutiveErrors++;
|
||||
var delay = ComputeBackoff(consecutiveErrors);
|
||||
Console.Error.WriteLine($"IPC listen error (attempt {consecutiveErrors}), retrying in {delay}ms: {ex.Message}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(delay, _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
pipe?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (ReferenceEquals(_currentPipe, pipe))
|
||||
{
|
||||
_currentPipe = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int ComputeBackoff(int attempt)
|
||||
{
|
||||
var exponential = BackoffBaseMs * (1 << Math.Min(attempt - 1, 5));
|
||||
var capped = Math.Min(exponential, BackoffMaxMs);
|
||||
var jitter = Random.Shared.Next(0, BackoffJitterMs);
|
||||
return capped + jitter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从已连接的管道中持续读取消息,直到连接断开或取消
|
||||
/// </summary>
|
||||
private async Task ReadMessagesFromConnectionAsync(NamedPipeServerStream pipe, CancellationToken cancellationToken)
|
||||
{
|
||||
var lengthBuffer = ArrayPool<byte>.Shared.Rent(LengthPrefixSize);
|
||||
try
|
||||
{
|
||||
while (pipe.IsConnected && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// 1. 读取 4 字节长度前缀
|
||||
var totalRead = 0;
|
||||
while (totalRead < LengthPrefixSize)
|
||||
{
|
||||
var read = await pipe.ReadAsync(lengthBuffer.AsMemory(totalRead, LengthPrefixSize - totalRead), cancellationToken);
|
||||
if (read == 0)
|
||||
{
|
||||
// 连接已关闭
|
||||
return;
|
||||
}
|
||||
totalRead += read;
|
||||
}
|
||||
|
||||
var payloadLength = BitConverter.ToInt32(lengthBuffer, 0);
|
||||
if (payloadLength <= 0 || payloadLength > 1024 * 1024) // 最大 1MB 单条消息
|
||||
{
|
||||
// 无效长度,跳过此连接
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 读取消息正文
|
||||
var payloadBuffer = ArrayPool<byte>.Shared.Rent(payloadLength);
|
||||
try
|
||||
{
|
||||
totalRead = 0;
|
||||
while (totalRead < payloadLength)
|
||||
{
|
||||
var read = await pipe.ReadAsync(payloadBuffer.AsMemory(totalRead, payloadLength - totalRead), cancellationToken);
|
||||
if (read == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
totalRead += read;
|
||||
}
|
||||
|
||||
// 3. 反序列化并回调
|
||||
var json = System.Text.Encoding.UTF8.GetString(payloadBuffer, 0, payloadLength);
|
||||
var message = JsonSerializer.Deserialize(json, AppJsonContext.Default.StartupProgressMessage);
|
||||
if (message is not null)
|
||||
{
|
||||
_onProgress(message);
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// 忽略解析错误,继续读取下一条消息
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(payloadBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(lengthBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止 IPC 服务端
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_cts.Cancel();
|
||||
try
|
||||
{
|
||||
_currentPipe?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_cts.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
_listenTask?.Wait(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
@@ -218,56 +218,53 @@ internal sealed class LauncherFlowCoordinator
|
||||
{
|
||||
if (ShouldProbeExistingHostBeforeLaunch(_context))
|
||||
{
|
||||
var existingActivation = await TryActivateExistingHostWithStatusAsync(ipcClient, TimeSpan.FromMilliseconds(900))
|
||||
var multiInstanceBehavior = LoadMultiInstanceLaunchBehavior();
|
||||
var existingShellStatus = await TryGetExistingHostStatusAsync(ipcClient, TimeSpan.FromMilliseconds(900))
|
||||
.ConfigureAwait(false);
|
||||
if (existingActivation is not null)
|
||||
if (IsExistingHostReadyForLauncherDecision(existingShellStatus))
|
||||
{
|
||||
ipcConnected = true;
|
||||
shellStatus = existingActivation.Status;
|
||||
var recoverableActivationFailure = IsRecoverableActivationFailure(existingActivation);
|
||||
lastStage = existingActivation.Accepted
|
||||
shellStatus = existingShellStatus;
|
||||
var decisionResult = await ApplyExistingHostBehaviorAsync(
|
||||
ipcClient,
|
||||
multiInstanceBehavior,
|
||||
existingShellStatus!)
|
||||
.ConfigureAwait(false);
|
||||
shellStatus = decisionResult.ActivationResult?.Status ?? existingShellStatus;
|
||||
var recoverableActivationFailure = decisionResult.ActivationResult is not null &&
|
||||
IsRecoverableActivationFailure(decisionResult.ActivationResult);
|
||||
lastStage = decisionResult.Success || recoverableActivationFailure
|
||||
? StartupStage.ActivationRedirected
|
||||
: StartupStage.ActivationFailed;
|
||||
lastStageMessage = existingActivation.Message;
|
||||
if (existingActivation.Accepted)
|
||||
lastStageMessage = decisionResult.Message;
|
||||
if (decisionResult.Success || recoverableActivationFailure)
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedSucceeded(lastStage, lastStageMessage);
|
||||
}
|
||||
else if (recoverableActivationFailure)
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedWaitingForShell(lastStageMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedFailed(lastStage, lastStageMessage);
|
||||
}
|
||||
|
||||
PublishCoordinatorStatus(
|
||||
hostProcessAliveOverride: true,
|
||||
completed: true,
|
||||
succeeded: existingActivation.Accepted || recoverableActivationFailure);
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: true, completed: true, succeeded: decisionResult.Success);
|
||||
windowsClosingByCoordinator = true;
|
||||
await CloseWindowsAsync(splashWindow, loadingDetailsWindow).ConfigureAwait(false);
|
||||
return BuildResult(
|
||||
success: existingActivation.Accepted || recoverableActivationFailure,
|
||||
success: decisionResult.Success,
|
||||
stage: "launch",
|
||||
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,
|
||||
code: decisionResult.Code,
|
||||
message: decisionResult.Message,
|
||||
details: MergeDetails(
|
||||
launcherContextDetails,
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["publicIpcConnected"] = "true",
|
||||
["existingHostPid"] = existingActivation.Status.ProcessId.ToString(),
|
||||
["existingShellState"] = existingActivation.Status.ShellState,
|
||||
["existingTrayState"] = existingActivation.Status.Tray.State,
|
||||
["existingTaskbarUsable"] = existingActivation.Status.Taskbar.IsUsable.ToString()
|
||||
["multiInstanceBehavior"] = multiInstanceBehavior.ToString(),
|
||||
["existingHostPid"] = shellStatus?.ProcessId.ToString() ?? string.Empty,
|
||||
["existingShellState"] = shellStatus?.ShellState ?? string.Empty,
|
||||
["existingTrayState"] = shellStatus?.Tray.State ?? string.Empty,
|
||||
["existingTaskbarUsable"] = shellStatus?.Taskbar.IsUsable.ToString() ?? string.Empty,
|
||||
["activationAccepted"] = decisionResult.ActivationResult?.Accepted.ToString() ?? string.Empty
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -492,7 +489,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
var connected = await TryConnectToPublicIpcAsync(ipcClient, TimeSpan.FromMilliseconds(1200)).ConfigureAwait(false);
|
||||
if (!connected)
|
||||
{
|
||||
Logger.Warn("Timed out waiting for host public IPC. Launcher will continue without live startup notifications.");
|
||||
Logger.Info("Host public IPC is not ready yet. Launcher will keep monitoring the host process and retry.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -557,30 +554,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
recoveryActivationAttempted: true));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
Logger.Info("Activation failure did not recover through public IPC yet. Launcher will keep monitoring the current host attempt.");
|
||||
}
|
||||
|
||||
if (processExitTask.IsCompleted)
|
||||
@@ -589,7 +563,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
Logger.Warn($"Host exited before startup success criteria were met. ExitCode={exitCode}.");
|
||||
|
||||
windowsClosingByCoordinator = true;
|
||||
if (exitCode == HostExitCodes.SecondaryActivationSucceeded)
|
||||
if (IsSuccessfulActivationExitCode(exitCode))
|
||||
{
|
||||
_startupAttemptRegistry.MarkOwnedSucceeded(StartupStage.ActivationRedirected, "Host redirected activation to the existing desktop instance.");
|
||||
PublishCoordinatorStatus(hostProcessAliveOverride: false, completed: true, succeeded: true);
|
||||
@@ -608,7 +582,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
}
|
||||
|
||||
if (!activationRetryAttempted &&
|
||||
exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired)
|
||||
IsFailedActivationExitCode(exitCode))
|
||||
{
|
||||
activationRetryAttempted = true;
|
||||
var activationRecovery = await TryRecoverActivationThroughExistingHostAsync(
|
||||
@@ -633,30 +607,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
}));
|
||||
}
|
||||
|
||||
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()
|
||||
}));
|
||||
}
|
||||
Logger.Info("Activation exit code did not recover through public IPC. Launcher will report the activation failure without launching another host.");
|
||||
}
|
||||
|
||||
_startupAttemptRegistry.MarkOwnedFailed(lastStage, activationFailureReason);
|
||||
@@ -665,10 +616,10 @@ internal sealed class LauncherFlowCoordinator
|
||||
return BuildResult(
|
||||
success: false,
|
||||
stage: "launch",
|
||||
code: exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired
|
||||
code: IsFailedActivationExitCode(exitCode)
|
||||
? "activation_failed"
|
||||
: "host_exited_early",
|
||||
message: exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired
|
||||
message: IsFailedActivationExitCode(exitCode)
|
||||
? $"Host activation handshake failed before the required startup state was reported. ExitCode={exitCode}."
|
||||
: $"Host exited before the required startup state was reported. ExitCode={exitCode}.",
|
||||
details: MergeDetails(
|
||||
@@ -909,54 +860,6 @@ internal sealed class LauncherFlowCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LauncherResult?> RetryActivationAfterEarlyFailureAsync()
|
||||
{
|
||||
Logger.Warn("Attempting one explicit activation retry after host early failure.");
|
||||
var retryOutcome = await LaunchHostWithIpcAsync(forceDirectMode: true, retryTag: "explicit-activation-retry").ConfigureAwait(false);
|
||||
if (!retryOutcome.Result.Success)
|
||||
{
|
||||
return retryOutcome.Result;
|
||||
}
|
||||
|
||||
if (retryOutcome.ImmediateResult is not null)
|
||||
{
|
||||
return retryOutcome.ImmediateResult;
|
||||
}
|
||||
|
||||
if (retryOutcome.Process is not null)
|
||||
{
|
||||
var retryExitTask = retryOutcome.Process.WaitForExitAsync();
|
||||
var completed = await Task.WhenAny(retryExitTask, Task.Delay(TimeSpan.FromSeconds(15))).ConfigureAwait(false);
|
||||
|
||||
if (completed != retryExitTask)
|
||||
{
|
||||
return BuildResult(
|
||||
success: true,
|
||||
stage: "launch",
|
||||
code: "activation_retry_started",
|
||||
message: "Activation retry started the host successfully.",
|
||||
details: retryOutcome.Details);
|
||||
}
|
||||
|
||||
if (retryOutcome.Process.ExitCode == HostExitCodes.SecondaryActivationSucceeded)
|
||||
{
|
||||
return BuildResult(
|
||||
success: true,
|
||||
stage: "launch",
|
||||
code: "activation_redirected",
|
||||
message: "Activation retry redirected to the existing desktop instance.",
|
||||
details: retryOutcome.Details);
|
||||
}
|
||||
}
|
||||
|
||||
return BuildResult(
|
||||
success: false,
|
||||
stage: "launch",
|
||||
code: "activation_failed",
|
||||
message: "Activation retry failed to make the desktop visible.",
|
||||
details: retryOutcome.Details);
|
||||
}
|
||||
|
||||
private static async Task CloseWindowsAsync(SplashWindow splashWindow, LoadingDetailsWindow? loadingDetailsWindow)
|
||||
{
|
||||
try
|
||||
@@ -1087,7 +990,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
previousAttempt is null ? null : finalAttempt,
|
||||
!finalAttempt.ProcessCreated
|
||||
? "start"
|
||||
: finalAttempt.ExitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired
|
||||
: finalAttempt.ExitCode is int finalExitCode && IsFailedActivationExitCode(finalExitCode)
|
||||
? "activation"
|
||||
: "early-exit");
|
||||
|
||||
@@ -1101,7 +1004,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
details));
|
||||
}
|
||||
|
||||
if (finalAttempt.ExitCode == HostExitCodes.SecondaryActivationSucceeded)
|
||||
if (finalAttempt.ExitCode is not null && IsSuccessfulActivationExitCode(finalAttempt.ExitCode.Value))
|
||||
{
|
||||
return HostLaunchOutcome.FromImmediateResult(BuildResult(
|
||||
true,
|
||||
@@ -1111,7 +1014,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
details));
|
||||
}
|
||||
|
||||
if (finalAttempt.ExitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired)
|
||||
if (finalAttempt.ExitCode is not null && IsFailedActivationExitCode(finalAttempt.ExitCode.Value))
|
||||
{
|
||||
return HostLaunchOutcome.FromResult(BuildResult(
|
||||
false,
|
||||
@@ -1469,12 +1372,12 @@ internal sealed class LauncherFlowCoordinator
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Public IPC connect failed: {ex.Message}");
|
||||
Logger.Info($"Public IPC is not ready yet: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldProbeExistingHostBeforeLaunch(CommandContext context)
|
||||
internal static bool ShouldProbeExistingHostBeforeLaunch(CommandContext context)
|
||||
{
|
||||
if (!string.Equals(context.Command, "launch", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1489,6 +1392,169 @@ internal sealed class LauncherFlowCoordinator
|
||||
return !string.Equals(context.LaunchSource, "restart", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private MultiInstanceLaunchBehavior LoadMultiInstanceLaunchBehavior()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsPath = HostAppSettingsOobeMerger.GetSettingsFilePath(_dataLocationResolver.ResolveDataRoot());
|
||||
return HostAppSettingsOobeMerger.LoadMultiInstanceLaunchBehavior(settingsPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to load multi-instance launch behavior. Falling back to default. {ex.Message}");
|
||||
return MultiInstanceLaunchBehavior.NotifyAndOpenDesktop;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsExistingHostReadyForLauncherDecision(PublicShellStatus? status)
|
||||
{
|
||||
return status is { PublicIpcReady: true, ProcessId: > 0 };
|
||||
}
|
||||
|
||||
private static async Task<PublicShellStatus?> TryGetExistingHostStatusAsync(
|
||||
LanMountainDesktopIpcClient ipcClient,
|
||||
TimeSpan timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connected = ipcClient.IsConnected ||
|
||||
await TryConnectToPublicIpcAsync(ipcClient, timeout).ConfigureAwait(false);
|
||||
if (!connected)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var shellProxy = ipcClient.CreateProxy<IPublicShellControlService>();
|
||||
return await shellProxy.GetShellStatusAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Info($"Existing host status probe did not complete: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<ExistingHostBehaviorResult> ApplyExistingHostBehaviorAsync(
|
||||
LanMountainDesktopIpcClient ipcClient,
|
||||
MultiInstanceLaunchBehavior behavior,
|
||||
PublicShellStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
var shellProxy = ipcClient.CreateProxy<IPublicShellControlService>();
|
||||
return behavior switch
|
||||
{
|
||||
MultiInstanceLaunchBehavior.OpenDesktopSilently => await ActivateExistingHostForBehaviorAsync(
|
||||
shellProxy,
|
||||
showLauncherNotice: false,
|
||||
successCode: "existing_host_activated",
|
||||
successMessage: "Launcher activated the existing desktop instance.",
|
||||
failureCode: "existing_host_activation_failed").ConfigureAwait(false),
|
||||
|
||||
MultiInstanceLaunchBehavior.NotifyAndOpenDesktop => await ActivateExistingHostForBehaviorAsync(
|
||||
shellProxy,
|
||||
showLauncherNotice: true,
|
||||
successCode: "existing_host_activated_with_notice",
|
||||
successMessage: "Launcher activated the existing desktop instance and showed the repeated-launch notice.",
|
||||
failureCode: "existing_host_activation_failed").ConfigureAwait(false),
|
||||
|
||||
MultiInstanceLaunchBehavior.PromptOnly => await ShowPromptOnlyExistingHostAsync(
|
||||
shellProxy,
|
||||
status).ConfigureAwait(false),
|
||||
|
||||
MultiInstanceLaunchBehavior.RestartApp => await RestartExistingHostAsync(shellProxy).ConfigureAwait(false),
|
||||
|
||||
_ => await ActivateExistingHostForBehaviorAsync(
|
||||
shellProxy,
|
||||
showLauncherNotice: true,
|
||||
successCode: "existing_host_activated_with_notice",
|
||||
successMessage: "Launcher activated the existing desktop instance and showed the repeated-launch notice.",
|
||||
failureCode: "existing_host_activation_failed").ConfigureAwait(false)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Failed to apply multi-instance behavior '{behavior}': {ex.Message}");
|
||||
return new ExistingHostBehaviorResult(
|
||||
false,
|
||||
"multi_instance_behavior_failed",
|
||||
$"Failed to apply multi-instance behavior '{behavior}': {ex.Message}",
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<ExistingHostBehaviorResult> ActivateExistingHostForBehaviorAsync(
|
||||
IPublicShellControlService shellProxy,
|
||||
bool showLauncherNotice,
|
||||
string successCode,
|
||||
string successMessage,
|
||||
string failureCode)
|
||||
{
|
||||
var activation = await shellProxy.ActivateMainWindowWithStatusAsync().ConfigureAwait(false);
|
||||
var success = activation.Accepted || IsRecoverableActivationFailure(activation);
|
||||
if (showLauncherNotice && success)
|
||||
{
|
||||
var promptResult = await ShowMultiInstancePromptAsync(activation.Status).ConfigureAwait(false);
|
||||
if (promptResult == MultiInstancePromptResult.OpenDesktop)
|
||||
{
|
||||
activation = await shellProxy.ActivateMainWindowWithStatusAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new ExistingHostBehaviorResult(
|
||||
success,
|
||||
activation.Accepted ? successCode : success ? "existing_host_startup_pending" : failureCode,
|
||||
activation.Accepted ? successMessage : activation.Message,
|
||||
activation);
|
||||
}
|
||||
|
||||
private static async Task<ExistingHostBehaviorResult> RestartExistingHostAsync(
|
||||
IPublicShellControlService shellProxy)
|
||||
{
|
||||
var accepted = await shellProxy.RestartAsync().ConfigureAwait(false);
|
||||
return new ExistingHostBehaviorResult(
|
||||
accepted,
|
||||
accepted ? "existing_host_restart_requested" : "existing_host_restart_failed",
|
||||
accepted
|
||||
? "Launcher requested the existing desktop instance to restart."
|
||||
: "Launcher could not request restart from the existing desktop instance.",
|
||||
null);
|
||||
}
|
||||
|
||||
private static async Task<ExistingHostBehaviorResult> ShowPromptOnlyExistingHostAsync(
|
||||
IPublicShellControlService shellProxy,
|
||||
PublicShellStatus status)
|
||||
{
|
||||
var promptResult = await ShowMultiInstancePromptAsync(status).ConfigureAwait(false);
|
||||
|
||||
if (promptResult == MultiInstancePromptResult.OpenDesktop)
|
||||
{
|
||||
return await ActivateExistingHostForBehaviorAsync(
|
||||
shellProxy,
|
||||
showLauncherNotice: false,
|
||||
successCode: "existing_host_activated_from_prompt",
|
||||
successMessage: "Launcher activated the existing desktop instance from the prompt.",
|
||||
failureCode: "existing_host_activation_failed").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new ExistingHostBehaviorResult(
|
||||
true,
|
||||
"existing_host_prompt_only",
|
||||
"Launcher showed the repeated-launch prompt and did not open the desktop automatically.",
|
||||
null);
|
||||
}
|
||||
|
||||
private static async Task<MultiInstancePromptResult> ShowMultiInstancePromptAsync(PublicShellStatus status)
|
||||
{
|
||||
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var prompt = new MultiInstancePromptWindow();
|
||||
prompt.SetDetails(status.ProcessId, status.ShellState);
|
||||
prompt.Show();
|
||||
return await prompt.WaitForChoiceAsync().ConfigureAwait(true);
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<PublicShellActivationResult?> TryActivateExistingHostWithStatusAsync(
|
||||
LanMountainDesktopIpcClient ipcClient,
|
||||
TimeSpan timeout)
|
||||
@@ -1507,7 +1573,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Existing host activation probe failed: {ex.Message}");
|
||||
Logger.Info($"Existing host activation probe did not complete: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1541,7 +1607,7 @@ internal sealed class LauncherFlowCoordinator
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool IsRecoverableActivationFailure(PublicShellActivationResult activation)
|
||||
internal static bool IsRecoverableActivationFailure(PublicShellActivationResult activation)
|
||||
{
|
||||
if (activation.Accepted)
|
||||
{
|
||||
@@ -1560,6 +1626,12 @@ internal sealed class LauncherFlowCoordinator
|
||||
string.Equals(activation.Code, "startup_pending", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
internal static bool IsSuccessfulActivationExitCode(int exitCode) =>
|
||||
exitCode == HostExitCodes.SecondaryActivationSucceeded;
|
||||
|
||||
internal static bool IsFailedActivationExitCode(int exitCode) =>
|
||||
exitCode is HostExitCodes.SecondaryActivationFailed or HostExitCodes.RestartLockNotAcquired;
|
||||
|
||||
private static async Task<PublicShellStatus?> TryGetPublicShellStatusAsync(
|
||||
LanMountainDesktopIpcClient ipcClient)
|
||||
{
|
||||
@@ -1759,6 +1831,12 @@ internal sealed class LauncherFlowCoordinator
|
||||
plan is null ? null : HostLaunchPlanBuilder.FormatArgumentsForLog(plan.Arguments));
|
||||
}
|
||||
|
||||
private sealed record ExistingHostBehaviorResult(
|
||||
bool Success,
|
||||
string Code,
|
||||
string Message,
|
||||
PublicShellActivationResult? ActivationResult);
|
||||
|
||||
private sealed record HostLaunchOutcome(
|
||||
LauncherResult Result,
|
||||
Process? Process,
|
||||
|
||||
Reference in New Issue
Block a user