feat.尝试弄了AOT的启动器。

This commit is contained in:
lincube
2026-04-17 15:16:01 +08:00
parent 59c4824425
commit 81ee19f360
49 changed files with 4175 additions and 468 deletions

View File

@@ -1,8 +1,11 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LanMountainDesktop.Launcher.Models;
using LanMountainDesktop.Launcher.Services;
using LanMountainDesktop.Launcher.Views;
namespace LanMountainDesktop.Launcher;
@@ -18,33 +21,367 @@ public partial class App : Application
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var context = LauncherRuntimeContext.Current;
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());
_ = RunCoordinatorAsync(desktop, coordinator);
// 调试模式:显示开发调试窗口
if (context.IsDebugMode)
{
var devDebugWindow = new DevDebugWindow();
devDebugWindow.Show();
// 调试模式下不自动启动正常流程,由开发者通过调试窗口控制
base.OnFrameworkInitializationCompleted();
return;
}
// 处理各界面的预览命令
if (HandlePreviewCommand(context, desktop))
{
base.OnFrameworkInitializationCompleted();
return;
}
// apply-update 模式:显示 UpdateWindow执行增量更新 + 插件升级
if (string.Equals(context.Command, "apply-update", StringComparison.OrdinalIgnoreCase))
{
// 先显示窗口,再启动后台任务
var updateWindow = new UpdateWindow();
updateWindow.Show();
_ = RunApplyUpdateWithWindowAsync(desktop, context, updateWindow);
}
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);
}
}
base.OnFrameworkInitializationCompleted();
}
private static async Task RunCoordinatorAsync(
IClassicDesktopStyleApplicationLifetime desktop,
LauncherFlowCoordinator coordinator)
/// <summary>
/// 处理界面预览命令
/// </summary>
private bool HandlePreviewCommand(CommandContext context, IClassicDesktopStyleApplicationLifetime desktop)
{
var result = await coordinator.RunAsync().ConfigureAwait(false);
var command = context.Command.ToLowerInvariant();
switch (command)
{
case "preview-splash":
Console.WriteLine("[Launcher] Preview mode: SplashWindow");
var splashWindow = new SplashWindow();
splashWindow.SetDebugMode(true);
splashWindow.Show();
_ = SimulateSplashPreviewAsync(desktop, splashWindow);
return true;
case "preview-error":
Console.WriteLine("[Launcher] Preview mode: ErrorWindow");
var errorWindow = new ErrorWindow();
errorWindow.SetErrorMessage("[预览模式] 这是一个错误页面预览。\n\n用于查看错误页面的样式和布局。");
errorWindow.Show();
_ = WaitForWindowCloseAsync(desktop, errorWindow);
return true;
case "preview-update":
Console.WriteLine("[Launcher] Preview mode: UpdateWindow");
var updateWindow = new UpdateWindow();
updateWindow.SetDebugMode(true);
updateWindow.Show();
_ = SimulateUpdatePreviewAsync(desktop, updateWindow);
return true;
case "preview-oobe":
Console.WriteLine("[Launcher] Preview mode: OobeWindow");
var oobeWindow = new OobeWindow();
oobeWindow.Show();
_ = SimulateOobePreviewAsync(desktop, oobeWindow);
return true;
case "preview-debug":
Console.WriteLine("[Launcher] Preview mode: DevDebugWindow");
var devDebugWindow = new DevDebugWindow();
devDebugWindow.Show();
return true;
default:
return false;
}
}
/// <summary>
/// 模拟 Splash 窗口预览
/// </summary>
private async Task SimulateSplashPreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, SplashWindow window)
{
var stages = new[] { "initializing", "update", "plugins", "launch", "ready" };
var messages = new[] { "初始化...", "检查更新...", "检查插件...", "正在启动...", "就绪" };
var reporter = (ISplashStageReporter)window;
for (int i = 0; i < stages.Length; i++)
{
reporter.Report(stages[i], messages[i]);
await Task.Delay(800);
}
// 等待5秒后自动关闭
await Task.Delay(5000);
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
}
/// <summary>
/// 模拟 Update 窗口预览
/// </summary>
private async Task SimulateUpdatePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, UpdateWindow window)
{
var stages = new[] { "verify", "extract", "apply", "plugins", "cleanup" };
for (int i = 0; i < stages.Length; i++)
{
window.Report(stages[i], $"正在{GetStageName(stages[i])}...", (i + 1) * 20);
await Task.Delay(600);
}
window.ReportComplete(true, null);
// 等待3秒后自动关闭
await Task.Delay(3000);
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
string GetStageName(string stage) => stage switch
{
"verify" => "验证",
"extract" => "解压",
"apply" => "应用",
"plugins" => "升级插件",
"cleanup" => "清理",
_ => stage
};
}
/// <summary>
/// 模拟 OOBE 窗口预览
/// </summary>
private async Task SimulateOobePreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, OobeWindow window)
{
try
{
// 等待用户点击开始按钮
await window.WaitForEnterAsync();
Console.WriteLine("[Launcher] OOBE preview completed by user");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[Launcher] OOBE preview error: {ex.Message}");
}
// 用户点击后关闭应用程序
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
}
/// <summary>
/// 等待窗口关闭
/// </summary>
private async Task WaitForWindowCloseAsync(IClassicDesktopStyleApplicationLifetime desktop, Window window)
{
var tcs = new TaskCompletionSource();
window.Closed += (s, e) => tcs.TrySetResult();
await tcs.Task;
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(0));
}
private static async Task RunCoordinatorWithSplashAsync(
IClassicDesktopStyleApplicationLifetime desktop,
LauncherFlowCoordinator coordinator,
SplashWindow splashWindow)
{
LauncherResult result;
ErrorWindow? errorWindow = null;
try
{
result = await coordinator.RunAsync(splashWindow).ConfigureAwait(false);
}
catch (Exception ex)
{
// 捕获异常并显示错误窗口
result = new LauncherResult
{
Success = false,
Stage = "launch",
Code = "exception",
Message = $"启动器发生错误: {ex.Message}",
ErrorMessage = ex.ToString()
};
Console.Error.WriteLine($"[Launcher] Exception caught: {ex}");
// 在 UI 线程显示错误窗口 - 使用更健壮的方式
try
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
try
{
// 安全关闭 Splash 窗口
if (splashWindow.IsVisible && splashWindow.IsLoaded)
{
splashWindow.Close();
}
}
catch (Exception closeEx)
{
Console.Error.WriteLine($"[Launcher] Error closing splash window: {closeEx.Message}");
}
// 创建并显示错误窗口
try
{
errorWindow = new ErrorWindow();
errorWindow.SetErrorMessage($"启动器发生错误:\n{ex.Message}\n\n请检查应用安装是否完整或尝试重新安装。");
errorWindow.Show();
Console.WriteLine("[Launcher] ErrorWindow shown successfully");
}
catch (Exception windowEx)
{
Console.Error.WriteLine($"[Launcher] Failed to show ErrorWindow: {windowEx.Message}");
}
});
// 如果错误窗口成功显示,等待它关闭
if (errorWindow != null)
{
try
{
// 等待用户选择或窗口关闭
var errorResult = await errorWindow.WaitForChoiceAsync();
Console.WriteLine($"[Launcher] ErrorWindow result: {errorResult}");
}
catch (Exception waitEx)
{
Console.Error.WriteLine($"[Launcher] Error waiting for ErrorWindow: {waitEx.Message}");
// 如果等待失败至少给用户5秒时间看到错误信息
await Task.Delay(5000);
}
}
else
{
// 错误窗口未能显示等待5秒让用户看到控制台输出
await Task.Delay(5000);
}
}
catch (Exception uiEx)
{
// 最后的兜底:记录到控制台
Console.Error.WriteLine($"[Launcher] Critical error in UI thread: {uiEx.Message}");
await Task.Delay(3000);
}
}
await Commands.WriteResultIfNeededAsync(LauncherRuntimeContext.Current.GetOption("result"), result).ConfigureAwait(false);
Environment.ExitCode = result.Success ? 0 : 1;
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
}
/// <summary>
/// apply-update 模式:执行增量更新和插件升级,完成后自动退出
/// </summary>
private static async Task RunApplyUpdateWithWindowAsync(
IClassicDesktopStyleApplicationLifetime desktop,
CommandContext context,
UpdateWindow window)
{
var appRoot = Commands.ResolveAppRoot(context);
var deploymentLocator = new DeploymentLocator(appRoot);
var updateEngine = new UpdateEngineService(deploymentLocator);
var pluginInstaller = new PluginInstallerService();
var pluginUpgrades = new PluginUpgradeQueueService(pluginInstaller);
var success = true;
string? errorMessage = null;
try
{
// 1. 应用增量更新
await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", "正在验证更新...", 10));
var updateResult = await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
if (!updateResult.Success)
{
success = false;
errorMessage = updateResult.Message;
}
// 2. 应用待处理的插件升级
if (success)
{
await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", "正在升级插件...", 60));
var pluginsDir = context.GetOption("plugins-dir")
?? Path.Combine(appRoot, "plugins");
var queueResult = pluginUpgrades.ApplyPendingUpgrades(pluginsDir);
if (!queueResult.Success && queueResult.Code != "noop")
{
// 插件升级失败不阻断整体流程,仅记录到控制台
Console.Error.WriteLine($"Plugin upgrade had failures: {queueResult.Message}");
}
}
// 3. 清理旧版本
if (success)
{
await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "正在清理...", 90));
deploymentLocator.CleanupDestroyedDeployments();
}
}
catch (Exception ex)
{
success = false;
errorMessage = ex.Message;
}
// 显示完成状态,短暂停留后关闭
await Dispatcher.UIThread.InvokeAsync(() => window.ReportComplete(success, errorMessage));
if (success)
{
// 成功:停留 1.5 秒让用户看到"更新完成"
await Task.Delay(1500);
}
else
{
// 失败:停留 5 秒让用户看到错误信息
await Task.Delay(5000);
}
await Commands.WriteResultIfNeededAsync(context.GetOption("result"), new LauncherResult
{
Success = success,
Stage = "apply-update",
Code = success ? "ok" : "failed",
Message = success ? "Update applied successfully." : (errorMessage ?? "Unknown error")
}).ConfigureAwait(false);
Environment.ExitCode = success ? 0 : 1;
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(Environment.ExitCode), DispatcherPriority.Background);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -10,16 +10,30 @@ internal sealed class CommandContext
public IReadOnlyDictionary<string, string> Options { get; }
/// <summary>
/// 原始命令行参数,用于转发给主程序
/// </summary>
public IReadOnlyList<string> RawArgs { get; }
public bool IsLegacyPluginInstall =>
Options.ContainsKey("source") &&
Options.ContainsKey("plugins-dir") &&
Options.ContainsKey("result");
private CommandContext(string command, string subCommand, Dictionary<string, string> options)
/// <summary>
/// 是否处于调试模式(从 Rider/VS 等 IDE 启动)
/// 仅当明确指定 --debug 参数或调试器附加时才启用
/// </summary>
public bool IsDebugMode =>
Options.ContainsKey("debug") ||
System.Diagnostics.Debugger.IsAttached;
private CommandContext(string command, string subCommand, Dictionary<string, string> options, string[] rawArgs)
{
Command = command;
SubCommand = subCommand;
Options = options;
RawArgs = rawArgs;
}
public static CommandContext FromArgs(string[] args)
@@ -32,7 +46,7 @@ internal sealed class CommandContext
? args[1]
: string.Empty;
return new CommandContext(command, subCommand, options);
return new CommandContext(command, subCommand, options, args);
}
public string? GetOption(string key)

View File

