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 -->
+
+
+
+
+
+
+
+
+
-
-
-
+