# LanMountainDesktop Launcher 全面改进计划 ## 概述 本计划旨在将 LanMountainDesktop 的 Launcher 改进为符合原子化架构的独立启动器,参考 ClassIsland 的极简设计,同时保留阑山桌面的特色功能。 ## 目标 1. **P0 (必须完成)**: 重写 Launcher 为极简模式,移除与主程序的耦合 2. **P1 (应该完成)**: 将 OOBE、Splash、更新、插件管理迁移到主程序 3. **P2 (推荐完成)**: 实现 Launcher 自更新机制 4. **P3 (可选优化)**: 性能优化和代码清理 5. **P4 (长期规划)**: 增强功能和可扩展性 ## 当前问题 1. Launcher 是 Avalonia 应用,启动慢、内存占用高 2. Launcher 引用了 PluginSdk,与主程序有耦合 3. 主程序引用了 Launcher,构建关系复杂 4. Launcher 职责过多(OOBE + Splash + 更新 + 插件 + 启动) 5. 缺少 Launcher 自更新机制 6. GitHub Actions 工作流需要适配新的目录结构 ## 改进后架构 ``` 安装根目录/ ├── LanMountainDesktop.exe ← 启动器(唯一入口,极简,~100行代码) ├── app-1.0.0/ ← 版本目录 │ ├── .current ← 当前版本标记 │ ├── LanMountainDesktop.exe ← 主程序 │ └── ... (所有依赖) └── .launcher/ ← 启动器数据(可选) └── snapshots/ ← 版本快照 ``` ## 详细实施步骤 ### P0: 基础架构重构 #### 1. 重写 Launcher 为极简模式 **文件**: `LanMountainDesktop.Launcher/Program.cs` **目标**: - 代码量控制在 100 行以内 - 零外部依赖(不使用 Avalonia) - 只负责:版本选择、启动主程序、清理旧版本 **完整实现代码**: ```csharp // LanMountainDesktop.Launcher/Program.cs using System.Diagnostics; using System.Runtime.InteropServices; namespace LanMountainDesktop.Launcher; internal static class Program { private const string HostExecutableName = "LanMountainDesktop.exe"; private const string HostExecutableNameLinux = "LanMountainDesktop"; [STAThread] private static int Main(string[] args) { var rootDir = GetRootDirectory(); // 1. 查找最佳版本 var installation = FindBestVersion(rootDir); if (installation == null) { ShowError("找不到有效的 LanMountainDesktop 版本,请重新安装。"); return 1; } // 2. 清理旧版本(异步,不阻塞) _ = Task.Run(() => CleanupOldVersions(rootDir)); // 3. 启动主程序 return LaunchHost(installation, args); } private static string GetRootDirectory() { return Path.GetFullPath( Path.GetDirectoryName(Environment.ProcessPath) ?? ""); } private static string? FindBestVersion(string rootDir) { var exeName = OperatingSystem.IsWindows() ? HostExecutableName : HostExecutableNameLinux; return Directory.GetDirectories(rootDir) .Where(x => IsValidVersionDirectory(x, exeName)) .OrderBy(x => File.Exists(Path.Combine(x, ".current")) ? 0 : 1) .ThenByDescending(x => ParseVersion(Path.GetFileName(x))) .FirstOrDefault(); } private static bool IsValidVersionDirectory(string path, string exeName) { var dirName = Path.GetFileName(path); return dirName.StartsWith("app-") && !File.Exists(Path.Combine(path, ".destroy")) && !File.Exists(Path.Combine(path, ".partial")) && File.Exists(Path.Combine(path, exeName)); } private static Version ParseVersion(string dirName) { // app-1.0.0 or app-1.0.0-123 var parts = dirName.Split('-'); if (parts.Length >= 2 && Version.TryParse(parts[1], out var v)) return v; return new Version(0, 0); } private static void CleanupOldVersions(string rootDir) { try { var oldVersions = Directory.GetDirectories(rootDir) .Where(x => File.Exists(Path.Combine(x, ".destroy"))); foreach (var dir in oldVersions) { try { Directory.Delete(dir, recursive: true); } catch { } } } catch { /* 忽略清理失败 */ } } private static int LaunchHost(string installation, string[] args) { var exeName = OperatingSystem.IsWindows() ? HostExecutableName : HostExecutableNameLinux; var exePath = Path.Combine(installation, exeName); // Linux/macOS: 确保可执行权限 if (!OperatingSystem.IsWindows()) { EnsureExecutable(exePath); } var startInfo = new ProcessStartInfo { FileName = exePath, WorkingDirectory = Path.GetDirectoryName(installation), UseShellExecute = true }; foreach (var arg in args) startInfo.ArgumentList.Add(arg); // 传递环境变量 startInfo.EnvironmentVariables["LMD_PACKAGE_ROOT"] = Path.GetDirectoryName(installation); startInfo.EnvironmentVariables["LMD_VERSION"] = Path.GetFileName(installation).Replace("app-", ""); try { Process.Start(startInfo); return 0; } catch (Exception ex) { ShowError($"启动失败: {ex.Message}"); return 1; } } private static void EnsureExecutable(string path) { try { Process.Start(new ProcessStartInfo { FileName = "chmod", Arguments = $"+x \"{path}\"", CreateNoWindow = true })?.WaitForExit(); } catch { } } private static void ShowError(string message) { if (OperatingSystem.IsWindows()) { // Win32 MessageBox try { MessageBox(IntPtr.Zero, message, "LanMountainDesktop", 0x10); } catch { Console.Error.WriteLine(message); } } else { Console.Error.WriteLine(message); } } [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type); } ``` #### 2. 修改 Launcher 项目文件 **文件**: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj` **完整内容**: ```xml WinExe net10.0 enable enable 1.0.0 Assets\logo_nightly.ico PreserveNewest ``` #### 3. 移除主程序对 Launcher 的引用 **文件**: `LanMountainDesktop/LanMountainDesktop.csproj` **修改**: 删除以下行 ```xml ``` #### 4. 修改主程序支持新架构 **文件**: `LanMountainDesktop/Program.cs` **修改**: 添加环境变量读取 ```csharp // 在 Program.cs 中添加 internal static class LaunchContext { public static string? PackageRoot => Environment.GetEnvironmentVariable("LMD_PACKAGE_ROOT"); public static string? Version => Environment.GetEnvironmentVariable("LMD_VERSION"); public static bool IsLaunchedByLauncher => !string.IsNullOrEmpty(PackageRoot); } ``` --- ### P1: 功能迁移 #### 5. 将 OOBE 迁移到主程序 **新建文件**: `LanMountainDesktop/Services/Oobe/OobeService.cs` ```csharp using LanMountainDesktop.Models; using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Services.Oobe; public class OobeService { private readonly string _oobeStatePath; public OobeService() { var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); _oobeStatePath = Path.Combine(appData, "LanMountainDesktop", ".oobe_completed"); } public bool IsFirstRun() { return !File.Exists(_oobeStatePath); } public void MarkCompleted() { var dir = Path.GetDirectoryName(_oobeStatePath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(_oobeStatePath, DateTime.UtcNow.ToString("O")); } } ``` **新建文件**: `LanMountainDesktop/Views/Oobe/OobeWindow.axaml` ```xml