@@ -0,0 +1,62 @@
<!-- AOT 发布配置文件 -->
<Project>
<PropertyGroup Condition="'$(PublishAot)' == 'true'">
<!-- 启用 Native AOT -->
<PublishAot>true</PublishAot>
<!-- 启用修剪以减小体积 -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<!-- 自包含(不依赖系统 .NET Runtime -->
<SelfContained>true</SelfContained>
<!-- 单文件发布 -->
<PublishSingleFile>true</PublishSingleFile>
<!-- 包含 native 库到单文件中 -->
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<!-- 压缩单文件 -->
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<!-- 优化大小 -->
<OptimizationPreference>Size</OptimizationPreference>
<!-- 禁用 ReadyToRunAOT 不需要) -->
<PublishReadyToRun>false</PublishReadyToRun>
<!-- 目标运行时 -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<!-- AOT 兼容性设置 -->
<PropertyGroup>
<!-- 允许不安全代码(某些 AOT 场景需要) -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- 启用编译时绑定Avalonia 需要) -->
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<!-- AOT 修剪配置 -->
<ItemGroup Condition="'$(PublishAot)' == 'true'">
<!-- 保留 Avalonia 必要的类型 -->
<TrimmerRootAssembly Include="Avalonia" />
<TrimmerRootAssembly Include="Avalonia.Desktop" />
<!-- 保留动态序列化类型 -->
<TrimmerRootAssembly Include="System.Text.Json" />
</ItemGroup>
<!-- AOT 兼容性:某些包可能需要特殊处理 -->
<PropertyGroup Condition="'$(PublishAot)' == 'true'">
<!-- 忽略某些警告 -->
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<!-- 允许 IL 警告 -->
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<!-- FluentAvaloniaUI 需要启用反射序列化AOT 兼容模式) -->
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,29 @@
<!-- 单文件发布配置文件(非 AOT但接近单文件体验 -->
<Project>
<PropertyGroup Condition="'$(PublishSingleFileMode)' == 'true'">
<!-- 自包含 -->
<SelfContained>true</SelfContained>
<!-- 单文件发布 -->
<PublishSingleFile>true</PublishSingleFile>
<!-- 包含 native 库到单文件中 -->
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<!-- 压缩单文件 -->
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<!-- ReadyToRun 预编译(提升启动速度) -->
<PublishReadyToRun>true</PublishReadyToRun>
<!-- 修剪以减小体积 -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<!-- 优化大小 -->
<OptimizationPreference>Size</OptimizationPreference>
<!-- 目标运行时 -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>

View File

@@ -1,4 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk" TreatAsLocalProperty="Version;PackageVersion;InformationalVersion;AssemblyVersion;FileVersion">
<!-- 导入 AOT 配置 -->
<Import Project="LanMountainDesktop.Launcher.AOT.props" Condition="Exists('LanMountainDesktop.Launcher.AOT.props')" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
@@ -7,6 +11,8 @@
<Version>1.0.0</Version>
<PackageVersion>$(Version)</PackageVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<!-- 应用程序图标 -->
<ApplicationIcon>Assets\logo.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
@@ -23,9 +29,12 @@
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
</ItemGroup>
<!-- Embed public-key.pem and copy to .launcher/update/ in output directory -->
<!-- 资源文件 -->
<ItemGroup>
<!-- 公钥文件 -->
<None Include="Assets\public-key.pem" CopyToOutputDirectory="PreserveNewest" />
<!-- Avalonia 资源文件 -->
<AvaloniaResource Include="Assets\logo.ico" />
</ItemGroup>
<Target Name="CopyPublicKeyToLauncherDir" AfterTargets="Build">

View File

@@ -1,14 +1,7 @@
using System.Diagnostics;
using Avalonia;
using LanMountainDesktop.Launcher.Models;
using LanMountainDesktop.Launcher.Services;
#if WINDOWS
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
#endif
namespace LanMountainDesktop.Launcher;
internal static class Program
@@ -25,6 +18,14 @@ internal static class Program
return await Commands.RunLegacyPluginInstallAsync(commandContext, installer).ConfigureAwait(false);
}
// apply-update 命令:启动 Avalonia GUI 显示更新进度窗口
if (string.Equals(commandContext.Command, "apply-update", StringComparison.OrdinalIgnoreCase))
{
LauncherRuntimeContext.Current = commandContext;
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
return Environment.ExitCode;
}
// 处理其他 CLI 命令 (update, plugin, rollback 等)
if (!string.Equals(commandContext.Command, "launch", StringComparison.OrdinalIgnoreCase))
{
@@ -37,150 +38,6 @@ internal static class Program
return Environment.ExitCode;
}
private static int LaunchMainApplication(string[] args)
{
// 获取可执行文件名
string executableName = OperatingSystem.IsWindows()
? "LanMountainDesktop.exe"
: "LanMountainDesktop";
// 获取安装根目录
var rootDir = Path.GetFullPath(Path.GetDirectoryName(Environment.ProcessPath) ?? "");
// 查找最佳版本
var installation = FindBestVersion(rootDir, executableName);
if (installation == null)
{
ShowError("找不到有效的 LanMountainDesktop 版本,可能是安装已损坏。\n请访问 https://github.com/ClassIsland/LanMountainDesktop 重新下载并安装。");
return 1;
}
var exePath = Path.Combine(installation, executableName);
// Linux/macOS: 自动添加可执行权限
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
try
{
var chmod = Process.Start(new ProcessStartInfo
{
FileName = "chmod",
Arguments = $"+x \"{exePath}\"",
CreateNoWindow = true
});
chmod?.WaitForExit();
}
catch (Exception ex)
{
Console.Error.WriteLine($"无法设置可执行权限: {ex.Message}");
}
}
// 清理待删除的旧版本
CleanupDestroyedVersions(rootDir);
// 启动主程序
var startInfo = new ProcessStartInfo
{
FileName = exePath,
WorkingDirectory = rootDir,
UseShellExecute = false
};
foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}
// 传递包根目录环境变量
startInfo.EnvironmentVariables["LanMountainDesktop_PackageRoot"] = rootDir;
try
{
Process.Start(startInfo);
return 0;
}
catch (Exception ex)
{
ShowError($"启动主程序失败: {ex.Message}");
return 1;
}
}
private static string? FindBestVersion(string rootDir, string executableName)
{
return Directory.GetDirectories(rootDir)
.Where(x =>
{
var dirName = Path.GetFileName(x);
return dirName.StartsWith("app-") &&
!File.Exists(Path.Combine(x, ".destroy")) &&
!File.Exists(Path.Combine(x, ".partial")) &&
File.Exists(Path.Combine(x, executableName));
})
.OrderBy(x => File.Exists(Path.Combine(x, ".current")) ? 0 : 1) // .current 优先
.ThenByDescending(x => ParseVersion(Path.GetFileName(x))) // 版本号降序
.FirstOrDefault();
}
private static Version ParseVersion(string dirName)
{
// 从 "app-1.0.0" 格式解析版本号
var parts = dirName.Split('-');
if (parts.Length >= 2 && Version.TryParse(parts[1], out var version))
{
return version;
}
return new Version(0, 0, 0);
}
private static void CleanupDestroyedVersions(string rootDir)
{
try
{
var destroyedDirs = Directory.GetDirectories(rootDir)
.Where(x => File.Exists(Path.Combine(x, ".destroy")));
foreach (var dir in destroyedDirs)
{
try
{
Directory.Delete(dir, recursive: true);
}
catch
{
// 忽略删除失败(可能文件被占用),下次启动再试
}
}
}
catch
{
// 忽略清理失败
}
}
private static void ShowError(string message)
{
#if WINDOWS
try
{
PInvoke.MessageBox(
HWND.Null,
message,
"LanMountainDesktop Launcher",
MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_OK
);
}
catch
{
Console.Error.WriteLine(message);
}
#else
Console.Error.WriteLine(message);
#endif
}
private static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure<App>()

View File

@@ -165,10 +165,27 @@ internal static class Commands
}
var baseDir = AppContext.BaseDirectory;
// 发布版结构Launcher 和 app-* 目录在同一目录
// 检查当前目录是否有 app-* 子目录(发布版)
var appDirs = Directory.GetDirectories(baseDir, "app-*", SearchOption.TopDirectoryOnly);
if (appDirs.Length > 0)
{
// 找到 app-* 目录,说明是发布版结构
return baseDir;
}
// 开发环境:检查父目录是否有主程序
var parent = Path.GetFullPath(Path.Combine(baseDir, ".."));
var parentHost = OperatingSystem.IsWindows()
? Path.Combine(parent, "LanMountainDesktop.exe")
: Path.Combine(parent, "LanMountainDesktop");
return File.Exists(parentHost) ? parent : baseDir;
if (File.Exists(parentHost))
{
return parent;
}
// 默认返回 baseDir
return baseDir;
}
}

View File

