Refactor launcher startup, logging & host resolution

Improve launcher startup flow, logging, and host resolution. Key changes: add detailed startup logging and standardized preview messages; unify CLI vs GUI handling and error/result reporting (write result file when requested); refactor DeploymentLocator to a more robust host resolution (new HostResolutionResult, explicit/portable/published/debug resolution paths, legacy fallback); overhaul LauncherFlowCoordinator to better handle IPC stages, activation retries, window lifecycle, plugin/update flows and error reporting; add CommandContext helpers (IsGui/IsPreview/ExplicitAppRoot) and JSON context options; tighten async usage and ConfigureAwait calls; add better UI error handling and consistent exit codes. Several UX/debug conveniences and robustness fixes included.
This commit is contained in:
lincube
2026-04-22 07:31:54 +08:00
parent 5af7ac8b56
commit 703ed7b48a
13 changed files with 1172 additions and 867 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -79,6 +80,10 @@ public partial class App : Application
private LoadingStateReporter? _loadingStateReporter;
private bool _singleInstanceReleased;
private int _forcedExitScheduled;
private bool _mainWindowOpened;
private bool _trayInitialized;
private readonly object _launcherProgressLock = new();
private readonly List<StartupProgressMessage> _pendingLauncherProgressMessages = [];
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
@@ -86,10 +91,9 @@ public partial class App : Application
internal static INotificationService? CurrentNotificationService =>
(Current as App)?._notificationService;
// 隐私政策查看事件
// 闅愮鏀跨瓥鏌ョ湅浜嬩欢
public static event Action? CurrentPrivacyPolicyViewRequested;
// 触发隐私政策查看事件的方法
public static void RaisePrivacyPolicyViewRequested()
{
CurrentPrivacyPolicyViewRequested?.Invoke();
@@ -156,6 +160,7 @@ public partial class App : Application
RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled();
_ = InitializeLauncherIpcAsync();
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
@@ -164,37 +169,43 @@ public partial class App : Application
}
base.OnFrameworkInitializationCompleted();
// IPC 初始化移到窗口创建之后,避免 async void 中的 await 导致窗口创建延迟
// 使用 fire-and-forget 模式,不阻塞主流程
_ = InitializeLauncherIpcAsync();
}
private async Task InitializeLauncherIpcAsync()
{
if (!LauncherIpcClient.IsLaunchedByLauncher())
return;
try
{
_launcherIpcClient = new LauncherIpcClient();
var connected = await _launcherIpcClient.ConnectAsync();
if (connected)
if (!connected)
{
AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server.");
// 初始化加载状态管理器
_loadingStateManager = new LoadingStateManager();
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient);
_loadingStateReporter.Start();
// 注册系统初始化加载项
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件");
_loadingStateManager.StartItem("system.init", "已连接启动器");
ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化...");
ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置...");
return;
}
AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server.");
bool hadBufferedMessages;
lock (_launcherProgressLock)
{
hadBufferedMessages = _pendingLauncherProgressMessages.Count > 0;
}
await FlushPendingLauncherProgressAsync();
_loadingStateManager = new LoadingStateManager();
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient);
_loadingStateReporter.Start();
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "System Initialization", "Initialize core application services.");
_loadingStateManager.StartItem("system.init", "Launcher IPC connected.");
if (!hadBufferedMessages)
{
ReportStartupProgress(StartupStage.Initializing, 10, "Initializing application...");
ReportStartupProgress(StartupStage.LoadingSettings, 20, "Loading settings...");
}
}
catch (Exception ex)
@@ -203,67 +214,86 @@ public partial class App : Application
}
}
/// <summary>
/// 向 Launcher 报告启动进度fire-and-forget不阻塞主流程
/// </summary>
private void ReportStartupProgress(StartupStage stage, int percent, string message)
{
if (_launcherIpcClient is null)
return;
_ = Task.Run(async () =>
QueueOrSendLauncherProgress(new StartupProgressMessage
{
try
{
await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage
{
Stage = stage,
ProgressPercent = percent,
Message = message
});
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
}
});
Stage = stage,
ProgressPercent = percent,
Message = message,
Timestamp = DateTimeOffset.UtcNow
}, logSuccess: false);
}
/// <summary>
/// 向 Launcher 报告关键启动进度,使用后台线程避免阻塞 UI
/// 用于 Ready 等关键状态报告
/// </summary>
private void ReportStartupProgressSync(StartupStage stage, int percent, string message)
{
if (_launcherIpcClient is null)
return;
QueueOrSendLauncherProgress(new StartupProgressMessage
{
Stage = stage,
ProgressPercent = percent,
Message = message,
Timestamp = DateTimeOffset.UtcNow
}, logSuccess: true);
}
private void QueueOrSendLauncherProgress(StartupProgressMessage message, bool logSuccess)
{
var ipcClient = _launcherIpcClient;
if (ipcClient is null || !ipcClient.IsConnected)
{
lock (_launcherProgressLock)
{
_pendingLauncherProgressMessages.Add(message);
}
AppLogger.Info("LauncherIpc", $"Buffered launcher stage '{message.Stage}' because IPC is not connected yet.");
return;
}
_ = SendLauncherProgressAsync(ipcClient, message, logSuccess);
}
private async Task FlushPendingLauncherProgressAsync()
{
var ipcClient = _launcherIpcClient;
if (ipcClient is null || !ipcClient.IsConnected)
{
return;
}
StartupProgressMessage[] pendingMessages;
lock (_launcherProgressLock)
{
pendingMessages = _pendingLauncherProgressMessages.ToArray();
_pendingLauncherProgressMessages.Clear();
}
foreach (var pendingMessage in pendingMessages)
{
await SendLauncherProgressAsync(ipcClient, pendingMessage, logSuccess: false);
}
}
private async Task SendLauncherProgressAsync(LauncherIpcClient ipcClient, StartupProgressMessage message, bool logSuccess)
{
try
{
_ = Task.Run(async () =>
await ipcClient.ReportProgressAsync(message);
if (logSuccess)
{
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}");
}
});
AppLogger.Info("LauncherIpc", $"Successfully reported stage: {message.Stage}");
}
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to launch progress report task: {ex.Message}");
AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
lock (_launcherProgressLock)
{
_pendingLauncherProgressMessages.Add(message);
}
}
}
private void ApplyDesignTimeTheme()
{
RequestedThemeVariant = ThemeVariant.Light;
@@ -289,7 +319,7 @@ public partial class App : Application
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
ReportStartupProgress(StartupStage.InitializingUI, 60, "正在初始化界面...");
ReportStartupProgress(StartupStage.InitializingUI, 60, "姝e湪鍒濆鍖栫晫闈?..");
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
},
OnDesktopLifetimeExit,
@@ -337,25 +367,21 @@ public partial class App : Application
_ = sender;
_ = e;
// 仅在 Windows 上支持融合桌面功能
if (!OperatingSystem.IsWindows())
{
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
return;
}
// 切换进入编辑模式,隐藏常态零散的小部件
FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode();
// 确保透明覆盖层窗口存在并显示
// 纭繚閫忔槑瑕嗙洊灞傜獥鍙e瓨鍦ㄥ苟鏄剧ず
EnsureTransparentOverlayWindow();
// 打开融合桌面组件库窗口
Dispatcher.UIThread.Post(() =>
{
try
{
// 确保覆盖层窗口已显示(组件要渲染在上面,必须先 Show
if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible)
{
_transparentOverlayWindow.Show();
@@ -368,16 +394,15 @@ public partial class App : Application
window.SetOverlayWindow(_transparentOverlayWindow);
}
// 当组件库关闭时,退出编辑态
window.Closed += (s, ev) =>
window.Closed += (s, ev) =>
{
if (_transparentOverlayWindow is not null)
{
// 触发画布保存,并隐藏画布
// 瑙﹀彂鐢诲竷淇濆瓨锛屽苟闅愯棌鐢诲竷
_transparentOverlayWindow.SaveLayoutAndHide();
}
// 让管理器根据已存储的最新快照重建生成所有实体小组件
// 璁╃鐞嗗櫒鏍规嵁宸插瓨鍌ㄧ殑鏈€鏂板揩鐓ч噸寤虹敓鎴愭墍鏈夊疄浣撳皬缁勪欢
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
};
@@ -434,7 +459,7 @@ public partial class App : Application
private void InitializePluginRuntime()
{
ReportStartupProgress(StartupStage.LoadingPlugins, 30, "正在加载插件...");
ReportStartupProgress(StartupStage.LoadingPlugins, 30, "姝e湪鍔犺浇鎻掍欢...");
try
{
_pluginRuntimeService?.Dispose();
@@ -489,9 +514,12 @@ public partial class App : Application
}
RefreshTrayIconContent();
_trayInitialized = true;
AppLogger.Info("TrayIcon", $"Tray initialized successfully. Pid={Environment.ProcessId}.");
}
catch (Exception ex)
{
_trayInitialized = false;
AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex);
}
}
@@ -537,14 +565,12 @@ public partial class App : Application
return;
}
// 仅在 Windows 上支持融合桌面功能
if (!OperatingSystem.IsWindows())
{
_trayComponentLibraryMenuItem.IsVisible = false;
return;
}
// 检查融合桌面功能是否启用
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
_trayComponentLibraryMenuItem.IsVisible = appSnapshot.EnableFusedDesktop;
@@ -855,13 +881,12 @@ public partial class App : Application
if (languageChanged)
{
// 清除本地化缓存,强制重新加载语言文件
// 娓呴櫎鏈湴鍖栫紦瀛橈紝寮哄埗閲嶆柊鍔犺浇璇█鏂囦欢
_localizationService.ClearCache();
ApplyCurrentCultureFromSettings();
RefreshTrayIconContent();
}
// 检查融合桌面设置是否变更
var fusedDesktopChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase);
@@ -1076,64 +1101,49 @@ public partial class App : Application
ShowInTaskbar = true
};
_mainWindowOpened = false;
AttachMainWindow(mainWindow);
desktop.MainWindow = mainWindow;
AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}");
LogBrowserStartupDiagnostics();
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}");
// 延迟报告 Ready 直到窗口实际打开并可见
// 使用 Opened 事件确保所有资源已加载完毕
ReportStartupProgress(StartupStage.ShellInitialized, 85, "Desktop shell initialized.");
AppLogger.Info(
"App",
$"Shell initialized. Reason='{reason}'; TrayInitialized={_trayInitialized}; MainWindowVisible={mainWindow.IsVisible}.");
mainWindow.Opened += OnMainWindowOpened;
// 手动显示窗口,因为在 ShutdownMode.OnExplicitShutdown 模式下框架不会自动调用 Show
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
// 兜底机制:如果 Opened 事件 10 秒内未触发,强制发送 Ready 信号
// 防止因渲染问题导致 Opened 不触发,启动器 Splash 窗口一直显示
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(10));
if (_launcherIpcClient is not null && _launcherIpcClient.IsConnected)
await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
if (!_mainWindowOpened)
{
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 { }
AppLogger.Warn("App", "Main window Opened event did not fire within 10 seconds. DesktopVisible was not reported.");
}
});
return mainWindow;
}
/// <summary>
/// 主窗口打开完成事件 - 此时所有组件、资源及功能模块均已完全加载
/// </summary>
private void OnMainWindowOpened(object? sender, EventArgs e)
{
if (sender is MainWindow mainWindow)
{
mainWindow.Opened -= OnMainWindowOpened;
AppLogger.Info("App", "Main window opened and ready. Reporting Ready to Launcher...");
// 完成系统初始化加载项
_loadingStateManager?.CompleteItem("system.init", "系统初始化完成");
// 报告 Ready 状态,启动器可以安全关闭 Splash 窗口
ReportStartupProgressSync(StartupStage.Ready, 100, "就绪");
// 停止加载状态上报
_mainWindowOpened = true;
AppLogger.Info(
"App",
$"Main window opened. Reporting DesktopVisible. TrayInitialized={_trayInitialized}; ShellState='{_desktopShellState}'.");
_loadingStateManager?.CompleteItem("system.init", "System initialization completed.");
ReportStartupProgressSync(StartupStage.DesktopVisible, 100, "Desktop visible.");
ReportStartupProgressSync(StartupStage.Ready, 100, "Ready.");
_loadingStateReporter?.Stop();
}
}
@@ -1327,3 +1337,5 @@ public partial class App : Application
return _localizationService.GetString(languageCode, key, fallback);
}
}

