From 9cf3a15c89ca78be579f9769228eab0bd1a028a0 Mon Sep 17 00:00:00 2001 From: lincube Date: Sat, 18 Apr 2026 23:36:31 +0800 Subject: [PATCH] =?UTF-8?q?fix.=E6=88=91=E4=BB=AC=E8=AF=95=E9=AA=8C?= =?UTF-8?q?=E6=80=A7=E5=9C=B0=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=99=A8=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E5=8E=9F=E5=9B=A0=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E6=98=AF=E8=BF=99=E4=B8=AA=E7=94=BB=E9=9D=A2=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E5=90=AF=E5=8A=A8=EF=BC=8C=E5=B0=B1GUI=E6=B2=A1?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E3=80=82=E7=84=B6=E5=90=8E=E8=BF=98=E6=8A=8A?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E9=97=AE=E9=A2=98=E4=BF=AE=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=B8=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/LauncherFlowCoordinator.cs | 100 ++++++++++++++---- LanMountainDesktop/App.axaml.cs | 68 ++++++++---- .../Services/AppRestartService.cs | 70 ++++++++++-- .../Services/Launcher/LauncherIpcClient.cs | 26 ++++- .../Services/Loading/LoadingTimeoutHandler.cs | 1 + LanMountainDesktop/app.manifest | 15 ++- 6 files changed, 224 insertions(+), 56 deletions(-) diff --git a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs index 784b03e..4a3a97c 100644 --- a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs +++ b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs @@ -9,6 +9,15 @@ namespace LanMountainDesktop.Launcher.Services; internal sealed class LauncherFlowCoordinator { + private static readonly string[] LauncherOnlyOptions = + [ + "debug", "show-loading-details", "plugins-dir", "source", "result", + LauncherIpcConstants.LauncherPidEnvVar, + LauncherIpcConstants.PackageRootEnvVar, + LauncherIpcConstants.VersionEnvVar, + LauncherIpcConstants.CodenameEnvVar + ]; + private readonly CommandContext _context; private readonly DeploymentLocator _deploymentLocator; private readonly OobeStateService _oobeStateService; @@ -167,11 +176,11 @@ internal sealed class LauncherFlowCoordinator var processExitTask = hostProcess.WaitForExitAsync(); // 等待主程序就绪或进程退出(取先发生者) - // 延长超时到 120 秒,给主程序足够的加载时间 + // 30 秒超时,宿主端有 10 秒兜底机制确保 Ready 信号发送 var readyOrTimeoutOrExit = Task.WhenAny( hostReadyTcs.Task, processExitTask, - Task.Delay(TimeSpan.FromSeconds(120))); + Task.Delay(TimeSpan.FromSeconds(30))); var completedTask = await readyOrTimeoutOrExit; @@ -315,32 +324,55 @@ internal sealed class LauncherFlowCoordinator EnsureExecutable(hostPath); } + var hostWorkingDir = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot(); + var versionInfo = _deploymentLocator.GetVersionInfo(); + + // 构建命令行参数:转发用户参数 + IPC 环境信息通过命令行传递 + // UseShellExecute = true 确保 Shell 启动子进程,使其正确关联到交互式桌面窗口站(WinSta0), + // 避免子进程窗口创建成功但不可见的问题。 + var arguments = new System.Text.StringBuilder(); + + // 转发命令行参数给主程序(排除 Launcher 自己的命令和选项) + // 只过滤 Launcher 专属的选项,保留宿主程序需要的参数(如 --restart-parent-pid) + foreach (var arg in _context.RawArgs) + { + if (arg == _context.Command || arg == _context.SubCommand) + continue; + + if (arg.StartsWith("--")) + { + var key = arg[2..]; + var equalsIndex = key.IndexOf('='); + if (equalsIndex >= 0) key = key[..equalsIndex]; + + if (LauncherOnlyOptions.Contains(key, StringComparer.OrdinalIgnoreCase)) + continue; + } + + if (arguments.Length > 0) arguments.Append(' '); + arguments.Append(QuoteArgument(arg)); + } + + // 通过命令行参数传递 IPC 连接信息(UseShellExecute=true 时不支持 EnvironmentVariables) + if (arguments.Length > 0) arguments.Append(' '); + arguments.Append($"--{LauncherIpcConstants.LauncherPidEnvVar}={Environment.ProcessId}"); + arguments.Append($" --{LauncherIpcConstants.PackageRootEnvVar}={QuoteArgument(_deploymentLocator.GetAppRoot())}"); + arguments.Append($" --{LauncherIpcConstants.VersionEnvVar}={versionInfo.Version}"); + arguments.Append($" --{LauncherIpcConstants.CodenameEnvVar}={versionInfo.Codename}"); + var processStartInfo = new ProcessStartInfo { FileName = hostPath, - UseShellExecute = false, - WorkingDirectory = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot() + UseShellExecute = true, + WorkingDirectory = hostWorkingDir, + Arguments = arguments.ToString() }; - // 转发命令行参数给主程序(排除 Launcher 自己的命令和选项) - foreach (var arg in _context.RawArgs) - { - // 跳过 Launcher 自己的命令和选项,只传递用户原始参数 - if (arg == _context.Command || arg == _context.SubCommand || arg.StartsWith("--")) - { - continue; - } - processStartInfo.ArgumentList.Add(arg); - } - - // 传递环境变量供 IPC 使用 + // 同时设置环境变量作为备选(当 UseShellExecute=true 时 EnvironmentVariables 仍会被子进程继承) processStartInfo.EnvironmentVariables[LauncherIpcConstants.LauncherPidEnvVar] = Environment.ProcessId.ToString(); processStartInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] = _deploymentLocator.GetAppRoot(); - - // 传递版本信息 - var versionInfo = _deploymentLocator.GetVersionInfo(); processStartInfo.EnvironmentVariables[LauncherIpcConstants.VersionEnvVar] = versionInfo.Version; processStartInfo.EnvironmentVariables[LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename; @@ -483,6 +515,36 @@ internal sealed class LauncherFlowCoordinator return result; } + private static string QuoteArgument(string value) + { + if (string.IsNullOrEmpty(value)) + { + return "\"\""; + } + + if (!value.Contains('"') && !value.Contains(' ') && !value.Contains('\t')) + { + return value; + } + + var builder = new System.Text.StringBuilder(); + builder.Append('"'); + foreach (var ch in value) + { + if (ch == '"') + { + builder.Append("\\\""); + } + else + { + builder.Append(ch); + } + } + + builder.Append('"'); + return builder.ToString(); + } + private static void EnsureExecutable(string path) { if (OperatingSystem.IsWindows()) diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 206b754..a615549 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -142,7 +142,7 @@ public partial class App : Application EnsureNotificationService(); } - public override async void OnFrameworkInitializationCompleted() + public override void OnFrameworkInitializationCompleted() { if (Design.IsDesignMode) { @@ -152,12 +152,8 @@ public partial class App : Application AppLogger.Info("App", "Framework initialization completed."); - // 初始化 Launcher IPC 客户端(如果从 Launcher 启动) - await InitializeLauncherIpcAsync(); - RegisterUiUnhandledExceptionGuard(); LinuxDesktopEntryInstaller.EnsureInstalled(); - ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置..."); DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); if (!Design.IsDesignMode && OperatingSystem.IsWindows()) @@ -166,6 +162,10 @@ public partial class App : Application } base.OnFrameworkInitializationCompleted(); + + // IPC 初始化移到窗口创建之后,避免 async void 中的 await 导致窗口创建延迟 + // 使用 fire-and-forget 模式,不阻塞主流程 + _ = InitializeLauncherIpcAsync(); } private async Task InitializeLauncherIpcAsync() @@ -189,9 +189,10 @@ public partial class App : Application // 注册系统初始化加载项 _loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件"); - _loadingStateManager.StartItem("system.init", "正在连接启动器..."); + _loadingStateManager.StartItem("system.init", "已连接启动器"); ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化..."); + ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置..."); } } catch (Exception ex) @@ -227,7 +228,7 @@ public partial class App : Application } /// - /// 同步向 Launcher 报告启动进度,确保关键消息可靠送达 + /// 向 Launcher 报告关键启动进度,使用后台线程避免阻塞 UI /// 用于 Ready 等关键状态报告 /// private void ReportStartupProgressSync(StartupStage stage, int percent, string message) @@ -237,27 +238,27 @@ public partial class App : Application try { - // 使用同步等待确保消息发送完成 - var task = _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage + _ = Task.Run(async () => { - Stage = stage, - ProgressPercent = percent, - Message = message + try + { + await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage + { + Stage = stage, + ProgressPercent = percent, + Message = message + }); + AppLogger.Info("LauncherIpc", $"Successfully reported stage: {stage}"); + } + catch (Exception ex) + { + AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}"); + } }); - - // 等待最多 5 秒,确保消息发送成功 - if (!task.Wait(TimeSpan.FromSeconds(5))) - { - AppLogger.Warn("LauncherIpc", "Report progress timeout after 5 seconds"); - } - else - { - AppLogger.Info("LauncherIpc", $"Successfully reported stage: {stage}"); - } } catch (Exception ex) { - AppLogger.Warn("LauncherIpc", $"Failed to report progress synchronously: {ex.Message}"); + AppLogger.Warn("LauncherIpc", $"Failed to launch progress report task: {ex.Message}"); } } @@ -980,6 +981,27 @@ public partial class App : Application // 使用 Opened 事件确保所有资源已加载完毕 mainWindow.Opened += OnMainWindowOpened; + // 兜底机制:如果 Opened 事件 10 秒内未触发,强制发送 Ready 信号 + // 防止因渲染问题导致 Opened 不触发,启动器 Splash 窗口一直显示 + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(10)); + if (_launcherIpcClient is not null && _launcherIpcClient.IsConnected) + { + try + { + await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage + { + Stage = StartupStage.Ready, + ProgressPercent = 100, + Message = "就绪" + }); + AppLogger.Warn("App", "Ready signal sent via fallback (Opened event did not fire within 10s)"); + } + catch { } + } + }); + return mainWindow; } diff --git a/LanMountainDesktop/Services/AppRestartService.cs b/LanMountainDesktop/Services/AppRestartService.cs index f82c010..a4d28fc 100644 --- a/LanMountainDesktop/Services/AppRestartService.cs +++ b/LanMountainDesktop/Services/AppRestartService.cs @@ -100,12 +100,15 @@ public static class AppRestartService var startInfo = new ProcessStartInfo { FileName = executablePath, - UseShellExecute = false, + UseShellExecute = true, WorkingDirectory = ResolveWorkingDirectory(executablePath, entryAssemblyPath) }; - AppendArguments(startInfo, commandLineArgs); - AppendRestartParentProcessArgument(startInfo); + // UseShellExecute=true 时使用 Arguments 字符串而非 ArgumentList + var args = new System.Text.StringBuilder(); + AppendArgumentsToString(args, commandLineArgs); + AppendRestartParentProcessArgumentToString(args); + startInfo.Arguments = args.ToString(); return startInfo; } @@ -122,13 +125,16 @@ public static class AppRestartService var startInfo = new ProcessStartInfo { FileName = dotnetHostPath, - UseShellExecute = false, + UseShellExecute = true, WorkingDirectory = ResolveWorkingDirectory(dotnetHostPath, entryAssemblyPath) }; - startInfo.ArgumentList.Add(entryAssemblyPath); - AppendArguments(startInfo, commandLineArgs); - AppendRestartParentProcessArgument(startInfo); + // UseShellExecute=true 时使用 Arguments 字符串 + var args = new System.Text.StringBuilder(); + args.Append(QuoteArgument(entryAssemblyPath)); + AppendArgumentsToString(args, commandLineArgs); + AppendRestartParentProcessArgumentToString(args); + startInfo.Arguments = args.ToString(); return startInfo; } @@ -145,11 +151,61 @@ public static class AppRestartService } } + private static void AppendArgumentsToString(System.Text.StringBuilder builder, IReadOnlyList commandLineArgs) + { + for (var i = 1; i < commandLineArgs.Count; i++) + { + if (TryParseRestartParentProcessId(commandLineArgs[i], out _)) + { + continue; + } + + if (builder.Length > 0) builder.Append(' '); + builder.Append(QuoteArgument(commandLineArgs[i])); + } + } + private static void AppendRestartParentProcessArgument(ProcessStartInfo startInfo) { startInfo.ArgumentList.Add($"{RestartParentPidArgumentPrefix}{Environment.ProcessId}"); } + private static void AppendRestartParentProcessArgumentToString(System.Text.StringBuilder builder) + { + if (builder.Length > 0) builder.Append(' '); + builder.Append($"{RestartParentPidArgumentPrefix}{Environment.ProcessId}"); + } + + private static string QuoteArgument(string value) + { + if (string.IsNullOrEmpty(value)) + { + return "\"\""; + } + + if (!value.Contains('"') && !value.Contains(' ') && !value.Contains('\t')) + { + return value; + } + + var builder = new System.Text.StringBuilder(); + builder.Append('"'); + foreach (var ch in value) + { + if (ch == '"') + { + builder.Append("\\\""); + } + else + { + builder.Append(ch); + } + } + + builder.Append('"'); + return builder.ToString(); + } + private static bool TryParseRestartParentProcessId(string? argument, out int processId) { processId = 0; diff --git a/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs b/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs index 04a5fa1..f05c200 100644 --- a/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs +++ b/LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs @@ -17,6 +17,11 @@ public class LauncherIpcClient : IDisposable private bool _isConnected; private readonly object _writeLock = new(); + /// + /// 是否已连接到 Launcher + /// + public bool IsConnected => _isConnected && _pipeClient?.IsConnected == true; + /// /// 协议:每条消息以 4 字节小端 int32 长度前缀开头,后跟 UTF-8 JSON 正文。 /// @@ -92,11 +97,28 @@ public class LauncherIpcClient : IDisposable /// /// 检查是否从 Launcher 启动 + /// 优先检查环境变量,回退到命令行参数(UseShellExecute=true 时环境变量仍可继承, + /// 命令行参数作为备选确保兼容性) /// public static bool IsLaunchedByLauncher() { - return !string.IsNullOrEmpty( - Environment.GetEnvironmentVariable(LauncherIpcConstants.LauncherPidEnvVar)); + // 优先检查环境变量 + if (!string.IsNullOrEmpty( + Environment.GetEnvironmentVariable(LauncherIpcConstants.LauncherPidEnvVar))) + { + return true; + } + + // 回退到命令行参数检查(格式: --LMD_LAUNCHER_PID=) + foreach (var arg in Environment.GetCommandLineArgs()) + { + if (arg.StartsWith($"--{LauncherIpcConstants.LauncherPidEnvVar}=", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; } public void Dispose() diff --git a/LanMountainDesktop/Services/Loading/LoadingTimeoutHandler.cs b/LanMountainDesktop/Services/Loading/LoadingTimeoutHandler.cs index a163c62..612c1e0 100644 --- a/LanMountainDesktop/Services/Loading/LoadingTimeoutHandler.cs +++ b/LanMountainDesktop/Services/Loading/LoadingTimeoutHandler.cs @@ -1,4 +1,5 @@ using System.Timers; +using LanMountainDesktop.Shared.Contracts.Launcher; namespace LanMountainDesktop.Services.Loading; diff --git a/LanMountainDesktop/app.manifest b/LanMountainDesktop/app.manifest index 42bb96e..e46fabd 100644 --- a/LanMountainDesktop/app.manifest +++ b/LanMountainDesktop/app.manifest @@ -5,13 +5,18 @@ For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> + + + + + + + + + - - - +