@@ -29,4 +29,12 @@ internal sealed class DeferredSplashStageReporter : ISplashStageReporter
_pending.Add((stage, message));
}
}
public void ReportStage(string stage, int progress)
{
if (_inner is not null)
{
_inner.ReportStage(stage, progress);
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Globalization;
using System.Text.Json;
using LanMountainDesktop.Shared.Contracts.Launcher;
namespace LanMountainDesktop.Launcher.Services;
@@ -56,6 +58,38 @@ internal sealed class DeploymentLocator
}
public string? ResolveHostExecutablePath()
{
// 使用新的灵活定位器
var options = new HostDiscoveryOptions
{
ExecutableName = "LanMountainDesktop",
PreferDevModeConfig = true,
RecursiveSearch = false, // 默认不启用递归搜索以提高性能
AdditionalSearchPaths = new List<string>
{
// 可以通过配置文件或环境变量添加更多路径
"${AppRoot}",
"${AppRoot}/..",
"${BaseDirectory}/../..",
}
};
var locator = new FlexibleHostLocator(_appRoot, options);
var result = locator.ResolveHostExecutablePath();
if (result != null)
{
return result;
}
// 回退到旧逻辑(作为备选)
return ResolveHostExecutablePathLegacy();
}
/// <summary>
/// 传统的主程序路径解析(作为备选)
/// </summary>
private string? ResolveHostExecutablePathLegacy()
{
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
@@ -85,9 +119,17 @@ internal sealed class DeploymentLocator
return inParent;
}
// 4. 开发模式:如果启用了开发模式,优先扫描开发路径
// 4. 开发模式:如果启用了开发模式,优先使用保存的自定义路径
if (Views.ErrorWindow.CheckDevModeEnabled())
{
// 4.1 首先检查保存的自定义路径
var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath();
if (!string.IsNullOrWhiteSpace(savedCustomPath) && File.Exists(savedCustomPath))
{
return savedCustomPath;
}
// 4.2 扫描开发路径
var devPath = ScanDevelopmentPaths(executable);
if (!string.IsNullOrWhiteSpace(devPath))
{
@@ -242,4 +284,39 @@ internal sealed class DeploymentLocator
return segments[1];
}
/// <summary>
/// 从部署目录读取版本信息
/// </summary>
public AppVersionInfo GetVersionInfo()
{
var deploymentDir = FindCurrentDeploymentDirectory();
if (!string.IsNullOrWhiteSpace(deploymentDir))
{
var versionFile = Path.Combine(deploymentDir, "version.json");
if (File.Exists(versionFile))
{
try
{
var json = File.ReadAllText(versionFile);
var info = JsonSerializer.Deserialize<AppVersionInfo>(json);
if (info is not null)
{
return info;
}
}
catch
{
// 忽略读取失败,回退到默认值
}
}
}
// 回退:从目录名解析版本,使用默认开发代号
return new AppVersionInfo
{
Version = GetCurrentVersion(),
Codename = "Administrate" // 默认开发代号
};
}
}

View File

@@ -0,0 +1,612 @@
using System.Diagnostics;
using System.Text.Json;
namespace LanMountainDesktop.Launcher.Services;
/// <summary>
/// 灵活的主程序定位器
/// </summary>
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();
}
/// <summary>
/// 解析主程序可执行文件路径
/// </summary>
public string? ResolveHostExecutablePath()
{
var executable = GetExecutableName();
var searchContext = new SearchContext
{
ExecutableName = executable,
AppRoot = _appRoot,
Options = _options
};
// ========== 第一阶段:标准路径查找(快速路径)==========
// 1. 检查环境变量指定的路径(最高优先级 - 用于调试和特殊场景)
var envPath = GetPathFromEnvironment();
if (!string.IsNullOrWhiteSpace(envPath))
{
var validated = ValidateAndReturn(envPath, "environment variable");
if (validated != null) return validated;
}
// 2. 搜索部署目录app-*- 生产环境标准路径
var deploymentPath = SearchDeploymentDirectories(searchContext);
if (!string.IsNullOrWhiteSpace(deploymentPath))
{
return deploymentPath;
}
// 3. 检查 Launcher 同级目录(便携模式)
var portablePath = SearchPortableLocation(searchContext);
if (!string.IsNullOrWhiteSpace(portablePath))
{
return portablePath;
}
// ========== 第二阶段:灵活查找(标准路径找不到时)==========
// 4. 检查配置文件中的路径 - 用户自定义配置
var configPath = GetPathFromConfigFile();
if (!string.IsNullOrWhiteSpace(configPath))
{
var validated = ValidateAndReturn(configPath, "config file");
if (validated != null) return validated;
}
// 5. 搜索附近目录(向上、向下各一层)
var nearbyPath = SearchNearbyDirectories(searchContext);
if (!string.IsNullOrWhiteSpace(nearbyPath))
{
return nearbyPath;
}
// 6. 开发模式:检查保存的自定义路径
if (_options.PreferDevModeConfig && Views.ErrorWindow.CheckDevModeEnabled())
{
var savedPath = Views.ErrorWindow.GetSavedCustomHostPath();
if (!string.IsNullOrWhiteSpace(savedPath))
{
var validated = ValidateAndReturn(savedPath, "saved dev mode path");
if (validated != null) return validated;
}
}
// 7. 搜索标准开发路径
var devPath = SearchDevelopmentPaths(searchContext);
if (!string.IsNullOrWhiteSpace(devPath))
{
return devPath;
}
// 8. 搜索额外的配置路径
var additionalPath = SearchAdditionalPaths(searchContext);
if (!string.IsNullOrWhiteSpace(additionalPath))
{
return additionalPath;
}
// 9. 递归搜索(如果启用)
if (_options.RecursiveSearch)
{
var recursivePath = SearchRecursively(searchContext);
if (!string.IsNullOrWhiteSpace(recursivePath))
{
return recursivePath;
}
}
return null;
}
/// <summary>
/// 从环境变量获取路径
/// </summary>
private string? GetPathFromEnvironment()
{
if (string.IsNullOrWhiteSpace(_options.CustomPathEnvVar))
{
return null;
}
var path = Environment.GetEnvironmentVariable(_options.CustomPathEnvVar);
return path;
}
/// <summary>
/// 从配置文件获取路径
/// </summary>
private string? GetPathFromConfigFile()
{
if (string.IsNullOrWhiteSpace(_options.ConfigFileName))
{
return null;
}
var configPath = Path.Combine(_appRoot, _options.ConfigFileName);
if (!File.Exists(configPath))
{
return null;
}
try
{
var json = File.ReadAllText(configPath);
var config = JsonSerializer.Deserialize<HostDiscoveryConfig>(json);
if (config?.HostPath != null && File.Exists(config.HostPath))
{
return config.HostPath;
}
}
catch
{
// 忽略配置文件读取错误
}
return null;
}
/// <summary>
/// 搜索部署目录
/// </summary>
private string? SearchDeploymentDirectories(SearchContext context)
{
if (!Directory.Exists(_appRoot))
{
return null;
}
try
{
// 查找 app-* 目录
var appDirs = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly)
.Where(dir => !File.Exists(Path.Combine(dir, ".destroy")))
.Where(dir => !File.Exists(Path.Combine(dir, ".partial")))
.ToList();
// 优先选择带 .current 标记的
var currentMarked = appDirs
.Where(dir => File.Exists(Path.Combine(dir, ".current")))
.Select(dir => Path.Combine(dir, context.ExecutableName))
.FirstOrDefault(File.Exists);
if (currentMarked != null)
{
return currentMarked;
}
// 选择版本号最高的
var latest = appDirs
.Select(dir => new
{
Dir = dir,
Version = ParseVersionFromDirectoryName(dir)
})
.OrderByDescending(x => x.Version)
.Select(x => Path.Combine(x.Dir, context.ExecutableName))
.FirstOrDefault(File.Exists);
return latest;
}
catch
{
return null;
}
}
/// <summary>
/// 搜索便携模式位置Launcher 同级目录)
/// </summary>
private string? SearchPortableLocation(SearchContext context)
{
try
{
var launcherDir = AppContext.BaseDirectory;
var portablePath = Path.Combine(launcherDir, context.ExecutableName);
if (File.Exists(portablePath))
{
return portablePath;
}
}
catch
{
// 忽略错误
}
return null;
}
/// <summary>
/// 搜索附近目录(灵活查找,适用于各种部署场景)
/// </summary>
private string? SearchNearbyDirectories(SearchContext context)
{
try
{
var searchDirs = new List<string>();
// Launcher 所在目录
var launcherDir = AppContext.BaseDirectory;
searchDirs.Add(launcherDir);
// 上级目录
var parentDir = Path.GetFullPath(Path.Combine(launcherDir, ".."));
if (Directory.Exists(parentDir))
{
searchDirs.Add(parentDir);
}
// 上上级目录
var grandparentDir = Path.GetFullPath(Path.Combine(launcherDir, "..", ".."));
if (Directory.Exists(grandparentDir))
{
searchDirs.Add(grandparentDir);
}
// AppRoot 及其上级
if (!string.IsNullOrWhiteSpace(_appRoot) && Directory.Exists(_appRoot))
{
searchDirs.Add(_appRoot);
var appParent = Path.GetFullPath(Path.Combine(_appRoot, ".."));
if (Directory.Exists(appParent))
{
searchDirs.Add(appParent);
}
}
// 去重后搜索
foreach (var dir in searchDirs.Distinct(StringComparer.OrdinalIgnoreCase))
{
// 直接搜索
var directPath = Path.Combine(dir, context.ExecutableName);
if (File.Exists(directPath))
{
return directPath;
}
// 搜索子目录(一层)
if (Directory.Exists(dir))
{
foreach (var subDir in Directory.GetDirectories(dir))
{
var subPath = Path.Combine(subDir, context.ExecutableName);
if (File.Exists(subPath))
{
return subPath;
}
}
}
}
}
catch
{
// 忽略搜索错误
}
return null;
}
/// <summary>
/// 搜索开发路径
/// </summary>
private string? SearchDevelopmentPaths(SearchContext context)
{
// 获取 Launcher 所在目录
var launcherDir = AppContext.BaseDirectory;
// 动态构建可能的开发路径(支持不同的项目结构)
var possiblePaths = new List<string>();
// 从解决方案根目录搜索(支持不同的解决方案结构)
var solutionRoot = FindSolutionRoot(launcherDir);
if (!string.IsNullOrWhiteSpace(solutionRoot))
{
// 搜索所有可能的 bin 目录
possiblePaths.AddRange(SearchBinDirectories(solutionRoot, context.ExecutableName));
}
// 添加硬编码的备用路径
possiblePaths.AddRange(new[]
{
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", context.ExecutableName),
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", context.ExecutableName),
Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Debug", "net10.0", context.ExecutableName),
Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Release", "net10.0", context.ExecutableName),
});
foreach (var path in possiblePaths.Select(Path.GetFullPath).Distinct())
{
if (File.Exists(path))
{
return path;
}
}
return null;
}
/// <summary>
/// 搜索额外的配置路径
/// </summary>
private string? SearchAdditionalPaths(SearchContext context)
{
if (_options.AdditionalSearchPaths == null || !_options.AdditionalSearchPaths.Any())
{
return null;
}
foreach (var pattern in _options.AdditionalSearchPaths)
{
try
{
// 替换变量
var expandedPattern = ExpandVariables(pattern);
// 支持通配符
if (expandedPattern.Contains('*') || expandedPattern.Contains('?'))
{
var dir = Path.GetDirectoryName(expandedPattern) ?? _appRoot;
var filePattern = Path.GetFileName(expandedPattern);
if (Directory.Exists(dir))
{
var matches = Directory.GetFiles(dir, filePattern, SearchOption.TopDirectoryOnly);
var validMatch = matches.FirstOrDefault(File.Exists);
if (validMatch != null)
{
return validMatch;
}
}
}
else if (File.Exists(expandedPattern))
{
return expandedPattern;
}
}
catch
{
// 忽略搜索错误
}
}
return null;
}
/// <summary>
/// 递归搜索
/// </summary>
private string? SearchRecursively(SearchContext context)
{
try
{
var searchDirs = new[] { _appRoot, Path.GetFullPath(Path.Combine(_appRoot, "..")) };
foreach (var searchDir in searchDirs.Where(Directory.Exists))
{
var result = SearchDirectoryRecursively(searchDir, context.ExecutableName, 0);
if (result != null)
{
return result;
}
}
}
catch
{
// 忽略递归搜索错误
}
return null;
}
/// <summary>
/// 递归搜索目录
/// </summary>
private string? SearchDirectoryRecursively(string dir, string executableName, int depth)
{
if (depth > _options.MaxRecursionDepth)
{
return null;
}
try
{
// 检查当前目录
var directPath = Path.Combine(dir, executableName);
if (File.Exists(directPath))
{
return directPath;
}
// 检查子目录
foreach (var subDir in Directory.GetDirectories(dir))
{
// 跳过某些目录
var dirName = Path.GetFileName(subDir).ToLowerInvariant();
if (dirName is ".git" or "node_modules" or ".vs" or "obj" or ".launcher")
{
continue;
}
var result = SearchDirectoryRecursively(subDir, executableName, depth + 1);
if (result != null)
{
return result;
}
}
}
catch
{
// 忽略访问错误
}
return null;
}
/// <summary>
/// 查找解决方案根目录
/// </summary>
private string? FindSolutionRoot(string startDir)
{
var current = new DirectoryInfo(startDir);
while (current != null)
{
// 查找 .sln 文件
if (current.GetFiles("*.sln").Any())
{
return current.FullName;
}
// 查找 .git 目录作为备选
if (current.GetDirectories(".git").Any())
{
return current.FullName;
}
current = current.Parent;
}
return null;
}
/// <summary>
/// 搜索 bin 目录
/// </summary>
private IEnumerable<string> SearchBinDirectories(string root, string executableName)
{
var results = new List<string>();
try
{
// 查找所有 bin 目录
var binDirs = Directory.GetDirectories(root, "bin", SearchOption.AllDirectories);
foreach (var binDir in binDirs)
{
// 检查 Debug 和 Release 子目录
var configDirs = new[] { "Debug", "Release" };
foreach (var config in configDirs)
{
var configPath = Path.Combine(binDir, config);
if (Directory.Exists(configPath))
{
// 检查所有 net* 子目录
var frameworkDirs = Directory.GetDirectories(configPath, "net*");
foreach (var fwDir in frameworkDirs)
{
var exePath = Path.Combine(fwDir, executableName);
if (File.Exists(exePath))
{
results.Add(exePath);
}
}
}
}
}
}
catch
{
// 忽略搜索错误
}
return results;
}
/// <summary>
/// 验证路径并返回
/// </summary>
private string? ValidateAndReturn(string path, string source)
{
if (File.Exists(path))
{
Debug.WriteLine($"Found host executable from {source}: {path}");
return path;
}
// 尝试添加 .exeWindows
if (OperatingSystem.IsWindows() && !path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
var withExe = path + ".exe";
if (File.Exists(withExe))
{
Debug.WriteLine($"Found host executable from {source}: {withExe}");
return withExe;
}
}
return null;
}
/// <summary>
/// 获取可执行文件名
/// </summary>
private string GetExecutableName()
{
var name = _options.ExecutableName;
if (OperatingSystem.IsWindows() && !name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
name += ".exe";
}
return name;
}
/// <summary>
/// 展开路径变量
/// </summary>
private string ExpandVariables(string path)
{
return path
.Replace("${AppRoot}", _appRoot)
.Replace("${BaseDirectory}", AppContext.BaseDirectory)
.Replace("${UserProfile}", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
.Replace("${LocalAppData}", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
}
/// <summary>
/// 从目录名解析版本
/// </summary>
private static Version ParseVersionFromDirectoryName(string path)
{
var fileName = Path.GetFileName(path);
if (string.IsNullOrWhiteSpace(fileName))
{
return new Version(0, 0, 0);
}
var segments = fileName.Split('-');
if (segments.Length < 2)
{
return new Version(0, 0, 0);
}
return Version.TryParse(segments[1], out var version) ? version : new Version(0, 0, 0);
}
/// <summary>
/// 搜索上下文
/// </summary>
private class SearchContext
{
public required string ExecutableName { get; set; }
public required string AppRoot { get; set; }
public required HostDiscoveryOptions Options { get; set; }
}
/// <summary>
/// 发现配置文件
/// </summary>
private class HostDiscoveryConfig
{
public string? HostPath { get; set; }
public List<string>? AdditionalPaths { get; set; }
}
}

View File

@@ -0,0 +1,47 @@
namespace LanMountainDesktop.Launcher.Services;
/// <summary>
/// 主程序发现选项
/// </summary>
public sealed class HostDiscoveryOptions
{
/// <summary>
/// 可执行文件名Windows 下自动添加 .exe
/// </summary>
public string ExecutableName { get; set; } = "LanMountainDesktop";
/// <summary>
/// 额外的搜索路径(支持通配符)
/// </summary>
public List<string> AdditionalSearchPaths { get; set; } = new();
/// <summary>
/// 是否递归搜索子目录
/// </summary>
public bool RecursiveSearch { get; set; } = false;
/// <summary>
/// 递归搜索的最大深度
/// </summary>
public int MaxRecursionDepth { get; set; } = 3;
/// <summary>
/// 环境变量名称,用于指定自定义路径
/// </summary>
public string? CustomPathEnvVar { get; set; } = "LMD_HOST_PATH";
/// <summary>
/// 配置文件路径(相对于 app root
/// </summary>
public string? ConfigFileName { get; set; } = "host-discovery.json";
/// <summary>
/// 是否优先使用开发模式配置
/// </summary>
public bool PreferDevModeConfig { get; set; } = true;
/// <summary>
/// 搜索超时(毫秒)
/// </summary>
public int SearchTimeoutMs { get; set; } = 5000;
}

View File

@@ -3,4 +3,9 @@ namespace LanMountainDesktop.Launcher.Services;
internal interface ISplashStageReporter
{
void Report(string stage, string message);
/// <summary>
/// 报告阶段和进度0-100
/// </summary>
void ReportStage(string stage, int progress);
}

View File

@@ -1,3 +1,4 @@
using System.Buffers;
using System.IO.Pipes;
using System.Text.Json;
using LanMountainDesktop.Shared.Contracts.Launcher;
@@ -6,81 +7,164 @@ namespace LanMountainDesktop.Launcher.Services.Ipc;
/// <summary>
/// Launcher IPC 服务端 - 接收主程序的启动进度报告
/// 采用持久连接 + 长度前缀协议,支持客户端在同一连接上多次发送消息。
/// 跨平台实现Windows 使用命名管道Linux/macOS 使用 Unix 域套接字
/// </summary>
public class LauncherIpcServer : IDisposable
{
private readonly CancellationTokenSource _cts = new();
private NamedPipeServerStream? _pipeServer;
private readonly Action<StartupProgressMessage> _onProgress;
private Task? _listenTask;
private NamedPipeServerStream? _currentPipe;
/// <summary>
/// 协议:每条消息以 4 字节小端 int32 长度前缀开头,后跟 UTF-8 JSON 正文。
/// 这在 Windows Message 模式和 Unix Byte 模式下均能可靠工作。
/// </summary>
private const int LengthPrefixSize = 4;
public LauncherIpcServer(Action<StartupProgressMessage> onProgress)
{
_onProgress = onProgress;
}
/// <summary>
/// 启动 IPC 服务端监听
/// </summary>
public void Start()
{
_listenTask = Task.Run(async () =>
_listenTask = Task.Run(ListenLoopAsync, _cts.Token);
}
private async Task ListenLoopAsync()
{
while (!_cts.Token.IsCancellationRequested)
{
while (!_cts.Token.IsCancellationRequested)
NamedPipeServerStream? pipe = null;
try
{
pipe = new NamedPipeServerStream(
LauncherIpcConstants.PipeName,
PipeDirection.In,
1,
PipeTransmissionMode.Byte);
_currentPipe = pipe;
await pipe.WaitForConnectionAsync(_cts.Token);
// 持久连接:在同一连接上循环读取多条消息,直到客户端断开
await ReadMessagesFromConnectionAsync(pipe, _cts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (IOException)
{
// 客户端断开连接,继续等待新连接
continue;
}
catch (ObjectDisposedException)
{
break;
}
catch (Exception ex)
{
Console.Error.WriteLine($"IPC listen error: {ex.Message}");
try
{
_pipeServer = new NamedPipeServerStream(
LauncherIpcConstants.PipeName,
PipeDirection.In,
1,
PipeTransmissionMode.Message);
await _pipeServer.WaitForConnectionAsync(_cts.Token);
using var reader = new StreamReader(_pipeServer);
var json = await reader.ReadToEndAsync(_cts.Token);
if (!string.IsNullOrEmpty(json))
{
try
{
var message = JsonSerializer.Deserialize<StartupProgressMessage>(json);
if (message != null)
{
_onProgress(message);
}
}
catch (JsonException)
{
// 忽略解析错误
}
}
try
{
_pipeServer.Disconnect();
}
catch { }
await Task.Delay(200, _cts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (IOException)
}
finally
{
try
{
// 管道断开,继续监听
continue;
pipe?.Dispose();
}
catch (Exception ex)
catch { }
if (ReferenceEquals(_currentPipe, pipe))
{
Console.Error.WriteLine($"IPC error: {ex.Message}");
await Task.Delay(100, _cts.Token);
_currentPipe = null;
}
}
}, _cts.Token);
}
}
/// <summary>
/// 从已连接的管道中持续读取消息,直到连接断开或取消
/// </summary>
private async Task ReadMessagesFromConnectionAsync(NamedPipeServerStream pipe, CancellationToken cancellationToken)
{
var lengthBuffer = ArrayPool<byte>.Shared.Rent(LengthPrefixSize);
try
{
while (pipe.IsConnected && !cancellationToken.IsCancellationRequested)
{
// 1. 读取 4 字节长度前缀
var totalRead = 0;
while (totalRead < LengthPrefixSize)
{
var read = await pipe.ReadAsync(lengthBuffer.AsMemory(totalRead, LengthPrefixSize - totalRead), cancellationToken);
if (read == 0)
{
// 连接已关闭
return;
}
totalRead += read;
}
var payloadLength = BitConverter.ToInt32(lengthBuffer, 0);
if (payloadLength <= 0 || payloadLength > 1024 * 1024) // 最大 1MB 单条消息
{
// 无效长度,跳过此连接
return;
}
// 2. 读取消息正文
var payloadBuffer = ArrayPool<byte>.Shared.Rent(payloadLength);
try
{
totalRead = 0;
while (totalRead < payloadLength)
{
var read = await pipe.ReadAsync(payloadBuffer.AsMemory(totalRead, payloadLength - totalRead), cancellationToken);
if (read == 0)
{
return;
}
totalRead += read;
}
// 3. 反序列化并回调
var json = System.Text.Encoding.UTF8.GetString(payloadBuffer, 0, payloadLength);
var message = JsonSerializer.Deserialize<StartupProgressMessage>(json);
if (message is not null)
{
_onProgress(message);
}
}
catch (JsonException)
{
// 忽略解析错误,继续读取下一条消息
}
finally
{
ArrayPool<byte>.Shared.Return(payloadBuffer);
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(lengthBuffer);
}
}
/// <summary>
/// 停止 IPC 服务端
/// </summary>
@@ -89,17 +173,16 @@ public class LauncherIpcServer : IDisposable
_cts.Cancel();
try
{
_pipeServer?.Disconnect();
_currentPipe?.Dispose();
}
catch { }
}
public void Dispose()
{
Stop();
_pipeServer?.Dispose();
_cts.Dispose();
try
{
_listenTask?.Wait(TimeSpan.FromSeconds(2));

View File

@@ -34,15 +34,15 @@ internal sealed class LauncherFlowCoordinator
_oobeSteps = [new WelcomeOobeStep(_oobeStateService)];
}
public async Task<LauncherResult> RunAsync()
public async Task<LauncherResult> RunAsync(SplashWindow? existingSplashWindow = null)
{
try
{
// 清理待删除的旧版本
_deploymentLocator.CleanupDestroyedDeployments();
// 显示 Splash 窗口
var splashWindow = await Dispatcher.UIThread.InvokeAsync(() =>
// 使用传入的 Splash 窗口或创建新的
var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() =>
{
var window = new SplashWindow();
window.Show();
@@ -51,12 +51,29 @@ internal sealed class LauncherFlowCoordinator
var reporter = (ISplashStageReporter)splashWindow;
// 跟踪主程序是否已就绪,就绪后自动关闭 Splash 窗口
var hostReadyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
// 启动 IPC 服务端监听主程序进度
using var ipcServer = new LauncherIpcServer(msg =>
{
Dispatcher.UIThread.Post(() =>
{
reporter.Report(msg.Stage.ToString().ToLower(), msg.Message ?? "");
try
{
reporter.Report(msg.Stage.ToString().ToLower(), msg.Message ?? "");
// 主程序报告就绪后,关闭 Splash 窗口
if (msg.Stage == StartupStage.Ready && splashWindow.IsVisible && splashWindow.IsLoaded)
{
splashWindow.Close();
hostReadyTcs.TrySetResult();
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[LauncherFlowCoordinator] Error in IPC callback: {ex.Message}");
}
});
});
ipcServer.Start();
@@ -94,14 +111,53 @@ internal sealed class LauncherFlowCoordinator
// 启动主程序
reporter.Report("launch", "正在启动...");
var hostResult = await LaunchHostWithIpcAsync();
var (hostResult, hostProcess) = await LaunchHostWithIpcAsync(splashWindow);
if (!hostResult.Success)
{
return hostResult;
}
// 等待主程序就绪或超时
await Task.Delay(TimeSpan.FromSeconds(30));
// 等待主程序进程退出。Launcher 作为后台守护进程保持运行,
// 维持 IPC 管道服务端供主程序报告启动进度。
if (hostProcess is not null)
{
// 等待主程序就绪或进程退出(取先发生者)
// 如果主程序在 60 秒内未报告 Ready也关闭 Splash 窗口作为超时保护
var readyOrTimeout = Task.WhenAny(
hostReadyTcs.Task,
Task.Delay(TimeSpan.FromSeconds(60)));
var processExitTask = hostProcess.WaitForExitAsync();
// 先等待就绪/超时,然后等待进程退出
await readyOrTimeout;
// 如果 Splash 窗口仍然打开(超时情况),关闭它
if (splashWindow.IsVisible)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
try
{
if (splashWindow.IsVisible && splashWindow.IsLoaded)
{
splashWindow.Close();
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing splash window on timeout: {ex.Message}");
}
});
}
await processExitTask;
}
else
{
// 如果无法获取进程引用,退回到有限等待
await Task.Delay(TimeSpan.FromSeconds(30));
}
return new LauncherResult
{
@@ -113,7 +169,22 @@ internal sealed class LauncherFlowCoordinator
}
finally
{
await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Close());
// Splash 窗口可能已由 IPC Ready 回调关闭,这里做安全清理
await Dispatcher.UIThread.InvokeAsync(() =>
{
try
{
if (splashWindow.IsVisible && splashWindow.IsLoaded)
{
splashWindow.Close();
Console.WriteLine("[LauncherFlowCoordinator] Splash window closed in finally block");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing splash window in finally: {ex.Message}");
}
});
}
}
catch (Exception ex)
@@ -124,12 +195,12 @@ internal sealed class LauncherFlowCoordinator
Stage = "launch",
Code = "exception",
Message = ex.Message,
ErrorMessage = ex.Message
ErrorMessage = ex.ToString()
};
}
}
private async Task<LauncherResult> LaunchHostWithIpcAsync(string? customHostPath = null)
private async Task<(LauncherResult Result, Process? Process)> LaunchHostWithIpcAsync(SplashWindow? splashWindow = null, string? customHostPath = null)
{
// 优先使用自定义路径(调试模式选择的路径)
var hostPath = customHostPath ?? _deploymentLocator.ResolveHostExecutablePath();
@@ -145,19 +216,19 @@ internal sealed class LauncherFlowCoordinator
// 用户选择重试,如果有选择路径则使用,否则重新尝试
if (!string.IsNullOrWhiteSpace(selectedPath))
{
return await LaunchHostWithIpcAsync(selectedPath);
return await LaunchHostWithIpcAsync(splashWindow, selectedPath);
}
return await LaunchHostWithIpcAsync();
return await LaunchHostWithIpcAsync(splashWindow);
}
// 用户选择退出
return new LauncherResult
return (new LauncherResult
{
Success = false,
Stage = "launchHost",
Code = "host_not_found",
Message = "LanMountainDesktop host executable not found."
};
}, null);
}
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
@@ -168,24 +239,40 @@ internal sealed class LauncherFlowCoordinator
var processStartInfo = new ProcessStartInfo
{
FileName = hostPath,
UseShellExecute = true,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot()
};
// 转发命令行参数给主程序(排除 Launcher 自己的命令和选项)
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] =
Environment.ProcessId.ToString();
processStartInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] =
_deploymentLocator.GetAppRoot();
// 传递版本信息
var versionInfo = _deploymentLocator.GetVersionInfo();
processStartInfo.EnvironmentVariables[LauncherIpcConstants.VersionEnvVar] = versionInfo.Version;
processStartInfo.EnvironmentVariables[LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename;
Process.Start(processStartInfo);
return new LauncherResult
var hostProcess = Process.Start(processStartInfo);
return (new LauncherResult
{
Success = true,
Stage = "launchHost",
Code = "ok",
Message = "Host launched."
};
}, hostProcess);
}
/// <summary>
@@ -193,19 +280,65 @@ internal sealed class LauncherFlowCoordinator
/// </summary>
private async Task<(ErrorWindowResult Result, string? CustomPath)> ShowHostNotFoundErrorAsync()
{
return await Dispatcher.UIThread.InvokeAsync(async () =>
ErrorWindow? errorWindow = null;
// 在 UI 线程创建并显示错误窗口
await Dispatcher.UIThread.InvokeAsync(() =>
{
var errorWindow = new ErrorWindow();
errorWindow.SetErrorMessage("找不到阑山桌面应用程序。");
errorWindow.Show();
var result = await errorWindow.WaitForChoiceAsync();
var customPath = errorWindow.GetCustomHostPath();
await Dispatcher.UIThread.InvokeAsync(() => errorWindow.Close());
return (result, customPath);
try
{
errorWindow = new ErrorWindow();
errorWindow.SetErrorMessage("找不到阑山桌面应用程序。");
errorWindow.Show();
Console.WriteLine("[LauncherFlowCoordinator] ErrorWindow shown for host not found");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[LauncherFlowCoordinator] Failed to show ErrorWindow: {ex.Message}");
}
});
if (errorWindow is null)
{
Console.Error.WriteLine("[LauncherFlowCoordinator] ErrorWindow is null, cannot wait for choice");
return (ErrorWindowResult.Exit, null);
}
// 等待用户选择
ErrorWindowResult result;
string? customPath;
try
{
result = await errorWindow.WaitForChoiceAsync();
customPath = errorWindow.GetCustomHostPath();
Console.WriteLine($"[LauncherFlowCoordinator] ErrorWindow result: {result}, customPath: {customPath != null}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[LauncherFlowCoordinator] Error waiting for choice: {ex.Message}");
result = ErrorWindowResult.Exit;
customPath = null;
}
// 安全关闭错误窗口
await Dispatcher.UIThread.InvokeAsync(() =>
{
try
{
if (errorWindow.IsVisible && errorWindow.IsLoaded)
{
errorWindow.Close();
Console.WriteLine("[LauncherFlowCoordinator] ErrorWindow closed successfully");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[LauncherFlowCoordinator] Error closing ErrorWindow: {ex.Message}");
}
});
return (result, customPath);
}
private static void EnsureExecutable(string path)
@@ -237,22 +370,72 @@ internal sealed class LauncherFlowCoordinator
public async Task RunAsync(CancellationToken cancellationToken)
{
var window = await Dispatcher.UIThread.InvokeAsync(() =>
{
var oobeWindow = new OobeWindow();
oobeWindow.Show();
return oobeWindow;
});
OobeWindow? window = null;
try
{
using var _ = cancellationToken.Register(() => window.Close());
window = await Dispatcher.UIThread.InvokeAsync(() =>
{
try
{
var oobeWindow = new OobeWindow();
oobeWindow.Show();
Console.WriteLine("[WelcomeOobeStep] OOBE window shown");
return oobeWindow;
}
catch (Exception ex)
{
Console.Error.WriteLine($"[WelcomeOobeStep] Failed to show OOBE window: {ex.Message}");
return null;
}
});
if (window is null)
{
Console.Error.WriteLine("[WelcomeOobeStep] OOBE window is null, skipping OOBE");
_stateService.MarkCompleted();
return;
}
using var _ = cancellationToken.Register(() =>
{
try
{
if (window.IsVisible && window.IsLoaded)
{
window.Close();
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[WelcomeOobeStep] Error closing OOBE window on cancel: {ex.Message}");
}
});
await window.WaitForEnterAsync().ConfigureAwait(false);
Console.WriteLine("[WelcomeOobeStep] OOBE completed by user");
_stateService.MarkCompleted();
}
finally
{
await Dispatcher.UIThread.InvokeAsync(() => window.Close());
if (window is not null)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
try
{
if (window.IsVisible && window.IsLoaded)
{
window.Close();
Console.WriteLine("[WelcomeOobeStep] OOBE window closed in finally");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[WelcomeOobeStep] Error closing OOBE window in finally: {ex.Message}");
}
});
}
}
}
}

View File

@@ -6,7 +6,12 @@ internal sealed class OobeStateService
public OobeStateService(string appRoot)
{
var stateDir = Path.Combine(appRoot, ".launcher", "state");
// 将 OOBE 状态文件存储在用户可写的 LocalApplicationData 目录中,
// 而不是安装目录Program Files 下普通用户没有写入权限)。
var appDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
var stateDir = Path.Combine(appDataDir, ".launcher", "state");
Directory.CreateDirectory(stateDir);
_markerPath = Path.Combine(stateDir, "first_run_completed");
}

View File

@@ -0,0 +1,263 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace LanMountainDesktop.Launcher.ViewModels;
/// <summary>
/// 开发调试窗口 ViewModel
/// </summary>
public sealed class DevDebugWindowViewModel : INotifyPropertyChanged
{
private bool _isSplashEnabled = true;
private bool _isErrorEnabled = true;
private bool _isUpdateEnabled = true;
private bool _isOobeEnabled = true;
private string _statusMessage = "就绪";
public event PropertyChangedEventHandler? PropertyChanged;
#region
/// <summary>
/// 启动画面是否启用实际功能
/// </summary>
public bool IsSplashEnabled
{
get => _isSplashEnabled;
set
{
if (_isSplashEnabled != value)
{
_isSplashEnabled = value;
OnPropertyChanged();
UpdateStatus($"启动画面: {(value ? "" : "")}");
}
}
}
/// <summary>
/// 错误页面是否启用实际功能
/// </summary>
public bool IsErrorEnabled
{
get => _isErrorEnabled;
set
{
if (_isErrorEnabled != value)
{
_isErrorEnabled = value;
OnPropertyChanged();
UpdateStatus($"错误页面: {(value ? "" : "")}");
}
}
}
/// <summary>
/// 更新页面是否启用实际功能
/// </summary>
public bool IsUpdateEnabled
{
get => _isUpdateEnabled;
set
{
if (_isUpdateEnabled != value)
{
_isUpdateEnabled = value;
OnPropertyChanged();
UpdateStatus($"更新页面: {(value ? "" : "")}");
}
}
}
/// <summary>
/// OOBE页面是否启用实际功能
/// </summary>
public bool IsOobeEnabled
{
get => _isOobeEnabled;
set
{
if (_isOobeEnabled != value)
{
_isOobeEnabled = value;
OnPropertyChanged();
UpdateStatus($"OOBE页面: {(value ? "" : "")}");
}
}
}
#endregion
#region
/// <summary>
/// 状态消息
/// </summary>
public string StatusMessage
{
get => _statusMessage;
private set
{
if (_statusMessage != value)
{
_statusMessage = value;
OnPropertyChanged();
}
}
}
#endregion
#region
/// <summary>
/// 打开启动画面命令
/// </summary>
public ICommand OpenSplashCommand { get; }
/// <summary>
/// 打开错误页面命令
/// </summary>
public ICommand OpenErrorCommand { get; }
/// <summary>
/// 打开更新页面命令
/// </summary>
public ICommand OpenUpdateCommand { get; }
/// <summary>
/// 打开OOBE页面命令
/// </summary>
public ICommand OpenOobeCommand { get; }
/// <summary>
/// 全部切换到查看模式命令
/// </summary>
public ICommand SetAllViewOnlyCommand { get; }
/// <summary>
/// 全部切换到功能模式命令
/// </summary>
public ICommand SetAllFunctionalCommand { get; }
/// <summary>
/// 关闭窗口命令
/// </summary>
public ICommand CloseCommand { get; }
#endregion
#region
/// <summary>
/// 请求打开启动画面
/// </summary>
public event EventHandler<SplashOpenEventArgs>? OpenSplashRequested;
/// <summary>
/// 请求打开错误页面
/// </summary>
public event EventHandler<ErrorOpenEventArgs>? OpenErrorRequested;
/// <summary>
/// 请求打开更新页面
/// </summary>
public event EventHandler<UpdateOpenEventArgs>? OpenUpdateRequested;
/// <summary>
/// 请求打开OOBE页面
/// </summary>
public event EventHandler<OobeOpenEventArgs>? OpenOobeRequested;
/// <summary>
/// 请求关闭窗口
/// </summary>
public event EventHandler? CloseRequested;
#endregion
public DevDebugWindowViewModel()
{
OpenSplashCommand = new RelayCommand(() =>
{
OpenSplashRequested?.Invoke(this, new SplashOpenEventArgs(IsSplashEnabled));
});
OpenErrorCommand = new RelayCommand(() =>
{
OpenErrorRequested?.Invoke(this, new ErrorOpenEventArgs(IsErrorEnabled));
});
OpenUpdateCommand = new RelayCommand(() =>
{
OpenUpdateRequested?.Invoke(this, new UpdateOpenEventArgs(IsUpdateEnabled));
});
OpenOobeCommand = new RelayCommand(() =>
{
OpenOobeRequested?.Invoke(this, new OobeOpenEventArgs(IsOobeEnabled));
});
SetAllViewOnlyCommand = new RelayCommand(() =>
{
IsSplashEnabled = false;
IsErrorEnabled = false;
IsUpdateEnabled = false;
IsOobeEnabled = false;
UpdateStatus("全部页面已切换到查看模式");
});
SetAllFunctionalCommand = new RelayCommand(() =>
{
IsSplashEnabled = true;
IsErrorEnabled = true;
IsUpdateEnabled = true;
IsOobeEnabled = true;
UpdateStatus("全部页面已切换到功能模式");
});
CloseCommand = new RelayCommand(() =>
{
CloseRequested?.Invoke(this, EventArgs.Empty);
});
}
private void UpdateStatus(string message)
{
StatusMessage = $"[{DateTime.Now:HH:mm:ss}] {message}";
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
#region
public class SplashOpenEventArgs : EventArgs
{
public bool IsFunctional { get; }
public SplashOpenEventArgs(bool isFunctional) => IsFunctional = isFunctional;
}
public class ErrorOpenEventArgs : EventArgs
{
public bool IsFunctional { get; }
public ErrorOpenEventArgs(bool isFunctional) => IsFunctional = isFunctional;
}
public class UpdateOpenEventArgs : EventArgs
{
public bool IsFunctional { get; }
public UpdateOpenEventArgs(bool isFunctional) => IsFunctional = isFunctional;
}
public class OobeOpenEventArgs : EventArgs
{
public bool IsFunctional { get; }
public OobeOpenEventArgs(bool isFunctional) => IsFunctional = isFunctional;
}
#endregion

View File

@@ -0,0 +1,67 @@
using System.Windows.Input;
namespace LanMountainDesktop.Launcher.ViewModels;
/// <summary>
/// 简单的命令实现
/// </summary>
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return _canExecute?.Invoke() ?? true;
}
public void Execute(object? parameter)
{
_execute();
}
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// 带参数的 RelayCommand
/// </summary>
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Predicate<T>? _canExecute;
public RelayCommand(Action<T> execute, Predicate<T>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return _canExecute?.Invoke((T)parameter!) ?? true;
}
public void Execute(object? parameter)
{
_execute((T)parameter!);
}
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,182 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LanMountainDesktop.Launcher.ViewModels"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="600"
x:Class="LanMountainDesktop.Launcher.Views.DevDebugWindow"
x:DataType="vm:DevDebugWindowViewModel"
Title="开发调试窗口 - Launcher"
Width="500"
Height="600"
WindowStartupLocation="CenterScreen"
Icon="/Assets/logo.ico">
<Design.DataContext>
<vm:DevDebugWindowViewModel />
</Design.DataContext>
<Border Padding="20"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<Grid RowDefinitions="Auto,*,Auto,Auto">
<!-- 标题 -->
<StackPanel Grid.Row="0" Margin="0,0,0,20">
<TextBlock Text="🛠️ 开发调试窗口"
FontSize="24"
FontWeight="Bold"
Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<TextBlock Text="用于开发和调试 Launcher 的各个页面"
FontSize="12"
Opacity="0.7"
Margin="0,5,0,0"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" />
</StackPanel>
<!-- 页面列表 -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="15">
<!-- 启动画面 -->
<Border Background="{DynamicResource SystemControlBackgroundAltMediumBrush}"
CornerRadius="8"
Padding="15">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="🚀 启动画面 (SplashWindow)"
FontWeight="SemiBold"
FontSize="14" />
<TextBlock Text="显示启动进度和状态"
FontSize="11"
Opacity="0.6"
Margin="0,3,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8">
<ToggleSwitch Content="启用功能"
IsChecked="{Binding IsSplashEnabled}"
OnContent="功能"
OffContent="查看" />
<Button Content="打开"
Command="{Binding OpenSplashCommand}"
HorizontalAlignment="Right" />
</StackPanel>
</Grid>
</Border>
<!-- 错误页面 -->
<Border Background="{DynamicResource SystemControlBackgroundAltMediumBrush}"
CornerRadius="8"
Padding="15">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="❌ 错误页面 (ErrorWindow)"
FontWeight="SemiBold"
FontSize="14" />
<TextBlock Text="显示错误信息和重试选项"
FontSize="11"
Opacity="0.6"
Margin="0,3,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8">
<ToggleSwitch Content="启用功能"
IsChecked="{Binding IsErrorEnabled}"
OnContent="功能"
OffContent="查看" />
<Button Content="打开"
Command="{Binding OpenErrorCommand}"
HorizontalAlignment="Right" />
</StackPanel>
</Grid>
</Border>
<!-- 更新页面 -->
<Border Background="{DynamicResource SystemControlBackgroundAltMediumBrush}"
CornerRadius="8"
Padding="15">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="⬆️ 更新页面 (UpdateWindow)"
FontWeight="SemiBold"
FontSize="14" />
<TextBlock Text="显示更新进度和状态"
FontSize="11"
Opacity="0.6"
Margin="0,3,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8">
<ToggleSwitch Content="启用功能"
IsChecked="{Binding IsUpdateEnabled}"
OnContent="功能"
OffContent="查看" />
<Button Content="打开"
Command="{Binding OpenUpdateCommand}"
HorizontalAlignment="Right" />
</StackPanel>
</Grid>
</Border>
<!-- OOBE页面 -->
<Border Background="{DynamicResource SystemControlBackgroundAltMediumBrush}"
CornerRadius="8"
Padding="15">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="👋 OOBE页面 (OobeWindow)"
FontWeight="SemiBold"
FontSize="14" />
<TextBlock Text="首次运行引导页面"
FontSize="11"
Opacity="0.6"
Margin="0,3,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8">
<ToggleSwitch Content="启用功能"
IsChecked="{Binding IsOobeEnabled}"
OnContent="功能"
OffContent="查看" />
<Button Content="打开"
Command="{Binding OpenOobeCommand}"
HorizontalAlignment="Right" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
<!-- 批量操作 -->
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="10"
Margin="0,15">
<Button Content="全部设为查看模式"
Command="{Binding SetAllViewOnlyCommand}"
Background="{DynamicResource SystemControlBackgroundAltMediumBrush}" />
<Button Content="全部设为功能模式"
Command="{Binding SetAllFunctionalCommand}"
Background="{DynamicResource SystemControlHighlightAccentBrush}"
Foreground="White" />
</StackPanel>
<!-- 底部状态栏 -->
<Border Grid.Row="3"
Background="{DynamicResource SystemControlBackgroundAltMediumBrush}"
CornerRadius="4"
Padding="10">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="{Binding StatusMessage}"
FontSize="11"
Opacity="0.8"
TextTrimming="CharacterEllipsis" />
<Button Grid.Column="1"
Content="关闭"
Command="{Binding CloseCommand}"
Padding="15,5" />
</Grid>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,196 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LanMountainDesktop.Launcher.Services;
using LanMountainDesktop.Launcher.ViewModels;
using LanMountainDesktop.Launcher.Views;
namespace LanMountainDesktop.Launcher.Views;
/// <summary>
/// 开发调试窗口
/// </summary>
public partial class DevDebugWindow : Window
{
private readonly DevDebugWindowViewModel _viewModel;
public DevDebugWindow()
{
AvaloniaXamlLoader.Load(this);
_viewModel = new DevDebugWindowViewModel();
DataContext = _viewModel;
// 订阅事件
_viewModel.OpenSplashRequested += OnOpenSplashRequested;
_viewModel.OpenErrorRequested += OnOpenErrorRequested;
_viewModel.OpenUpdateRequested += OnOpenUpdateRequested;
_viewModel.OpenOobeRequested += OnOpenOobeRequested;
_viewModel.CloseRequested += OnCloseRequested;
}
/// <summary>
/// 打开启动画面
/// </summary>
private void OnOpenSplashRequested(object? sender, SplashOpenEventArgs e)
{
var splashWindow = new SplashWindow();
if (!e.IsFunctional)
{
// 查看模式:显示模拟内容
splashWindow.SetDebugMode(true);
}
splashWindow.Show();
if (e.IsFunctional)
{
// 功能模式:模拟正常启动流程
_ = SimulateSplashProgress(splashWindow);
}
}
/// <summary>
/// 打开错误页面
/// </summary>
private void OnOpenErrorRequested(object? sender, ErrorOpenEventArgs e)
{
var errorWindow = new ErrorWindow();
if (!e.IsFunctional)
{
// 查看模式:显示模拟错误
errorWindow.SetDebugMode(true);
errorWindow.SetErrorMessage("[调试模式] 这是一个模拟的错误消息,用于查看错误页面的样式和布局。");
}
else
{
// 功能模式:显示真实错误
errorWindow.SetErrorMessage("找不到阑山桌面应用程序。\n\n请检查应用安装是否完整。");
}
errorWindow.Show();
}
/// <summary>
/// 打开更新页面
/// </summary>
private void OnOpenUpdateRequested(object? sender, UpdateOpenEventArgs e)
{
var updateWindow = new UpdateWindow();
if (!e.IsFunctional)
{
// 查看模式:显示模拟更新
updateWindow.SetDebugMode(true);
}
updateWindow.Show();
if (e.IsFunctional)
{
// 功能模式:模拟更新进度
_ = SimulateUpdateProgress(updateWindow);
}
}
/// <summary>
/// 打开OOBE页面
/// </summary>
private void OnOpenOobeRequested(object? sender, OobeOpenEventArgs e)
{
var oobeWindow = new OobeWindow();
if (!e.IsFunctional)
{
// 查看模式:显示调试标记(通过标题)
oobeWindow.Title = "[调试模式] 欢迎使用阑山桌面";
}
oobeWindow.Show();
if (e.IsFunctional)
{
// 功能模式:等待用户点击后自动关闭
_ = SimulateOobeProgress(oobeWindow);
}
}
/// <summary>
/// 模拟OOBE流程
/// </summary>
private async Task SimulateOobeProgress(OobeWindow oobeWindow)
{
try
{
// 等待用户点击开始按钮
await oobeWindow.WaitForEnterAsync();
// 用户点击后窗口会自动关闭通过OobeWindow内部的动画和关闭逻辑
Console.WriteLine("[DevDebugWindow] OOBE completed by user");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DevDebugWindow] Error during OOBE simulation: {ex.Message}");
}
}
/// <summary>
/// 关闭窗口
/// </summary>
private void OnCloseRequested(object? sender, EventArgs e)
{
Close();
}
/// <summary>
/// 模拟启动画面进度
/// </summary>
private async Task SimulateSplashProgress(SplashWindow splashWindow)
{
var stages = new[] { "初始化", "检查更新", "加载组件", "启动应用" };
var reporter = (ISplashStageReporter)splashWindow;
for (int i = 0; i < stages.Length; i++)
{
reporter.ReportStage(stages[i], (i + 1) * 25);
await Task.Delay(500);
}
// 3秒后关闭
await Task.Delay(3000);
splashWindow.Close();
}
/// <summary>
/// 模拟更新进度
/// </summary>
private async Task SimulateUpdateProgress(UpdateWindow updateWindow)
{
var stages = new[] { "下载", "验证", "安装", "清理" };
foreach (var stage in stages)
{
updateWindow.Report(stage, $"正在{stage}...", Array.IndexOf(stages, stage) * 25 + 10);
await Task.Delay(800);
}
updateWindow.ReportComplete(true, null);
// 2秒后关闭
await Task.Delay(2000);
updateWindow.Close();
}
protected override void OnClosed(EventArgs e)
{
// 取消订阅事件
_viewModel.OpenSplashRequested -= OnOpenSplashRequested;
_viewModel.OpenErrorRequested -= OnOpenErrorRequested;
_viewModel.OpenUpdateRequested -= OnOpenUpdateRequested;
_viewModel.OpenOobeRequested -= OnOpenOobeRequested;
_viewModel.CloseRequested -= OnCloseRequested;
base.OnClosed(e);
}
}

View File

@@ -14,7 +14,8 @@
CanResize="False"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
TransparencyLevelHint="None">
TransparencyLevelHint="None"
Icon="/Assets/logo.ico">
<Design.DataContext>
<views:ErrorDebugWindow />
</Design.DataContext>

View File

@@ -11,6 +11,7 @@ namespace LanMountainDesktop.Launcher.Views;
public partial class ErrorDebugWindow : Window
{
private string? _selectedHostPath;
private bool _isInitialized = false;
/// <summary>
/// 是否启用了开发模式
@@ -25,22 +26,36 @@ public partial class ErrorDebugWindow : Window
public ErrorDebugWindow()
{
AvaloniaXamlLoader.Load(this);
InitializeComponents();
// 延迟到窗口加载完成后再初始化组件
this.Loaded += OnWindowLoaded;
}
public ErrorDebugWindow(bool devModeEnabled, string? initialPath) : this()
{
IsDevModeEnabled = devModeEnabled;
_selectedHostPath = initialPath;
}
// 设置初始值
/// <summary>
/// 窗口加载完成事件
/// </summary>
private void OnWindowLoaded(object? sender, RoutedEventArgs e)
{
if (_isInitialized) return;
_isInitialized = true;
Console.WriteLine("[ErrorDebugWindow] Window loaded, initializing components...");
InitializeComponents();
// 设置初始值(在视觉树准备好后)
var devModeToggle = this.FindControl<ToggleSwitch>("DevModeToggle");
if (devModeToggle is not null)
{
devModeToggle.IsChecked = devModeEnabled;
devModeToggle.IsChecked = IsDevModeEnabled;
}
UpdatePathDisplay(initialPath);
UpdatePathDisplay(_selectedHostPath);
}
private void InitializeComponents()
@@ -52,7 +67,13 @@ public partial class ErrorDebugWindow : Window
devModeToggle.IsCheckedChanged += (s, e) =>
{
IsDevModeEnabled = devModeToggle.IsChecked ?? false;
Console.WriteLine($"[ErrorDebugWindow] DevMode changed to: {IsDevModeEnabled}");
};
Console.WriteLine("[ErrorDebugWindow] DevModeToggle event bound");
}
else
{
Console.Error.WriteLine("[ErrorDebugWindow] Failed to find DevModeToggle!");
}
// 浏览按钮
@@ -60,6 +81,11 @@ public partial class ErrorDebugWindow : Window
if (browseButton is not null)
{
browseButton.Click += OnBrowseClick;
Console.WriteLine("[ErrorDebugWindow] BrowseButton event bound");
}
else
{
Console.Error.WriteLine("[ErrorDebugWindow] Failed to find BrowseButton!");
}
// 确定按钮
@@ -67,6 +93,11 @@ public partial class ErrorDebugWindow : Window
if (okButton is not null)
{
okButton.Click += (s, e) => Close();
Console.WriteLine("[ErrorDebugWindow] OkButton event bound");
}
else
{
Console.Error.WriteLine("[ErrorDebugWindow] Failed to find OkButton!");
}
// 取消按钮
@@ -78,9 +109,17 @@ public partial class ErrorDebugWindow : Window
// 取消时恢复原始状态
IsDevModeEnabled = false;
_selectedHostPath = null;
Console.WriteLine("[ErrorDebugWindow] Cancel clicked, resetting state");
Close();
};
Console.WriteLine("[ErrorDebugWindow] CancelButton event bound");
}
else
{
Console.Error.WriteLine("[ErrorDebugWindow] Failed to find CancelButton!");
}
Console.WriteLine("[ErrorDebugWindow] Components initialization completed");
}
/// <summary>
@@ -110,6 +149,7 @@ public partial class ErrorDebugWindow : Window
if (result.Count > 0)
{
_selectedHostPath = result[0].Path.LocalPath;
Console.WriteLine($"[ErrorDebugWindow] Selected host path: {_selectedHostPath}");
UpdatePathDisplay(_selectedHostPath);
}
}
@@ -124,5 +164,9 @@ public partial class ErrorDebugWindow : Window
{
pathTextBlock.Text = string.IsNullOrEmpty(path) ? "未选择" : path;
}
else
{
Console.Error.WriteLine("[ErrorDebugWindow] Failed to find PathTextBlock!");
}
}
}