View File

@@ -8,6 +8,7 @@ using LanMountainDesktop.DesktopHost;
using LanMountainDesktop.Models;
using LanMountainDesktop.Plugins;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Launcher;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Shared.Contracts.Launcher;
@@ -33,6 +34,7 @@ public sealed class Program
AppLogger.Warn(
"Startup",
$"Restart relaunch could not acquire the single-instance lock. pid={restartParentProcessId.Value}. Suppressing multi-open activation prompt.");
ReportLauncherStageBeforeExit(StartupStage.ActivationFailed, "Restart relaunch could not acquire the single-instance lock.");
Environment.ExitCode = HostExitCodes.RestartLockNotAcquired;
return;
}
@@ -43,6 +45,7 @@ public sealed class Program
AppLogger.Info(
"Startup",
$"Secondary launch forwarded to primary instance successfully. Acked={activationAcknowledged}; Pid={Environment.ProcessId}.");
ReportLauncherStageBeforeExit(StartupStage.ActivationRedirected, "Secondary launch forwarded to the primary instance.");
Environment.ExitCode = HostExitCodes.SecondaryActivationSucceeded;
}
else
@@ -50,6 +53,9 @@ public sealed class Program
AppLogger.Warn(
"Startup",
$"Secondary launch failed to activate the primary instance. Acked={activationAcknowledged}; Reason='{failureReason ?? "unknown"}'; Pid={Environment.ProcessId}.");
ReportLauncherStageBeforeExit(
StartupStage.ActivationFailed,
$"Secondary launch failed to activate the primary instance. Reason='{failureReason ?? "unknown"}'.");
Environment.ExitCode = HostExitCodes.SecondaryActivationFailed;
}
@@ -247,6 +253,35 @@ public sealed class Program
};
}
private static void ReportLauncherStageBeforeExit(StartupStage stage, string message)
{
if (!LauncherIpcClient.IsLaunchedByLauncher())
{
return;
}
try
{
using var launcherIpcClient = new LauncherIpcClient();
var connected = launcherIpcClient.ConnectAsync().GetAwaiter().GetResult();
if (!connected)
{
return;
}
launcherIpcClient.ReportProgressAsync(new StartupProgressMessage
{
Stage = stage,
ProgressPercent = 100,
Message = message
}).GetAwaiter().GetResult();
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to report early launcher stage '{stage}'.", ex);
}
}
private static void InitializeTelemetryIdentity()
{
try

View File

@@ -13,6 +13,11 @@ namespace LanMountainDesktop.Services.Launcher;
/// </summary>
public class LauncherIpcClient : IDisposable
{
private static readonly JsonSerializerOptions StartupProgressJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
private NamedPipeClientStream? _pipeClient;
private bool _isConnected;
private readonly object _writeLock = new();
@@ -65,7 +70,7 @@ public class LauncherIpcClient : IDisposable
try
{
var json = JsonSerializer.Serialize(message);
var json = JsonSerializer.Serialize(message, StartupProgressJsonOptions);
var payload = System.Text.Encoding.UTF8.GetBytes(json);
// 长度前缀协议:[4字节长度][消息正文]