fix.我们试验性地修复了启动器无法正常启动的问题,原因可能是这个画面没有启动,就GUI没显示。然后还把编译问题修了一下。

This commit is contained in:
lincube
2026-04-18 23:36:31 +08:00
parent e8d2575bc1
commit 9cf3a15c89
6 changed files with 224 additions and 56 deletions

View File

@@ -9,6 +9,15 @@ namespace LanMountainDesktop.Launcher.Services;
internal sealed class LauncherFlowCoordinator 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 CommandContext _context;
private readonly DeploymentLocator _deploymentLocator; private readonly DeploymentLocator _deploymentLocator;
private readonly OobeStateService _oobeStateService; private readonly OobeStateService _oobeStateService;
@@ -167,11 +176,11 @@ internal sealed class LauncherFlowCoordinator
var processExitTask = hostProcess.WaitForExitAsync(); var processExitTask = hostProcess.WaitForExitAsync();
// 等待主程序就绪或进程退出(取先发生者) // 等待主程序就绪或进程退出(取先发生者)
// 延长超时到 120 秒,给主程序足够的加载时间 // 30 秒超时,宿主端有 10 秒兜底机制确保 Ready 信号发送
var readyOrTimeoutOrExit = Task.WhenAny( var readyOrTimeoutOrExit = Task.WhenAny(
hostReadyTcs.Task, hostReadyTcs.Task,
processExitTask, processExitTask,
Task.Delay(TimeSpan.FromSeconds(120))); Task.Delay(TimeSpan.FromSeconds(30)));
var completedTask = await readyOrTimeoutOrExit; var completedTask = await readyOrTimeoutOrExit;
@@ -315,32 +324,55 @@ internal sealed class LauncherFlowCoordinator
EnsureExecutable(hostPath); 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 var processStartInfo = new ProcessStartInfo
{ {
FileName = hostPath, FileName = hostPath,
UseShellExecute = false, UseShellExecute = true,
WorkingDirectory = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot() WorkingDirectory = hostWorkingDir,
Arguments = arguments.ToString()
}; };
// 转发命令行参数给主程序(排除 Launcher 自己的命令和选项 // 同时设置环境变量作为备选(当 UseShellExecute=true 时 EnvironmentVariables 仍会被子进程继承
foreach (var arg in _context.RawArgs)
{
// 跳过 Launcher 自己的命令和选项,只传递用户原始参数
if (arg == _context.Command || arg == _context.SubCommand || arg.StartsWith("--"))
{
continue;
}
processStartInfo.ArgumentList.Add(arg);
}
// 传递环境变量供 IPC 使用
processStartInfo.EnvironmentVariables[LauncherIpcConstants.LauncherPidEnvVar] = processStartInfo.EnvironmentVariables[LauncherIpcConstants.LauncherPidEnvVar] =
Environment.ProcessId.ToString(); Environment.ProcessId.ToString();
processStartInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] = processStartInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] =
_deploymentLocator.GetAppRoot(); _deploymentLocator.GetAppRoot();
// 传递版本信息
var versionInfo = _deploymentLocator.GetVersionInfo();
processStartInfo.EnvironmentVariables[LauncherIpcConstants.VersionEnvVar] = versionInfo.Version; processStartInfo.EnvironmentVariables[LauncherIpcConstants.VersionEnvVar] = versionInfo.Version;
processStartInfo.EnvironmentVariables[LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename; processStartInfo.EnvironmentVariables[LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename;
@@ -483,6 +515,36 @@ internal sealed class LauncherFlowCoordinator
return result; 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) private static void EnsureExecutable(string path)
{ {
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())

View File

@@ -142,7 +142,7 @@ public partial class App : Application
EnsureNotificationService(); EnsureNotificationService();
} }
public override async void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
@@ -152,12 +152,8 @@ public partial class App : Application
AppLogger.Info("App", "Framework initialization completed."); AppLogger.Info("App", "Framework initialization completed.");
// 初始化 Launcher IPC 客户端(如果从 Launcher 启动)
await InitializeLauncherIpcAsync();
RegisterUiUnhandledExceptionGuard(); RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled(); LinuxDesktopEntryInstaller.EnsureInstalled();
ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置...");
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
if (!Design.IsDesignMode && OperatingSystem.IsWindows()) if (!Design.IsDesignMode && OperatingSystem.IsWindows())
@@ -166,6 +162,10 @@ public partial class App : Application
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
// IPC 初始化移到窗口创建之后,避免 async void 中的 await 导致窗口创建延迟
// 使用 fire-and-forget 模式,不阻塞主流程
_ = InitializeLauncherIpcAsync();
} }
private async Task InitializeLauncherIpcAsync() private async Task InitializeLauncherIpcAsync()
@@ -189,9 +189,10 @@ public partial class App : Application
// 注册系统初始化加载项 // 注册系统初始化加载项
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件"); _loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件");
_loadingStateManager.StartItem("system.init", "正在连接启动器..."); _loadingStateManager.StartItem("system.init", "连接启动器");
ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化..."); ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化...");
ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置...");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -227,7 +228,7 @@ public partial class App : Application
} }
/// <summary> /// <summary>
/// 同步向 Launcher 报告启动进度,确保关键消息可靠送达 /// 向 Launcher 报告关键启动进度,使用后台线程避免阻塞 UI
/// 用于 Ready 等关键状态报告 /// 用于 Ready 等关键状态报告
/// </summary> /// </summary>
private void ReportStartupProgressSync(StartupStage stage, int percent, string message) private void ReportStartupProgressSync(StartupStage stage, int percent, string message)
@@ -237,27 +238,27 @@ public partial class App : Application
try try
{ {
// 使用同步等待确保消息发送完成 _ = Task.Run(async () =>
var task = _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage {
try
{
await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage
{ {
Stage = stage, Stage = stage,
ProgressPercent = percent, ProgressPercent = percent,
Message = message Message = 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}"); AppLogger.Info("LauncherIpc", $"Successfully reported stage: {stage}");
} }
}
catch (Exception ex) catch (Exception ex)
{ {
AppLogger.Warn("LauncherIpc", $"Failed to report progress synchronously: {ex.Message}"); AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
}
});
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to launch progress report task: {ex.Message}");
} }
} }
@@ -980,6 +981,27 @@ public partial class App : Application
// 使用 Opened 事件确保所有资源已加载完毕 // 使用 Opened 事件确保所有资源已加载完毕
mainWindow.Opened += OnMainWindowOpened; 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; return mainWindow;
} }