View File

@@ -3,82 +3,94 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d"
d:DesignWidth="480"
d:DesignHeight="320"
d:DesignWidth="520"
d:DesignHeight="280"
x:Class="LanMountainDesktop.Launcher.Views.ErrorWindow"
x:DataType="views:ErrorWindow"
Title="阑山桌面 - 启动失败"
Width="480"
Height="320"
Title="阑山桌面"
Width="520"
Height="280"
CanResize="False"
WindowStartupLocation="CenterScreen"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
TransparencyLevelHint="None">
TransparencyLevelHint="None"
Icon="/Assets/logo.ico">
<Design.DataContext>
<views:ErrorWindow />
</Design.DataContext>
<Grid Margin="40" RowDefinitions="Auto,*,Auto">
<!-- 错误图标和标题 -->
<StackPanel Grid.Row="0" HorizontalAlignment="Center">
<!-- 错误图标 - 可点击进入调试模式(隐藏功能,无提示) -->
<!-- Fluent Design 风格对话框布局 -->
<Grid RowDefinitions="*,Auto">
<!-- 主内容区域:左侧图标 + 右侧文字 -->
<Grid Grid.Row="0" Margin="24,24,24,16" ColumnDefinitions="Auto,*">
<!-- 左侧:错误图标(可点击进入调试模式) -->
<Border x:Name="ErrorIconBorder"
Width="64"
Height="64"
Grid.Column="0"
Width="48"
Height="48"
Margin="0,4,16,0"
Background="{DynamicResource SystemFillColorCriticalBackgroundBrush}"
CornerRadius="32"
HorizontalAlignment="Center">
<TextBlock x:Name="ErrorIconText"
Text="!"
FontSize="36"
FontWeight="Bold"
CornerRadius="24"
VerticalAlignment="Top">
<TextBlock Text="&#xEA39;"
FontSize="24"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
VerticalAlignment="Center"/>
</Border>
<!-- 右侧:标题 + 内容 -->
<StackPanel Grid.Column="1" Spacing="8">
<!-- 标题 -->
<TextBlock x:Name="TitleText"
Text="启动失败"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextWrapping="Wrap"/>
<!-- 错误信息 -->
<TextBlock x:Name="ErrorMessageText"
Text="找不到阑山桌面应用程序。"
FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
LineHeight="20"/>
<!-- 建议信息 -->
<TextBlock x:Name="SuggestionText"
Text="请确保应用程序已正确安装,或尝试重新安装。"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap"
LineHeight="18"
Margin="0,4,0,0"/>
</StackPanel>
</Grid>
<TextBlock Text="启动失败"
FontSize="24"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Center"
Margin="0,20,0,0" />
</StackPanel>
<!-- 错误信息 -->
<StackPanel Grid.Row="1" VerticalAlignment="Center" Margin="0,20">
<TextBlock x:Name="ErrorMessageText"
Text="找不到阑山桌面应用程序。"
FontSize="14"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextWrapping="Wrap"
TextAlignment="Center"
LineHeight="22" />
<TextBlock Text="请确保应用程序已正确安装,或尝试重新安装。"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
TextAlignment="Center"
Margin="0,12,0,0"
LineHeight="20" />
</StackPanel>
<!-- 按钮区域 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Spacing="12">
<Button x:Name="RetryButton"
Content="重试"
Width="100"
Height="36"
FontSize="14"
Theme="{DynamicResource AccentButtonTheme}" />
<Button x:Name="ExitButton"
Content="退出"
Width="100"
Height="36"
FontSize="14" />
</StackPanel>
<!-- 底部:按钮区域 -->
<Border Grid.Row="1"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
Padding="24,16">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="8">
<Button x:Name="ExitButton"
Content="退出"
Width="80"
Height="32"
FontSize="13"/>
<Button x:Name="RetryButton"
Content="重试"
Width="80"
Height="32"
FontSize="13"
Theme="{DynamicResource AccentButtonTheme}"/>
</StackPanel>
</Border>
</Grid>
</Window>

