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 @@
-
-
-
+ HorizontalAlignment="Left"/>
+
+
+
+
+
diff --git a/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml.cs b/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml.cs
index e65134c..ead47d2 100644
--- a/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml.cs
+++ b/LanMountainDesktop.Launcher/Views/ErrorWindow.axaml.cs
@@ -2,6 +2,8 @@ using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
+using LanMountainDesktop.Launcher.Services;
+using System.Diagnostics;
namespace LanMountainDesktop.Launcher.Views;
@@ -66,6 +68,7 @@ public partial class ErrorWindow : Window
// 按钮事件
var retryButton = this.FindControl("RetryButton");
var exitButton = this.FindControl("ExitButton");
+ var openLogButton = this.FindControl("OpenLogButton");
if (retryButton is not null)
{
@@ -86,6 +89,16 @@ public partial class ErrorWindow : Window
{
Console.Error.WriteLine("[ErrorWindow] Failed to find ExitButton!");
}
+
+ if (openLogButton is not null)
+ {
+ openLogButton.Click += OnOpenLogClick;
+ Console.WriteLine("[ErrorWindow] OpenLogButton event bound");
+ }
+ else
+ {
+ Console.Error.WriteLine("[ErrorWindow] Failed to find OpenLogButton!");
+ }
Console.WriteLine("[ErrorWindow] Components initialization completed");
}
@@ -210,6 +223,61 @@ public partial class ErrorWindow : Window
}
}
+ ///
+ /// 获取配置存储的基础目录
+ ///
+ private static string GetConfigBaseDirectory()
+ {
+ try
+ {
+ // 优先使用 LocalApplicationData(用户状态)
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (!string.IsNullOrEmpty(appData))
+ {
+ var configDir = Path.Combine(appData, "LanMountainDesktop", ".launcher");
+ return configDir;
+ }
+ }
+ catch
+ {
+ // LocalApplicationData 不可用,回退到 Launcher 所在目录
+ }
+
+ // 回退方案:使用 Launcher 所在目录
+ try
+ {
+ var launcherDir = AppContext.BaseDirectory;
+ var configDir = Path.Combine(launcherDir, ".launcher");
+ return configDir;
+ }
+ catch
+ {
+ // 最后的兜底:使用当前目录
+ return Path.Combine(Directory.GetCurrentDirectory(), ".launcher");
+ }
+ }
+
+ ///
+ /// 确保配置目录存在
+ ///
+ private static bool EnsureConfigDirectory(string dirPath)
+ {
+ try
+ {
+ if (!Directory.Exists(dirPath))
+ {
+ Directory.CreateDirectory(dirPath);
+ Console.WriteLine($"[ErrorWindow] Created config directory: {dirPath}");
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[ErrorWindow] Failed to create config directory: {ex.Message}");
+ return false;
+ }
+ }
+
///
/// 保存开发模式状态(内部方法)
///
@@ -217,17 +285,20 @@ public partial class ErrorWindow : Window
{
try
{
- var devModeFile = GetDevModeFilePath();
- var dir = Path.GetDirectoryName(devModeFile);
- if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ var configDir = GetConfigBaseDirectory();
+ if (!EnsureConfigDirectory(configDir))
{
- Directory.CreateDirectory(dir);
+ Console.Error.WriteLine("[ErrorWindow] Cannot save dev mode: config directory unavailable");
+ return;
}
+
+ var devModeFile = Path.Combine(configDir, "devmode.config");
File.WriteAllText(devModeFile, enabled ? "1" : "0");
+ Console.WriteLine($"[ErrorWindow] Dev mode state saved: {enabled}");
}
catch (Exception ex)
{
- Console.Error.WriteLine($"Failed to save dev mode state: {ex.Message}");
+ Console.Error.WriteLine($"[ErrorWindow] Failed to save dev mode state: {ex.Message}");
}
}
@@ -238,29 +309,24 @@ public partial class ErrorWindow : Window
{
try
{
- var devModeFile = GetDevModeFilePath();
+ var configDir = GetConfigBaseDirectory();
+ var devModeFile = Path.Combine(configDir, "devmode.config");
+
if (File.Exists(devModeFile))
{
var content = File.ReadAllText(devModeFile).Trim();
- return content == "1";
+ var enabled = content == "1";
+ Console.WriteLine($"[ErrorWindow] Dev mode state loaded: {enabled}");
+ return enabled;
}
}
catch (Exception ex)
{
- Console.Error.WriteLine($"Failed to load dev mode state: {ex.Message}");
+ Console.Error.WriteLine($"[ErrorWindow] Failed to load dev mode state: {ex.Message}");
}
return false;
}
- ///
- /// 获取开发模式状态文件路径
- ///
- private static string GetDevModeFilePath()
- {
- var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- return Path.Combine(appData, "LanMountainDesktop", ".launcher", "devmode.config");
- }
-
///
/// 保存自定义主程序路径(内部方法)
///
@@ -268,17 +334,20 @@ public partial class ErrorWindow : Window
{
try
{
- var hostPathFile = GetCustomHostPathFilePath();
- var dir = Path.GetDirectoryName(hostPathFile);
- if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ var configDir = GetConfigBaseDirectory();
+ if (!EnsureConfigDirectory(configDir))
{
- Directory.CreateDirectory(dir);
+ Console.Error.WriteLine("[ErrorWindow] Cannot save custom path: config directory unavailable");
+ return;
}
+
+ var hostPathFile = Path.Combine(configDir, "custom-host-path.config");
File.WriteAllText(hostPathFile, path ?? string.Empty);
+ Console.WriteLine($"[ErrorWindow] Custom host path saved: {path}");
}
catch (Exception ex)
{
- Console.Error.WriteLine($"Failed to save custom host path: {ex.Message}");
+ Console.Error.WriteLine($"[ErrorWindow] Failed to save custom host path: {ex.Message}");
}
}
@@ -289,43 +358,42 @@ public partial class ErrorWindow : Window
{
try
{
- var hostPathFile = GetCustomHostPathFilePath();
+ var configDir = GetConfigBaseDirectory();
+ var hostPathFile = Path.Combine(configDir, "custom-host-path.config");
+
if (File.Exists(hostPathFile))
{
var content = File.ReadAllText(hostPathFile).Trim();
// 验证路径是否仍然有效
if (!string.IsNullOrEmpty(content) && File.Exists(content))
{
+ Console.WriteLine($"[ErrorWindow] Custom host path loaded: {content}");
return content;
}
+
// 路径已失效,清理配置文件
- try
+ if (!string.IsNullOrEmpty(content))
{
- File.Delete(hostPathFile);
- Console.WriteLine("Custom host path is no longer valid, cleared saved path.");
- }
- catch (Exception clearEx)
- {
- Console.Error.WriteLine($"Failed to clear invalid host path: {clearEx.Message}");
+ Console.WriteLine($"[ErrorWindow] Custom host path is no longer valid: {content}");
+ try
+ {
+ File.Delete(hostPathFile);
+ Console.WriteLine("[ErrorWindow] Cleared invalid custom host path");
+ }
+ catch (Exception clearEx)
+ {
+ Console.Error.WriteLine($"[ErrorWindow] Failed to clear invalid host path: {clearEx.Message}");
+ }
}
}
}
catch (Exception ex)
{
- Console.Error.WriteLine($"Failed to load custom host path: {ex.Message}");
+ Console.Error.WriteLine($"[ErrorWindow] Failed to load custom host path: {ex.Message}");
}
return null;
}
- ///
- /// 获取自定义主程序路径文件路径
- ///
- private static string GetCustomHostPathFilePath()
- {
- var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- return Path.Combine(appData, "LanMountainDesktop", ".launcher", "custom-host-path.config");
- }
-
///
/// 检查是否启用了开发模式(静态方法,启动时调用)
///
@@ -351,6 +419,110 @@ public partial class ErrorWindow : Window
{
_completionSource.TrySetResult(ErrorWindowResult.Exit);
}
+
+ ///
+ /// 打开日志文件
+ ///
+ private async void OnOpenLogClick(object? sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var logFilePath = Logger.GetLogFilePath();
+
+ if (string.IsNullOrEmpty(logFilePath) || !File.Exists(logFilePath))
+ {
+ // 如果没有日志文件,打开日志目录
+ var logDir = Path.GetDirectoryName(logFilePath);
+ if (!string.IsNullOrEmpty(logDir) && Directory.Exists(logDir))
+ {
+ OpenFolder(logDir);
+ }
+ else
+ {
+ // 尝试打开配置目录
+ var configDir = GetConfigBaseDirectory();
+ if (Directory.Exists(configDir))
+ {
+ OpenFolder(configDir);
+ }
+ else
+ {
+ Console.WriteLine("[ErrorWindow] No log file or directory available");
+ }
+ }
+ return;
+ }
+
+ Console.WriteLine($"[ErrorWindow] Opening log file: {logFilePath}");
+ OpenFile(logFilePath);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[ErrorWindow] Failed to open log: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 打开文件
+ ///
+ private static void OpenFile(string filePath)
+ {
+ try
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = "explorer.exe",
+ Arguments = $"\"{filePath}\"",
+ UseShellExecute = true
+ });
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ Process.Start("open", filePath);
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ Process.Start("xdg-open", filePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[ErrorWindow] Failed to open file: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 打开文件夹
+ ///
+ private static void OpenFolder(string folderPath)
+ {
+ try
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = "explorer.exe",
+ Arguments = $"\"{folderPath}\"",
+ UseShellExecute = true
+ });
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ Process.Start("open", folderPath);
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ Process.Start("xdg-open", folderPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[ErrorWindow] Failed to open folder: {ex.Message}");
+ }
+ }
}
///
diff --git a/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml b/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml
index 9d8d2e8..f2658da 100644
--- a/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml
+++ b/LanMountainDesktop.Launcher/Views/UpdateWindow.axaml
@@ -4,13 +4,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
mc:Ignorable="d"
- d:DesignWidth="400"
- d:DesignHeight="220"
+ d:DesignWidth="480"
+ d:DesignHeight="320"
x:Class="LanMountainDesktop.Launcher.Views.UpdateWindow"
x:DataType="views:UpdateWindow"
Title="阑山桌面 - 更新"
- Width="400"
- Height="220"
+ Width="480"
+ Height="320"
CanResize="False"
WindowStartupLocation="CenterScreen"
SystemDecorations="None"
@@ -21,48 +21,88 @@
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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("MinimizeButton");
+ if (minimizeButton != null)
+ {
+ minimizeButton.Click += (s, e) =>
+ {
+ this.WindowState = WindowState.Minimized;
+ };
+ }
}
///
@@ -23,11 +39,11 @@ public partial class UpdateWindow : Window
{
var statusText = this.FindControl("StatusText");
var progressIndicator = this.FindControl("ProgressIndicator");
- var detailText = this.FindControl("DetailText");
+ var percentText = this.FindControl("PercentText");
- if (statusText is null || progressIndicator is null || detailText is null)
+ if (statusText is null || progressIndicator is null || percentText is null)
{
- Console.Error.WriteLine($"[UpdateWindow] Controls not found in Report: StatusText={statusText != null}, ProgressIndicator={progressIndicator != null}, DetailText={detailText != null}");
+ Console.Error.WriteLine($"[UpdateWindow] Controls not found in Report: StatusText={statusText != null}, ProgressIndicator={progressIndicator != null}, PercentText={percentText != null}");
return;
}
@@ -37,23 +53,13 @@ public partial class UpdateWindow : Window
{
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = progressPercent;
+ percentText.Text = $"{progressPercent}%";
}
else
{
progressIndicator.IsIndeterminate = true;
+ percentText.Text = "";
}
-
- // 根据阶段显示不同的底部提示
- detailText.Text = stage.ToLowerInvariant() switch
- {
- "verify" => "正在验证更新完整性...",
- "extract" => "正在解压更新包...",
- "apply" => "正在应用更新文件...",
- "plugins" => "正在升级插件...",
- "cleanup" => "正在清理...",
- "done" => "",
- _ => ""
- };
});
}
@@ -66,10 +72,10 @@ public partial class UpdateWindow : Window
{
var statusText = this.FindControl("StatusText");
var progressIndicator = this.FindControl("ProgressIndicator");
- var detailText = this.FindControl("DetailText");
+ var percentText = this.FindControl("PercentText");
var titleText = this.FindControl("TitleText");
- if (statusText is null || progressIndicator is null || detailText is null || titleText is null)
+ if (statusText is null || progressIndicator is null || percentText is null || titleText is null)
{
Console.Error.WriteLine($"[UpdateWindow] Controls not found in ReportComplete");
return;
@@ -77,7 +83,7 @@ public partial class UpdateWindow : Window
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = 100;
- detailText.Text = "";
+ percentText.Text = "100%";
if (success)
{