View File

@@ -100,12 +100,15 @@ public static class AppRestartService
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = executablePath, FileName = executablePath,
UseShellExecute = false, UseShellExecute = true,
WorkingDirectory = ResolveWorkingDirectory(executablePath, entryAssemblyPath) WorkingDirectory = ResolveWorkingDirectory(executablePath, entryAssemblyPath)
}; };
AppendArguments(startInfo, commandLineArgs); // UseShellExecute=true 时使用 Arguments 字符串而非 ArgumentList
AppendRestartParentProcessArgument(startInfo); var args = new System.Text.StringBuilder();
AppendArgumentsToString(args, commandLineArgs);
AppendRestartParentProcessArgumentToString(args);
startInfo.Arguments = args.ToString();
return startInfo; return startInfo;
} }
@@ -122,13 +125,16 @@ public static class AppRestartService
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = dotnetHostPath, FileName = dotnetHostPath,
UseShellExecute = false, UseShellExecute = true,
WorkingDirectory = ResolveWorkingDirectory(dotnetHostPath, entryAssemblyPath) WorkingDirectory = ResolveWorkingDirectory(dotnetHostPath, entryAssemblyPath)
}; };
startInfo.ArgumentList.Add(entryAssemblyPath); // UseShellExecute=true 时使用 Arguments 字符串
AppendArguments(startInfo, commandLineArgs); var args = new System.Text.StringBuilder();
AppendRestartParentProcessArgument(startInfo); args.Append(QuoteArgument(entryAssemblyPath));
AppendArgumentsToString(args, commandLineArgs);
AppendRestartParentProcessArgumentToString(args);
startInfo.Arguments = args.ToString();
return startInfo; return startInfo;
} }
@@ -145,11 +151,61 @@ public static class AppRestartService
} }
} }
private static void AppendArgumentsToString(System.Text.StringBuilder builder, IReadOnlyList<string> 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) private static void AppendRestartParentProcessArgument(ProcessStartInfo startInfo)
{ {
startInfo.ArgumentList.Add($"{RestartParentPidArgumentPrefix}{Environment.ProcessId}"); 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) private static bool TryParseRestartParentProcessId(string? argument, out int processId)
{ {
processId = 0; processId = 0;

View File

@@ -17,6 +17,11 @@ public class LauncherIpcClient : IDisposable
private bool _isConnected; private bool _isConnected;
private readonly object _writeLock = new(); private readonly object _writeLock = new();
/// <summary>
/// 是否已连接到 Launcher
/// </summary>
public bool IsConnected => _isConnected && _pipeClient?.IsConnected == true;
/// <summary> /// <summary>
/// 协议:每条消息以 4 字节小端 int32 长度前缀开头,后跟 UTF-8 JSON 正文。 /// 协议:每条消息以 4 字节小端 int32 长度前缀开头,后跟 UTF-8 JSON 正文。
/// </summary> /// </summary>
@@ -92,11 +97,28 @@ public class LauncherIpcClient : IDisposable
/// <summary> /// <summary>
/// 检查是否从 Launcher 启动 /// 检查是否从 Launcher 启动
/// 优先检查环境变量回退到命令行参数UseShellExecute=true 时环境变量仍可继承,
/// 命令行参数作为备选确保兼容性)
/// </summary> /// </summary>
public static bool IsLaunchedByLauncher() public static bool IsLaunchedByLauncher()
{ {
return !string.IsNullOrEmpty( // 优先检查环境变量
Environment.GetEnvironmentVariable(LauncherIpcConstants.LauncherPidEnvVar)); if (!string.IsNullOrEmpty(
Environment.GetEnvironmentVariable(LauncherIpcConstants.LauncherPidEnvVar)))
{
return true;
}
// 回退到命令行参数检查(格式: --LMD_LAUNCHER_PID=<value>
foreach (var arg in Environment.GetCommandLineArgs())
{
if (arg.StartsWith($"--{LauncherIpcConstants.LauncherPidEnvVar}=", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
} }
public void Dispose() public void Dispose()

View File

@@ -1,4 +1,5 @@
using System.Timers; using System.Timers;
using LanMountainDesktop.Shared.Contracts.Launcher;
namespace LanMountainDesktop.Services.Loading; namespace LanMountainDesktop.Services.Loading;

View File

@@ -5,13 +5,18 @@
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="LanMountainDesktop.Desktop"/> <assemblyIdentity version="1.0.0.0" name="LanMountainDesktop.Desktop"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- 明确指定不需要管理员权限,以调用者权限运行 -->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>
<!-- A list of the Windows versions that this application has been tested on <!-- Windows 10/11 -->
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application> </application>
</compatibility> </compatibility>