View File

@@ -23,17 +23,44 @@ public partial class ErrorWindow : Window
// 先加载保存的状态
_devModeEnabled = LoadDevModeStateInternal();
_customHostPath = LoadCustomHostPathInternal();
// 延迟到窗口加载完成后再初始化组件,确保视觉树已准备好
this.Loaded += OnWindowLoaded;
this.Opened += OnWindowOpened;
}
/// <summary>
/// 窗口加载完成事件 - 视觉树已准备好
/// </summary>
private void OnWindowLoaded(object? sender, RoutedEventArgs e)
{
Console.WriteLine("[ErrorWindow] Window loaded, initializing components...");
InitializeComponents();
}
/// <summary>
/// 窗口打开事件
/// </summary>
private void OnWindowOpened(object? sender, EventArgs e)
{
Console.WriteLine("[ErrorWindow] Window opened and visible");
}
private void InitializeComponents()
{
Console.WriteLine("[ErrorWindow] Initializing components...");
// 错误图标点击事件(进入调试模式 - 隐藏功能)
var errorIconBorder = this.FindControl<Border>("ErrorIconBorder");
if (errorIconBorder is not null)
{
errorIconBorder.PointerPressed += OnErrorIconClick;
Console.WriteLine("[ErrorWindow] ErrorIconBorder event bound successfully");
}
else
{
Console.Error.WriteLine("[ErrorWindow] Failed to find ErrorIconBorder!");
}
// 按钮事件
@@ -43,12 +70,24 @@ public partial class ErrorWindow : Window
if (retryButton is not null)
{
retryButton.Click += OnRetryClick;
Console.WriteLine("[ErrorWindow] RetryButton event bound");
}
else
{
Console.Error.WriteLine("[ErrorWindow] Failed to find RetryButton!");
}
if (exitButton is not null)
{
exitButton.Click += OnExitClick;
Console.WriteLine("[ErrorWindow] ExitButton event bound");
}
else
{
Console.Error.WriteLine("[ErrorWindow] Failed to find ExitButton!");
}
Console.WriteLine("[ErrorWindow] Components initialization completed");
}
/// <summary>
@@ -63,6 +102,19 @@ public partial class ErrorWindow : Window
}
}
/// <summary>
/// 设置调试模式
/// </summary>
public void SetDebugMode(bool isDebugMode)
{
_isDebugMode = isDebugMode;
var titleText = this.FindControl<TextBlock>("TitleText");
if (titleText is not null && isDebugMode)
{
titleText.Text = "[调试模式] 错误页面";
}
}
/// <summary>
/// 获取用户选择的主程序路径
/// </summary>
@@ -114,13 +166,19 @@ public partial class ErrorWindow : Window
_devModeEnabled = debugWindow.IsDevModeEnabled;
_customHostPath = debugWindow.SelectedHostPath;
// 保存开发模式状态
// 保存开发模式状态和自定义路径
SaveDevModeStateInternal(_devModeEnabled);
SaveCustomHostPathInternal(_customHostPath);
// 如果启用了开发模式且没有选择路径,自动扫描
if (_devModeEnabled && string.IsNullOrEmpty(_customHostPath))
{
ScanDevPaths();
// 扫描到路径后也保存
if (!string.IsNullOrEmpty(_customHostPath))
{
SaveCustomHostPathInternal(_customHostPath);
}
}
};
@@ -203,6 +261,71 @@ public partial class ErrorWindow : Window
return Path.Combine(appData, "LanMountainDesktop", ".launcher", "devmode.config");
}
/// <summary>
/// 保存自定义主程序路径(内部方法)
/// </summary>
private static void SaveCustomHostPathInternal(string? path)
{
try
{
var hostPathFile = GetCustomHostPathFilePath();
var dir = Path.GetDirectoryName(hostPathFile);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(hostPathFile, path ?? string.Empty);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to save custom host path: {ex.Message}");
}
}
/// <summary>
/// 加载自定义主程序路径(内部方法)
/// </summary>
private static string? LoadCustomHostPathInternal()
{
try
{
var hostPathFile = GetCustomHostPathFilePath();
if (File.Exists(hostPathFile))
{
var content = File.ReadAllText(hostPathFile).Trim();
// 验证路径是否仍然有效
if (!string.IsNullOrEmpty(content) && File.Exists(content))
{
return content;
}
// 路径已失效,清理配置文件
try
{
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}");
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to load custom host path: {ex.Message}");
}
return null;
}
/// <summary>
/// 获取自定义主程序路径文件路径
/// </summary>
private static string GetCustomHostPathFilePath()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(appData, "LanMountainDesktop", ".launcher", "custom-host-path.config");
}
/// <summary>
/// 检查是否启用了开发模式(静态方法,启动时调用)
/// </summary>
@@ -211,6 +334,14 @@ public partial class ErrorWindow : Window
return LoadDevModeStateInternal();
}
/// <summary>
/// 获取保存的自定义主程序路径(静态方法,启动时调用)
/// </summary>
public static string? GetSavedCustomHostPath()
{
return LoadCustomHostPathInternal();
}
private void OnRetryClick(object? sender, RoutedEventArgs e)
{
_completionSource.TrySetResult(ErrorWindowResult.Retry);

View File

@@ -5,51 +5,72 @@
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="260"
d:DesignWidth="600"
d:DesignHeight="500"
x:Class="LanMountainDesktop.Launcher.Views.OobeWindow"
x:DataType="views:OobeWindow"
Title="欢迎使用阑山桌面"
Width="420"
Height="260"
Width="600"
Height="500"
CanResize="False"
WindowStartupLocation="CenterScreen"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
TransparencyLevelHint="None">
TransparencyLevelHint="None"
Icon="/Assets/logo.ico">
<Design.DataContext>
<views:OobeWindow />
</Design.DataContext>
<Grid Margin="32" RowDefinitions="*,Auto">
<!-- 欢迎文本 -->
<StackPanel Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="欢迎使用"
FontSize="18"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center" />
<TextBlock Text="阑山桌面"
FontSize="32"
FontWeight="Light"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Center"
Margin="0,8,0,0" />
<TextBlock Text="您的智能桌面助手"
FontSize="14"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
HorizontalAlignment="Center"
Margin="0,16,0,0" />
</StackPanel>
<Grid x:Name="ContentGrid">
<!-- 主内容区域 -->
<Grid Margin="48" RowDefinitions="*,Auto">
<!-- 中央内容区域 -->
<StackPanel Grid.Row="0"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Spacing="24">
<!-- 顶部:完成状态勾号图标 -->
<Border Width="80"
Height="80"
Background="{DynamicResource SystemFillColorSuccessBackgroundBrush}"
CornerRadius="40"
HorizontalAlignment="Center">
<ui:SymbolIcon Symbol="Accept"
FontSize="40"
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<!-- 中央:欢迎文字 -->
<StackPanel Spacing="8" HorizontalAlignment="Center">
<TextBlock Text="欢迎使用阑山桌面"
FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Center" />
<TextBlock Text="你的桌面,不止一面"
FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center" />
</StackPanel>
</StackPanel>
<!-- 进入按钮 -->
<Button Grid.Row="1"
x:Name="EnterButton"
HorizontalAlignment="Right"
Width="80"
Height="36"
Content="开始使用"
FontSize="14"
Background="{DynamicResource AccentFillColorDefaultBrush}"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
CornerRadius="4" />
<!-- 底部:圆形开始按钮 -->
<Button Grid.Row="1"
x:Name="EnterButton"
HorizontalAlignment="Center"
Width="56"
Height="56"
Margin="0,0,0,16"
Theme="{DynamicResource AccentButtonTheme}"
CornerRadius="28">
<ui:SymbolIcon Symbol="Forward"
FontSize="24"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"/>
</Button>
</Grid>
</Grid>
</Window>

View File

@@ -1,24 +1,123 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Styling;
namespace LanMountainDesktop.Launcher.Views;
/// <summary>
/// OOBE首次使用体验窗口
/// OOBE首次使用体验窗口 - 欢迎页面
/// </summary>
public partial class OobeWindow : Window
{
private readonly TaskCompletionSource<bool> _completionSource = new();
private bool _isTransitioning = false;
public OobeWindow()
{
AvaloniaXamlLoader.Load(this);
// 延迟到窗口加载完成后再初始化
this.Loaded += OnWindowLoaded;
this.Opened += OnWindowOpened;
}
/// <summary>
/// 窗口加载完成事件
/// </summary>
private void OnWindowLoaded(object? sender, RoutedEventArgs e)
{
Console.WriteLine("[OobeWindow] Window loaded, initializing components...");
var enterButton = this.FindControl<Button>("EnterButton");
if (enterButton is not null)
{
enterButton.Click += OnEnterClick;
Console.WriteLine("[OobeWindow] EnterButton event bound successfully");
}
else
{
Console.Error.WriteLine("[OobeWindow] Failed to find EnterButton!");
}
}
/// <summary>
/// 窗口打开事件 - 播放入场动画
/// </summary>
private async void OnWindowOpened(object? sender, EventArgs e)
{
Console.WriteLine("[OobeWindow] Window opened, playing entrance animation...");
await PlayEntranceAnimationAsync();
}
/// <summary>
/// 播放入场动画
/// </summary>
private async Task PlayEntranceAnimationAsync()
{
try
{
// 获取内容元素
var contentGrid = this.FindControl<Grid>("ContentGrid");
if (contentGrid is null)
{
// 如果没有命名网格,直接返回
return;
}
// 创建淡入动画
var fadeInAnimation = new Animation
{
Duration = TimeSpan.FromMilliseconds(600),
Easing = new CubicEaseOut(),
Children =
{
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 0.0) },
KeyTime = TimeSpan.FromMilliseconds(0)
},
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 1.0) },
KeyTime = TimeSpan.FromMilliseconds(600)
}
}
};
// 创建向上滑动动画
var slideUpAnimation = new Animation
{
Duration = TimeSpan.FromMilliseconds(600),
Easing = new CubicEaseOut(),
Children =
{
new KeyFrame
{
Setters = { new Setter(TranslateTransform.YProperty, 30.0) },
KeyTime = TimeSpan.FromMilliseconds(0)
},
new KeyFrame
{
Setters = { new Setter(TranslateTransform.YProperty, 0.0) },
KeyTime = TimeSpan.FromMilliseconds(600)
}
}
};
// 应用动画
await fadeInAnimation.RunAsync(contentGrid);
await slideUpAnimation.RunAsync(contentGrid);
Console.WriteLine("[OobeWindow] Entrance animation completed");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OobeWindow] Error playing entrance animation: {ex.Message}");
}
}
@@ -27,8 +126,72 @@ public partial class OobeWindow : Window
/// </summary>
public Task WaitForEnterAsync() => _completionSource.Task;
private void OnEnterClick(object? sender, RoutedEventArgs e)
/// <summary>
/// 进入按钮点击事件
/// </summary>
private async void OnEnterClick(object? sender, RoutedEventArgs e)
{
_completionSource.TrySetResult(true);
if (_isTransitioning) return;
_isTransitioning = true;
Console.WriteLine("[OobeWindow] Enter button clicked, starting transition...");
try
{
// 播放退出动画
await PlayExitAnimationAsync();
// 完成 OOBE
_completionSource.TrySetResult(true);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OobeWindow] Error during transition: {ex.Message}");
_completionSource.TrySetResult(true);
}
}
/// <summary>
/// 播放退出动画
/// </summary>
private async Task PlayExitAnimationAsync()
{
try
{
var contentGrid = this.FindControl<Grid>("ContentGrid");
if (contentGrid is null)
{
// 如果没有命名网格,直接延迟后返回
await Task.Delay(200);
return;
}
// 创建淡出动画
var fadeOutAnimation = new Animation
{
Duration = TimeSpan.FromMilliseconds(200),
Easing = new CubicEaseIn(),
Children =
{
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 1.0) },
KeyTime = TimeSpan.FromMilliseconds(0)
},
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 0.0) },
KeyTime = TimeSpan.FromMilliseconds(200)
}
}
};
await fadeOutAnimation.RunAsync(contentGrid);
Console.WriteLine("[OobeWindow] Exit animation completed");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OobeWindow] Error playing exit animation: {ex.Message}");
}
}
}

