From 9283da59400abb2294e7dabb4b8c81e80f4c951a Mon Sep 17 00:00:00 2001 From: lincube Date: Fri, 17 Apr 2026 22:33:41 +0800 Subject: [PATCH] =?UTF-8?q?changed.=E8=B0=83=E6=95=B4=E4=BA=86=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BA=86?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=A1=B5=E9=9D=A2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LanMountainDesktop.Launcher/App.axaml.cs | 45 ++-- .../Services/DeploymentLocator.cs | 230 +++++++++++++--- .../Services/FlexibleHostLocator.cs | 99 ++++--- .../Services/LauncherFlowCoordinator.cs | 4 +- .../Services/Logger.cs | 138 ++++++++++ .../Services/OobeStateService.cs | 96 ++++++- .../Services/UpdateEngineService.cs | 1 + .../Views/ErrorWindow.axaml | 35 ++- .../Views/ErrorWindow.axaml.cs | 252 +++++++++++++++--- .../Views/UpdateWindow.axaml | 130 +++++---- .../Views/UpdateWindow.axaml.cs | 42 +-- 11 files changed, 838 insertions(+), 234 deletions(-) create mode 100644 LanMountainDesktop.Launcher/Services/Logger.cs diff --git a/LanMountainDesktop.Launcher/App.axaml.cs b/LanMountainDesktop.Launcher/App.axaml.cs index 2098c2a..f545da1 100644 --- a/LanMountainDesktop.Launcher/App.axaml.cs +++ b/LanMountainDesktop.Launcher/App.axaml.cs @@ -13,6 +13,10 @@ public partial class App : Application { public override void Initialize() { + // 初始化日志记录器 + Logger.Initialize(); + Logger.Info("Launcher starting..."); + AvaloniaXamlLoader.Load(this); } @@ -50,27 +54,12 @@ public partial class App : Application } else { - // 正常启动流程 - var appRoot = Commands.ResolveAppRoot(context); - var deploymentLocator = new DeploymentLocator(appRoot); - - // TODO: 从配置读取 GitHub 仓库信息 - var updateCheckService = new UpdateCheckService("ClassIsland", "LanMountainDesktop"); - - var coordinator = new LauncherFlowCoordinator( - context, - deploymentLocator, - new OobeStateService(appRoot), - new UpdateEngineService(deploymentLocator), - updateCheckService, - new PluginInstallerService()); - // 先显示 Splash 窗口,确保应用程序不会立即退出 var splashWindow = new SplashWindow(); splashWindow.Show(); - // 启动协调器流程 - _ = RunCoordinatorWithSplashAsync(desktop, coordinator, splashWindow); + // 在 try-catch 块中实例化所有服务,确保任何异常都能被捕获 + _ = RunCoordinatorWithSplashAsync(desktop, context, splashWindow); } } @@ -211,14 +200,30 @@ public partial class App : Application private static async Task RunCoordinatorWithSplashAsync( IClassicDesktopStyleApplicationLifetime desktop, - LauncherFlowCoordinator coordinator, + CommandContext context, SplashWindow splashWindow) { LauncherResult result; ErrorWindow? errorWindow = null; + LauncherFlowCoordinator? coordinator = null; try { + // 在 try-catch 块中实例化所有服务,确保异常被捕获 + var appRoot = Commands.ResolveAppRoot(context); + var deploymentLocator = new DeploymentLocator(appRoot); + + // TODO: 从配置读取 GitHub 仓库信息 + var updateCheckService = new UpdateCheckService("ClassIsland", "LanMountainDesktop"); + + coordinator = new LauncherFlowCoordinator( + context, + deploymentLocator, + new OobeStateService(appRoot), + new UpdateEngineService(deploymentLocator), + updateCheckService, + new PluginInstallerService()); + result = await coordinator.RunAsync(splashWindow).ConfigureAwait(false); } catch (Exception ex) @@ -344,11 +349,11 @@ public partial class App : Application } } - // 3. 清理旧版本 + // 3. 清理旧版本,保留至少3个版本以支持回滚 if (success) { await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "正在清理...", 90)); - deploymentLocator.CleanupDestroyedDeployments(); + deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3); } } catch (Exception ex) diff --git a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs index c1a17d1..c0d3f72 100644 --- a/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs +++ b/LanMountainDesktop.Launcher/Services/DeploymentLocator.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text.Json; +using LanMountainDesktop.Launcher.Models; using LanMountainDesktop.Shared.Contracts.Launcher; namespace LanMountainDesktop.Launcher.Services; @@ -17,44 +18,65 @@ internal sealed class DeploymentLocator public string? FindCurrentDeploymentDirectory() { - var candidates = Directory.Exists(_appRoot) - ? Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly) - : []; + Console.WriteLine("[DeploymentLocator] Searching for deployment directories (ClassIsland style)..."); - // 过滤掉无效的部署目录 - var validCandidates = candidates - .Where(path => - !File.Exists(Path.Combine(path, ".destroy")) && // 排除待删除 - !File.Exists(Path.Combine(path, ".partial"))) // 排除未完成 - .ToList(); - - // 优先选择带 .current 标记的版本 - var withMarkers = validCandidates - .Where(path => File.Exists(Path.Combine(path, ".current"))) - .Select(path => new - { - Path = path, - Version = ParseVersionFromDirectory(path) - }) - .OrderByDescending(item => item.Version) - .ToList(); - - if (withMarkers.Count > 0) + if (!Directory.Exists(_appRoot)) { - return withMarkers[0].Path; + Console.WriteLine("[DeploymentLocator] App root directory does not exist"); + return null; } - // 如果没有 .current 标记,选择最新版本 - var byVersion = validCandidates - .Select(path => new - { - Path = path, - Version = ParseVersionFromDirectory(path) - }) - .OrderByDescending(item => item.Version) - .ToList(); + var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop"; - return byVersion.Count > 0 ? byVersion[0].Path : null; + try + { + var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly); + Console.WriteLine($"[DeploymentLocator] Found {candidates.Length} app-* directories"); + + // ClassIsland 风格的查询:先筛选,后排序 + var validInstallations = candidates + .Where(path => + { + var hasDestroy = File.Exists(Path.Combine(path, ".destroy")); + var hasPartial = File.Exists(Path.Combine(path, ".partial")); + var hasExe = File.Exists(Path.Combine(path, executable)); + var hasCurrent = File.Exists(Path.Combine(path, ".current")); + var version = ParseVersionFromDirectory(path); + + Console.WriteLine($"[DeploymentLocator] Candidate: {Path.GetFileName(path)} | " + + $"Version={version} | " + + $"Current={hasCurrent} | " + + $"Destroy={hasDestroy} | " + + $"Partial={hasPartial} | " + + $"HasExe={hasExe}"); + + return !hasDestroy && !hasPartial && hasExe; + }) + .Select(path => new + { + Path = path, + Version = ParseVersionFromDirectory(path), + HasCurrentMarker = File.Exists(Path.Combine(path, ".current")) + }) + .OrderBy(x => x.HasCurrentMarker ? 0 : 1) // .current 标记的排前面 + .ThenByDescending(x => x.Version) // 然后按版本号降序 + .ToList(); + + if (validInstallations.Count == 0) + { + Console.WriteLine("[DeploymentLocator] No valid deployment directories found"); + return null; + } + + var best = validInstallations[0]; + Console.WriteLine($"[DeploymentLocator] Selected: {Path.GetFileName(best.Path)} (current={best.HasCurrentMarker}, version={best.Version})"); + return best.Path; + } + catch (Exception ex) + { + Console.Error.WriteLine($"[DeploymentLocator] Error searching for deployments: {ex}"); + return null; + } } public string? ResolveHostExecutablePath() @@ -233,35 +255,159 @@ internal sealed class DeploymentLocator } } - public void CleanupDestroyedDeployments() + /// + /// 清理旧版本部署,保留最近的N个版本 + /// + /// 最少保留版本数,默认3个 + public void CleanupOldDeployments(int minVersionsToKeep = 3) { try { - var candidates = Directory.Exists(_appRoot) - ? Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly) - : []; + Console.WriteLine($"[DeploymentLocator] Starting cleanup with retention policy: keep at least {minVersionsToKeep} versions"); - var destroyedDirs = candidates - .Where(path => File.Exists(Path.Combine(path, ".destroy"))); + if (!Directory.Exists(_appRoot)) + { + return; + } - foreach (var dir in destroyedDirs) + var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly); + + // 过滤掉无效部署目录(排除partial),按版本排序 + var validDeployments = candidates + .Where(path => !File.Exists(Path.Combine(path, ".partial"))) + .Select(path => new + { + Path = path, + Version = ParseVersionFromDirectory(path), + IsDestroyed = File.Exists(Path.Combine(path, ".destroy")), + IsCurrent = File.Exists(Path.Combine(path, ".current")) + }) + .OrderByDescending(item => item.Version) + .ToList(); + + Console.WriteLine($"[DeploymentLocator] Found {validDeployments.Count} valid deployments"); + + // 确定要保留的版本 + var versionsToKeep = new HashSet(); + + // 1. 总是保留当前版本 + var currentVersion = validDeployments.FirstOrDefault(d => d.IsCurrent); + if (currentVersion != null) + { + versionsToKeep.Add(currentVersion.Path); + Console.WriteLine($"[DeploymentLocator] Keep current version: {currentVersion.Path}"); + } + + // 2. 保留最近的N个有效版本(不包括已标记destroy的) + var activeVersions = validDeployments + .Where(d => !d.IsDestroyed) + .Take(minVersionsToKeep) + .ToList(); + + foreach (var ver in activeVersions) + { + versionsToKeep.Add(ver.Path); + Console.WriteLine($"[DeploymentLocator] Keep recent version: {ver.Path}"); + } + + // 3. 保留有快照的版本(用于回滚) + var snapshotDir = Path.Combine(_appRoot, ".launcher", "snapshots"); + if (Directory.Exists(snapshotDir)) { try { - Directory.Delete(dir, recursive: true); + var snapshotFiles = Directory.GetFiles(snapshotDir, "*.json", SearchOption.TopDirectoryOnly); + foreach (var snapshotFile in snapshotFiles) + { + try + { + var json = File.ReadAllText(snapshotFile); + var snapshot = System.Text.Json.JsonSerializer.Deserialize(json); + if (snapshot != null && !string.IsNullOrEmpty(snapshot.SourceDirectory)) + { + if (Directory.Exists(snapshot.SourceDirectory)) + { + versionsToKeep.Add(snapshot.SourceDirectory); + Console.WriteLine($"[DeploymentLocator] Keep version for rollback: {snapshot.SourceDirectory}"); + } + } + } + catch + { + // 忽略快照解析错误 + } + } + } + catch + { + // 忽略快照目录访问错误 + } + } + + // 清理不需要的版本 + foreach (var deployment in validDeployments) + { + if (versionsToKeep.Contains(deployment.Path)) + { + // 保留此版本,如果之前标记了destroy则取消标记 + if (deployment.IsDestroyed) + { + try + { + File.Delete(Path.Combine(deployment.Path, ".destroy")); + Console.WriteLine($"[DeploymentLocator] Unmarked for deletion (kept): {deployment.Path}"); + } + catch + { + // 忽略取消标记失败 + } + } + continue; + } + + // 如果还没标记destroy的,先标记 + if (!deployment.IsDestroyed) + { + try + { + File.WriteAllText(Path.Combine(deployment.Path, ".destroy"), string.Empty); + Console.WriteLine($"[DeploymentLocator] Marked for deletion: {deployment.Path}"); + } + catch + { + // 忽略标记失败 + } + } + + // 尝试删除 + try + { + Directory.Delete(deployment.Path, recursive: true); + Console.WriteLine($"[DeploymentLocator] Deleted: {deployment.Path}"); } catch { // 忽略删除失败(可能文件被占用),下次启动再试 + Console.WriteLine($"[DeploymentLocator] Failed to delete (will retry later): {deployment.Path}"); } } } - catch + catch (Exception ex) { + Console.Error.WriteLine($"[DeploymentLocator] Cleanup failed: {ex.Message}"); // 忽略清理失败 } } + /// + /// 仅清理已标记为.destroy的部署(兼容旧方法) + /// + [Obsolete("Use CleanupOldDeployments instead")] + public void CleanupDestroyedDeployments() + { + CleanupOldDeployments(3); + } + public static Version ParseVersionFromDirectory(string path) { var text = ParseVersionTextFromDirectory(path); diff --git a/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs b/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs index 1b9b667..856dc88 100644 --- a/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs +++ b/LanMountainDesktop.Launcher/Services/FlexibleHostLocator.cs @@ -4,50 +4,67 @@ using System.Text.Json; namespace LanMountainDesktop.Launcher.Services; /// -/// 灵活的主程序定位器 -/// -internal sealed class FlexibleHostLocator -{ - private readonly HostDiscoveryOptions _options; - private readonly string _appRoot; - - public FlexibleHostLocator(string appRoot, HostDiscoveryOptions? options = null) - { - _appRoot = appRoot; - _options = options ?? new HostDiscoveryOptions(); - } - - /// - /// 解析主程序可执行文件路径 + /// 灵活的主程序定位器 /// - public string? ResolveHostExecutablePath() + internal sealed class FlexibleHostLocator { - var executable = GetExecutableName(); - var searchContext = new SearchContext - { - ExecutableName = executable, - AppRoot = _appRoot, - Options = _options - }; + private readonly HostDiscoveryOptions _options; + private readonly string _appRoot; + private readonly DeploymentLocator _deploymentLocator; - // ========== 第一阶段:标准路径查找(快速路径)========== - - // 1. 检查环境变量指定的路径(最高优先级 - 用于调试和特殊场景) - var envPath = GetPathFromEnvironment(); - if (!string.IsNullOrWhiteSpace(envPath)) + public FlexibleHostLocator(string appRoot, HostDiscoveryOptions? options = null) { - var validated = ValidateAndReturn(envPath, "environment variable"); - if (validated != null) return validated; + _appRoot = appRoot; + _options = options ?? new HostDiscoveryOptions(); + _deploymentLocator = new DeploymentLocator(appRoot); } - // 2. 搜索部署目录(app-*)- 生产环境标准路径 - var deploymentPath = SearchDeploymentDirectories(searchContext); - if (!string.IsNullOrWhiteSpace(deploymentPath)) + /// + /// 解析主程序可执行文件路径 + /// + public string? ResolveHostExecutablePath() { - return deploymentPath; - } + var executable = GetExecutableName(); + var searchContext = new SearchContext + { + ExecutableName = executable, + AppRoot = _appRoot, + Options = _options + }; - // 3. 检查 Launcher 同级目录(便携模式) + // ========== 第一阶段:标准路径查找(快速路径)========== + + // 1. 检查环境变量指定的路径(最高优先级 - 用于调试和特殊场景) + var envPath = GetPathFromEnvironment(); + if (!string.IsNullOrWhiteSpace(envPath)) + { + var validated = ValidateAndReturn(envPath, "environment variable"); + if (validated != null) return validated; + } + + // 2. 使用 DeploymentLocator(ClassIsland 风格的简洁查询 - 优先) + Console.WriteLine("[FlexibleHostLocator] Trying quick path: DeploymentLocator.FindCurrentDeploymentDirectory()"); + var deploymentDir = _deploymentLocator.FindCurrentDeploymentDirectory(); + if (!string.IsNullOrWhiteSpace(deploymentDir)) + { + var deploymentExePath = Path.Combine(deploymentDir, executable); + if (File.Exists(deploymentExePath)) + { + Console.WriteLine($"[FlexibleHostLocator] Quick path found: {deploymentExePath}"); + return deploymentExePath; + } + Console.WriteLine($"[FlexibleHostLocator] Quick path found dir but no exe: {deploymentExePath}"); + } + + // 3. 快速路径失败,尝试旧的 SearchDeploymentDirectories 作为 fallback + Console.WriteLine("[FlexibleHostLocator] Quick path failed, falling back to SearchDeploymentDirectories"); + var deploymentPath = SearchDeploymentDirectories(searchContext); + if (!string.IsNullOrWhiteSpace(deploymentPath)) + { + return deploymentPath; + } + + // 4. 检查 Launcher 同级目录(便携模式) var portablePath = SearchPortableLocation(searchContext); if (!string.IsNullOrWhiteSpace(portablePath)) { @@ -56,7 +73,7 @@ internal sealed class FlexibleHostLocator // ========== 第二阶段:灵活查找(标准路径找不到时)========== - // 4. 检查配置文件中的路径 - 用户自定义配置 + // 5. 检查配置文件中的路径 - 用户自定义配置 var configPath = GetPathFromConfigFile(); if (!string.IsNullOrWhiteSpace(configPath)) { @@ -71,7 +88,7 @@ internal sealed class FlexibleHostLocator return nearbyPath; } - // 6. 开发模式:检查保存的自定义路径 + // 7. 开发模式:检查保存的自定义路径 if (_options.PreferDevModeConfig && Views.ErrorWindow.CheckDevModeEnabled()) { var savedPath = Views.ErrorWindow.GetSavedCustomHostPath(); @@ -82,21 +99,21 @@ internal sealed class FlexibleHostLocator } } - // 7. 搜索标准开发路径 + // 8. 搜索标准开发路径 var devPath = SearchDevelopmentPaths(searchContext); if (!string.IsNullOrWhiteSpace(devPath)) { return devPath; } - // 8. 搜索额外的配置路径 + // 9. 搜索额外的配置路径 var additionalPath = SearchAdditionalPaths(searchContext); if (!string.IsNullOrWhiteSpace(additionalPath)) { return additionalPath; } - // 9. 递归搜索(如果启用) + // 10. 递归搜索(如果启用) if (_options.RecursiveSearch) { var recursivePath = SearchRecursively(searchContext); diff --git a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs index bb1e524..cd0ef15 100644 --- a/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs +++ b/LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs @@ -38,8 +38,8 @@ internal sealed class LauncherFlowCoordinator { try { - // 清理待删除的旧版本 - _deploymentLocator.CleanupDestroyedDeployments(); + // 清理旧版本,保留至少3个版本 + _deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3); // 使用传入的 Splash 窗口或创建新的 var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() => diff --git a/LanMountainDesktop.Launcher/Services/Logger.cs b/LanMountainDesktop.Launcher/Services/Logger.cs new file mode 100644 index 0000000..5d60c39 --- /dev/null +++ b/LanMountainDesktop.Launcher/Services/Logger.cs @@ -0,0 +1,138 @@ +using System.Text; + +namespace LanMountainDesktop.Launcher.Services; + +/// +/// 简单的日志记录器 - 同时输出到控制台和文件 +/// +internal static class Logger +{ + private static readonly object _lock = new(); + private static string? _logFilePath; + private static bool _initialized; + + /// + /// 初始化日志记录器 + /// + public static void Initialize() + { + if (_initialized) + { + return; + } + + try + { + var logDir = GetLogDirectory(); + if (!string.IsNullOrEmpty(logDir)) + { + Directory.CreateDirectory(logDir); + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + _logFilePath = Path.Combine(logDir, $"launcher_{timestamp}.log"); + Console.WriteLine($"[Logger] Log file initialized: {_logFilePath}"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[Logger] Failed to initialize log file: {ex.Message}"); + } + + _initialized = true; + } + + /// + /// 获取日志文件路径 + /// + public static string? GetLogFilePath() + { + return _logFilePath; + } + + /// + /// 获取日志目录 + /// + private static string? GetLogDirectory() + { + try + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (!string.IsNullOrEmpty(appData)) + { + return Path.Combine(appData, "LanMountainDesktop", ".launcher", "logs"); + } + } + catch + { + } + + try + { + var launcherDir = AppContext.BaseDirectory; + return Path.Combine(launcherDir, ".launcher", "logs"); + } + catch + { + } + + return null; + } + + /// + /// 记录信息日志 + /// + public static void Info(string message) + { + WriteLog("INFO", message); + } + + /// + /// 记录警告日志 + /// + public static void Warn(string message) + { + WriteLog("WARN", message); + } + + /// + /// 记录错误日志 + /// + public static void Error(string message) + { + WriteLog("ERROR", message); + } + + /// + /// 记录错误日志(带异常) + /// + public static void Error(string message, Exception exception) + { + WriteLog("ERROR", $"{message}\n{exception}"); + } + + /// + /// 写入日志 + /// + private static void WriteLog(string level, string message) + { + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var logLine = $"[{timestamp}] [{level}] {message}"; + + Console.WriteLine(logLine); + + if (string.IsNullOrEmpty(_logFilePath)) + { + return; + } + + try + { + lock (_lock) + { + File.AppendAllText(_logFilePath, logLine + Environment.NewLine, Encoding.UTF8); + } + } + catch + { + } + } +} diff --git a/LanMountainDesktop.Launcher/Services/OobeStateService.cs b/LanMountainDesktop.Launcher/Services/OobeStateService.cs index 4ef3759..ba712c4 100644 --- a/LanMountainDesktop.Launcher/Services/OobeStateService.cs +++ b/LanMountainDesktop.Launcher/Services/OobeStateService.cs @@ -6,29 +6,99 @@ internal sealed class OobeStateService public OobeStateService(string appRoot) { - // 将 OOBE 状态文件存储在用户可写的 LocalApplicationData 目录中, - // 而不是安装目录(Program Files 下普通用户没有写入权限)。 - var appDataDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "LanMountainDesktop"); - var stateDir = Path.Combine(appDataDir, ".launcher", "state"); - Directory.CreateDirectory(stateDir); + // 优先使用 LocalApplicationData(用户目录,普通用户一定有权限) + string? stateDir = null; + Exception? lastException = null; + + // 策略1: LocalApplicationData(首选,用户目录,普通用户一定有写权限) + try + { + var appDataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "LanMountainDesktop"); + stateDir = Path.Combine(appDataDir, ".launcher", "state"); + Directory.CreateDirectory(stateDir); + Console.WriteLine($"[OobeStateService] Using LocalApplicationData: {stateDir}"); + } + catch (Exception ex) + { + lastException = ex; + Console.Error.WriteLine($"[OobeStateService] LocalApplicationData failed: {ex.Message}"); + stateDir = null; + } + + // 策略2: 如果LocalApplicationData不行,使用用户的临时目录 + if (stateDir == null) + { + try + { + var tempDir = Path.Combine(Path.GetTempPath(), "LanMountainDesktop", ".launcher", "state"); + Directory.CreateDirectory(tempDir); + stateDir = tempDir; + Console.WriteLine($"[OobeStateService] Using TempPath: {stateDir}"); + } + catch (Exception ex) + { + lastException = ex; + Console.Error.WriteLine($"[OobeStateService] TempPath failed: {ex.Message}"); + stateDir = null; + } + } + + // 策略3: 最后的兜底:使用当前用户的应用程序数据目录(和Launcher同目录 + if (stateDir == null) + { + try + { + var launcherDir = AppContext.BaseDirectory; + stateDir = Path.Combine(launcherDir, ".launcher", "state"); + Directory.CreateDirectory(stateDir); + Console.WriteLine($"[OobeStateService] Using Launcher directory: {stateDir}"); + } + catch (Exception ex) + { + lastException = ex; + Console.Error.WriteLine($"[OobeStateService] All strategies failed! Last error: {ex.Message}"); + // 如果所有策略都失败,抛出异常让上层处理 + throw new InvalidOperationException("无法创建 OOBE 状态存储目录失败", lastException); + } + } + _markerPath = Path.Combine(stateDir, "first_run_completed"); + Console.WriteLine($"[OobeStateService] Initialized successfully, marker path: {_markerPath}"); } public bool IsFirstRun() { - return !File.Exists(_markerPath); + try + { + return !File.Exists(_markerPath); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[OobeStateService] Failed to check first run: {ex.Message}"); + // 如果无法检查,默认视为首次运行,确保OOBE能显示 + return true; + } } public void MarkCompleted() { - var dir = Path.GetDirectoryName(_markerPath); - if (!string.IsNullOrWhiteSpace(dir)) + try { - Directory.CreateDirectory(dir); - } + var dir = Path.GetDirectoryName(_markerPath); + if (!string.IsNullOrWhiteSpace(dir)) + { + Directory.CreateDirectory(dir); + } - File.WriteAllText(_markerPath, DateTimeOffset.UtcNow.ToString("O")); + File.WriteAllText(_markerPath, DateTimeOffset.UtcNow.ToString("O")); + Console.WriteLine("[OobeStateService] Marked first run as completed"); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[OobeStateService] Failed to mark completed: {ex.Message}"); + // 如果无法写入也没关系,下次启动还会显示OOBE + } } } diff --git a/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs b/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs index 667fb98..4f2f917 100644 --- a/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs +++ b/LanMountainDesktop.Launcher/Services/UpdateEngineService.cs @@ -217,6 +217,7 @@ internal sealed class UpdateEngineService snapshot.Status = "applied"; SaveSnapshot(snapshotPath, snapshot); CleanupIncomingArtifacts(); + // 清理旧版本,但保留最近3个版本以支持回滚 CleanupDestroyedDeployments(); return new LauncherResult diff --git a/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml b/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml index 8d29ed8..3e27087 100644 --- a/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml +++ b/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml @@ -76,21 +76,30 @@ - - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml.cs b/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml.cs index a7dcc8e..1d1864e 100644 --- a/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml.cs +++ b/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml.cs @@ -12,6 +12,22 @@ public partial class UpdateWindow : Window public UpdateWindow() { AvaloniaXamlLoader.Load(this); + InitializeEventHandlers(); + } + + /// + /// 初始化事件处理程序 + /// + private void InitializeEventHandlers() + { + var minimizeButton = this.FindControl