View File

@@ -5,53 +5,83 @@
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="200"
d:DesignWidth="480"
d:DesignHeight="320"
x:Class="LanMountainDesktop.Launcher.Views.SplashWindow"
x:DataType="views:SplashWindow"
Title="阑山桌面"
Width="400"
Height="200"
Title="LanMountain Desktop"
Width="480"
Height="320"
CanResize="False"
WindowStartupLocation="CenterScreen"
SystemDecorations="None"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
TransparencyLevelHint="None">
TransparencyLevelHint="None"
Icon="/Assets/logo.ico">
<Design.DataContext>
<views:SplashWindow />
</Design.DataContext>
<Grid RowDefinitions="*,Auto,Auto">
<!-- 应用名称 -->
<Grid>
<!-- 左上角:应用名称 -->
<TextBlock x:Name="AppNameText"
Text="阑山桌面"
FontSize="36"
FontWeight="Light"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Grid.Row="0"
Text="LanMountain Desktop"
FontSize="24"
FontWeight="SemiBold"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="24,24,0,0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<!-- 进度条 -->
<ProgressBar x:Name="ProgressIndicator"
Grid.Row="1"
Minimum="0"
Maximum="100"
Value="0"
Height="3"
Width="200"
Margin="0,20,0,0"
IsIndeterminate="True"
Foreground="{DynamicResource AccentFillColorDefaultBrush}"
Background="{DynamicResource ControlStrokeColorDefaultBrush}" />
<!-- 状态文本 -->
<TextBlock x:Name="StatusText"
Grid.Row="2"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center"
Margin="0,12,0,24"
Text="正在启动..." />
<!-- 底部区域:进度条和状态 -->
<Grid VerticalAlignment="Bottom" Margin="24,0,24,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 第一行:左下角版本信息,右下角阶段文字 -->
<Grid Grid.Row="0" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 左下角:版本和开发代号 - 可点击打开开发者界面(隐藏功能) -->
<Border x:Name="VersionTextBorder"
Grid.Column="0"
Background="Transparent"
Cursor="Hand"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<TextBlock x:Name="VersionText"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Opacity="0.8"
Text="1.0.0 (Administrate)" />
</Border>
<!-- 右下角:阶段文字 -->
<TextBlock x:Name="StatusText"
Grid.Column="1"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Opacity="0.8"
Text="Initializing..." />
</Grid>
<!-- 底部:进度条 -->
<ProgressBar x:Name="ProgressIndicator"
Grid.Row="1"
Minimum="0"
Maximum="100"
Value="0"
Height="4"
IsIndeterminate="False"
Foreground="{DynamicResource AccentFillColorDefaultBrush}"
Background="{DynamicResource ControlStrokeColorDefaultBrush}" />
</Grid>
</Grid>
</Window>

View File

@@ -1,4 +1,6 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LanMountainDesktop.Launcher.Services;
@@ -10,9 +12,89 @@ namespace LanMountainDesktop.Launcher.Views;
/// </summary>
public partial class SplashWindow : Window, ISplashStageReporter
{
private int _versionTextClickCount = 0;
private const int DebugModeClickThreshold = 5;
private bool _isDebugModeOpened = false;
public SplashWindow()
{
AvaloniaXamlLoader.Load(this);
// 延迟到窗口加载完成后再绑定事件
this.Loaded += OnWindowLoaded;
}
/// <summary>
/// 窗口加载完成事件
/// </summary>
private void OnWindowLoaded(object? sender, RoutedEventArgs e)
{
Console.WriteLine("[SplashWindow] Window loaded, binding events...");
// 绑定版本文本点击事件隐藏功能点击5次打开开发者界面
var versionTextBorder = this.FindControl<Border>("VersionTextBorder");
if (versionTextBorder is not null)
{
versionTextBorder.PointerPressed += OnVersionTextClick;
Console.WriteLine("[SplashWindow] VersionTextBorder click event bound");
}
else
{
Console.Error.WriteLine("[SplashWindow] Failed to find VersionTextBorder!");
}
}
/// <summary>
/// 版本文本点击事件 - 连续点击5次打开开发者界面隐藏功能
/// </summary>
private void OnVersionTextClick(object? sender, PointerPressedEventArgs e)
{
if (_isDebugModeOpened) return;
_versionTextClickCount++;
Console.WriteLine($"[SplashWindow] Version text clicked {_versionTextClickCount}/{DebugModeClickThreshold}");
if (_versionTextClickCount >= DebugModeClickThreshold)
{
OpenDebugWindow();
}
}
/// <summary>
/// 打开开发者调试窗口
/// </summary>
private async void OpenDebugWindow()
{
_isDebugModeOpened = true;
Console.WriteLine("[SplashWindow] Opening debug window...");
try
{
// 加载保存的状态
var devModeEnabled = ErrorWindow.CheckDevModeEnabled();
var customHostPath = ErrorWindow.GetSavedCustomHostPath();
var debugWindow = new ErrorDebugWindow(devModeEnabled, customHostPath)
{
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
// 订阅窗口关闭事件以保存状态
debugWindow.Closed += (s, e) =>
{
Console.WriteLine("[SplashWindow] Debug window closed");
_isDebugModeOpened = false;
_versionTextClickCount = 0;
};
await debugWindow.ShowDialog(this);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[SplashWindow] Error opening debug window: {ex.Message}");
_isDebugModeOpened = false;
_versionTextClickCount = 0;
}
}
/// <summary>
@@ -22,8 +104,14 @@ public partial class SplashWindow : Window, ISplashStageReporter
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.GetControl<TextBlock>("StatusText");
var progressIndicator = this.GetControl<ProgressBar>("ProgressIndicator");
var statusText = this.FindControl<TextBlock>("StatusText");
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
if (statusText is null || progressIndicator is null)
{
Console.Error.WriteLine($"[SplashWindow] Controls not found: StatusText={statusText != null}, ProgressIndicator={progressIndicator != null}");
return;
}
// 更新状态文本
statusText.Text = message;
@@ -49,8 +137,14 @@ public partial class SplashWindow : Window, ISplashStageReporter
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.GetControl<TextBlock>("StatusText");
var progressIndicator = this.GetControl<ProgressBar>("ProgressIndicator");
var statusText = this.FindControl<TextBlock>("StatusText");
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
if (statusText is null || progressIndicator is null)
{
Console.Error.WriteLine($"[SplashWindow] Controls not found in UpdateProgress");
return;
}
if (!string.IsNullOrEmpty(message))
{
@@ -69,11 +163,75 @@ public partial class SplashWindow : Window, ISplashStageReporter
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.GetControl<TextBlock>("StatusText");
var statusText = this.FindControl<TextBlock>("StatusText");
if (statusText is null)
{
Console.Error.WriteLine($"[SplashWindow] StatusText not found in UpdateStatus");
return;
}
statusText.Text = message;
});
}
/// <summary>
/// 报告阶段和进度0-100
/// </summary>
public void ReportStage(string stage, int progress)
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.FindControl<TextBlock>("StatusText");
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
if (statusText is null || progressIndicator is null)
{
Console.Error.WriteLine($"[SplashWindow] Controls not found in ReportStage");
return;
}
statusText.Text = stage;
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = Math.Clamp(progress, 0, 100);
});
}
/// <summary>
/// 设置版本和开发代号
/// </summary>
public void SetVersionInfo(string version, string codename)
{
Dispatcher.UIThread.Post(() =>
{
var versionText = this.FindControl<TextBlock>("VersionText");
if (versionText is null)
{
Console.Error.WriteLine($"[SplashWindow] VersionText not found in SetVersionInfo");
return;
}
versionText.Text = $"{version} ({codename})";
});
}
/// <summary>
/// 设置调试模式
/// </summary>
public void SetDebugMode(bool isDebugMode)
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.FindControl<TextBlock>("StatusText");
if (statusText is null)
{
Console.Error.WriteLine($"[SplashWindow] StatusText not found in SetDebugMode");
return;
}
if (isDebugMode)
{
statusText.Text = "[Debug Mode] Splash Preview";
}
});
}
/// <summary>
/// 根据阶段名称解析进度值
/// </summary>

View File

@@ -0,0 +1,68 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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"
x:Class="LanMountainDesktop.Launcher.Views.UpdateWindow"
x:DataType="views:UpdateWindow"
Title="阑山桌面 - 更新"
Width="400"
Height="220"
CanResize="False"
WindowStartupLocation="CenterScreen"
SystemDecorations="None"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
TransparencyLevelHint="None"
Icon="/Assets/logo.ico">
<Design.DataContext>
<views:UpdateWindow />
</Design.DataContext>
<Grid RowDefinitions="Auto,*,Auto,Auto">
<!-- 应用名称 -->
<TextBlock x:Name="TitleText"
Text="阑山桌面"
FontSize="36"
FontWeight="Light"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Grid.Row="0"
Margin="0,30,0,0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<!-- 状态文本 -->
<TextBlock x:Name="StatusText"
Grid.Row="1"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,16,0,0"
Text="正在更新,请稍候..." />
<!-- 进度条 -->
<ProgressBar x:Name="ProgressIndicator"
Grid.Row="2"
Minimum="0"
Maximum="100"
Value="0"
Height="3"
Width="200"
Margin="0,16,0,0"
IsIndeterminate="True"
Foreground="{DynamicResource AccentFillColorDefaultBrush}"
Background="{DynamicResource ControlStrokeColorDefaultBrush}" />
<!-- 底部提示 -->
<TextBlock x:Name="DetailText"
Grid.Row="3"
FontSize="11"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
HorizontalAlignment="Center"
Margin="0,12,0,24"
Text="" />
</Grid>
</Window>

View File

@@ -0,0 +1,117 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace LanMountainDesktop.Launcher.Views;
/// <summary>
/// 更新进度窗口 - 用于 apply-update 命令模式下显示更新/插件升级进度
/// </summary>
public partial class UpdateWindow : Window
{
public UpdateWindow()
{
AvaloniaXamlLoader.Load(this);
}
/// <summary>
/// 更新状态和进度
/// </summary>
public void Report(string stage, string message, int progressPercent = -1)
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.FindControl<TextBlock>("StatusText");
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
var detailText = this.FindControl<TextBlock>("DetailText");
if (statusText is null || progressIndicator is null || detailText is null)
{
Console.Error.WriteLine($"[UpdateWindow] Controls not found in Report: StatusText={statusText != null}, ProgressIndicator={progressIndicator != null}, DetailText={detailText != null}");
return;
}
statusText.Text = message;
if (progressPercent >= 0)
{
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = progressPercent;
}
else
{
progressIndicator.IsIndeterminate = true;
}
// 根据阶段显示不同的底部提示
detailText.Text = stage.ToLowerInvariant() switch
{
"verify" => "正在验证更新完整性...",
"extract" => "正在解压更新包...",
"apply" => "正在应用更新文件...",
"plugins" => "正在升级插件...",
"cleanup" => "正在清理...",
"done" => "",
_ => ""
};
});
}
/// <summary>
/// 显示更新完成状态
/// </summary>
public void ReportComplete(bool success, string? errorMessage = null)
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.FindControl<TextBlock>("StatusText");
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
var detailText = this.FindControl<TextBlock>("DetailText");
var titleText = this.FindControl<TextBlock>("TitleText");
if (statusText is null || progressIndicator is null || detailText is null || titleText is null)
{
Console.Error.WriteLine($"[UpdateWindow] Controls not found in ReportComplete");
return;
}
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = 100;
detailText.Text = "";
if (success)
{
statusText.Text = "更新完成";
}
else
{
titleText.Text = "更新失败";
statusText.Text = errorMessage ?? "更新过程中发生错误";
}
});
}
/// <summary>
/// 设置调试模式
/// </summary>
public void SetDebugMode(bool isDebugMode)
{
Dispatcher.UIThread.Post(() =>
{
var statusText = this.FindControl<TextBlock>("StatusText");
var titleText = this.FindControl<TextBlock>("TitleText");
if (statusText is null || titleText is null)
{
Console.Error.WriteLine($"[UpdateWindow] Controls not found in SetDebugMode");
return;
}
if (isDebugMode)
{
titleText.Text = "[调试模式] 更新页面";
statusText.Text = "预览更新进度界面";
}
});
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="LanMountainDesktop.Launcher"/>
<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">
<application>
<!-- Windows 10/11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
</application>
</compatibility>
</assembly>