changed.调整了启动逻辑,优化了更新页面。

This commit is contained in:
lincube
2026-04-17 22:33:41 +08:00
parent 9efa43d92b
commit 9283da5940
11 changed files with 838 additions and 234 deletions

View File

@@ -13,6 +13,10 @@ public partial class App : Application
{
public override void Initialize()
{
// 初始化日志记录器
Logger.Initialize();
Logger.Info("Launcher starting...");
AvaloniaXamlLoader.Load(this);
}
@@ -50,27 +54,12 @@ public partial class App : Application
}
else
{
// 正常启动流程
var appRoot = Commands.ResolveAppRoot(context);
var deploymentLocator = new DeploymentLocator(appRoot);
// TODO: 从配置读取 GitHub 仓库信息
var updateCheckService = new UpdateCheckService("ClassIsland", "LanMountainDesktop");
var coordinator = new LauncherFlowCoordinator(
context,
deploymentLocator,
new OobeStateService(appRoot),
new UpdateEngineService(deploymentLocator),
updateCheckService,
new PluginInstallerService());
// 先显示 Splash 窗口,确保应用程序不会立即退出
var splashWindow = new SplashWindow();
splashWindow.Show();
// 启动协调器流程
_ = RunCoordinatorWithSplashAsync(desktop, coordinator, splashWindow);
// 在 try-catch 块中实例化所有服务,确保任何异常都能被捕获
_ = RunCoordinatorWithSplashAsync(desktop, context, splashWindow);
}
}
@@ -211,14 +200,30 @@ public partial class App : Application
private static async Task RunCoordinatorWithSplashAsync(
IClassicDesktopStyleApplicationLifetime desktop,
LauncherFlowCoordinator coordinator,
CommandContext context,
SplashWindow splashWindow)
{
LauncherResult result;
ErrorWindow? errorWindow = null;
LauncherFlowCoordinator? coordinator = null;
try
{
// 在 try-catch 块中实例化所有服务,确保异常被捕获
var appRoot = Commands.ResolveAppRoot(context);
var deploymentLocator = new DeploymentLocator(appRoot);
// TODO: 从配置读取 GitHub 仓库信息
var updateCheckService = new UpdateCheckService("ClassIsland", "LanMountainDesktop");
coordinator = new LauncherFlowCoordinator(
context,
deploymentLocator,
new OobeStateService(appRoot),
new UpdateEngineService(deploymentLocator),
updateCheckService,
new PluginInstallerService());
result = await coordinator.RunAsync(splashWindow).ConfigureAwait(false);
}
catch (Exception ex)
@@ -344,11 +349,11 @@ public partial class App : Application
}
}
// 3. 清理旧版本
// 3. 清理旧版本保留至少3个版本以支持回滚
if (success)
{
await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "正在清理...", 90));
deploymentLocator.CleanupDestroyedDeployments();
deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
}
}
catch (Exception ex)

View File

@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text.Json;
using LanMountainDesktop.Launcher.Models;
using LanMountainDesktop.Shared.Contracts.Launcher;
namespace LanMountainDesktop.Launcher.Services;
@@ -17,44 +18,65 @@ internal sealed class DeploymentLocator
public string? FindCurrentDeploymentDirectory()
{
var candidates = Directory.Exists(_appRoot)
? Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly)
: [];
Console.WriteLine("[DeploymentLocator] Searching for deployment directories (ClassIsland style)...");
// 过滤掉无效的部署目录
var validCandidates = candidates
.Where(path =>
!File.Exists(Path.Combine(path, ".destroy")) && // 排除待删除
!File.Exists(Path.Combine(path, ".partial"))) // 排除未完成
.ToList();
// 优先选择带 .current 标记的版本
var withMarkers = validCandidates
.Where(path => File.Exists(Path.Combine(path, ".current")))
.Select(path => new
{
Path = path,
Version = ParseVersionFromDirectory(path)
})
.OrderByDescending(item => item.Version)
.ToList();
if (withMarkers.Count > 0)
if (!Directory.Exists(_appRoot))
{
return withMarkers[0].Path;
Console.WriteLine("[DeploymentLocator] App root directory does not exist");
return null;
}
// 如果没有 .current 标记,选择最新版本
var byVersion = validCandidates
.Select(path => new
{
Path = path,
Version = ParseVersionFromDirectory(path)
})
.OrderByDescending(item => item.Version)
.ToList();
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
return byVersion.Count > 0 ? byVersion[0].Path : null;
try
{
var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly);
Console.WriteLine($"[DeploymentLocator] Found {candidates.Length} app-* directories");
// ClassIsland 风格的查询:先筛选,后排序
var validInstallations = candidates
.Where(path =>
{
var hasDestroy = File.Exists(Path.Combine(path, ".destroy"));
var hasPartial = File.Exists(Path.Combine(path, ".partial"));
var hasExe = File.Exists(Path.Combine(path, executable));
var hasCurrent = File.Exists(Path.Combine(path, ".current"));
var version = ParseVersionFromDirectory(path);
Console.WriteLine($"[DeploymentLocator] Candidate: {Path.GetFileName(path)} | " +
$"Version={version} | " +
$"Current={hasCurrent} | " +
$"Destroy={hasDestroy} | " +
$"Partial={hasPartial} | " +
$"HasExe={hasExe}");
return !hasDestroy && !hasPartial && hasExe;
})
.Select(path => new
{
Path = path,
Version = ParseVersionFromDirectory(path),
HasCurrentMarker = File.Exists(Path.Combine(path, ".current"))
})
.OrderBy(x => x.HasCurrentMarker ? 0 : 1) // .current 标记的排前面
.ThenByDescending(x => x.Version) // 然后按版本号降序
.ToList();
if (validInstallations.Count == 0)
{
Console.WriteLine("[DeploymentLocator] No valid deployment directories found");
return null;
}
var best = validInstallations[0];
Console.WriteLine($"[DeploymentLocator] Selected: {Path.GetFileName(best.Path)} (current={best.HasCurrentMarker}, version={best.Version})");
return best.Path;
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DeploymentLocator] Error searching for deployments: {ex}");
return null;
}
}
public string? ResolveHostExecutablePath()
@@ -233,35 +255,159 @@ internal sealed class DeploymentLocator
}
}
public void CleanupDestroyedDeployments()
/// <summary>
/// 清理旧版本部署保留最近的N个版本
/// </summary>
/// <param name="minVersionsToKeep">最少保留版本数默认3个</param>
public void CleanupOldDeployments(int minVersionsToKeep = 3)
{
try
{
var candidates = Directory.Exists(_appRoot)
? Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly)
: [];
Console.WriteLine($"[DeploymentLocator] Starting cleanup with retention policy: keep at least {minVersionsToKeep} versions");
var destroyedDirs = candidates
.Where(path => File.Exists(Path.Combine(path, ".destroy")));
if (!Directory.Exists(_appRoot))
{
return;
}
foreach (var dir in destroyedDirs)
var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly);
// 过滤掉无效部署目录排除partial按版本排序
var validDeployments = candidates
.Where(path => !File.Exists(Path.Combine(path, ".partial")))
.Select(path => new
{
Path = path,
Version = ParseVersionFromDirectory(path),
IsDestroyed = File.Exists(Path.Combine(path, ".destroy")),
IsCurrent = File.Exists(Path.Combine(path, ".current"))
})
.OrderByDescending(item => item.Version)
.ToList();
Console.WriteLine($"[DeploymentLocator] Found {validDeployments.Count} valid deployments");
// 确定要保留的版本
var versionsToKeep = new HashSet<string>();
// 1. 总是保留当前版本
var currentVersion = validDeployments.FirstOrDefault(d => d.IsCurrent);
if (currentVersion != null)
{
versionsToKeep.Add(currentVersion.Path);
Console.WriteLine($"[DeploymentLocator] Keep current version: {currentVersion.Path}");
}
// 2. 保留最近的N个有效版本不包括已标记destroy的
var activeVersions = validDeployments
.Where(d => !d.IsDestroyed)
.Take(minVersionsToKeep)
.ToList();
foreach (var ver in activeVersions)
{
versionsToKeep.Add(ver.Path);
Console.WriteLine($"[DeploymentLocator] Keep recent version: {ver.Path}");
}
// 3. 保留有快照的版本(用于回滚)
var snapshotDir = Path.Combine(_appRoot, ".launcher", "snapshots");
if (Directory.Exists(snapshotDir))
{
try
{
Directory.Delete(dir, recursive: true);
var snapshotFiles = Directory.GetFiles(snapshotDir, "*.json", SearchOption.TopDirectoryOnly);
foreach (var snapshotFile in snapshotFiles)
{
try
{
var json = File.ReadAllText(snapshotFile);
var snapshot = System.Text.Json.JsonSerializer.Deserialize<SnapshotMetadata>(json);
if (snapshot != null && !string.IsNullOrEmpty(snapshot.SourceDirectory))
{
if (Directory.Exists(snapshot.SourceDirectory))
{
versionsToKeep.Add(snapshot.SourceDirectory);
Console.WriteLine($"[DeploymentLocator] Keep version for rollback: {snapshot.SourceDirectory}");
}
}
}
catch
{
// 忽略快照解析错误
}
}
}
catch
{
// 忽略快照目录访问错误
}
}
// 清理不需要的版本
foreach (var deployment in validDeployments)
{
if (versionsToKeep.Contains(deployment.Path))
{
// 保留此版本如果之前标记了destroy则取消标记
if (deployment.IsDestroyed)
{
try
{
File.Delete(Path.Combine(deployment.Path, ".destroy"));
Console.WriteLine($"[DeploymentLocator] Unmarked for deletion (kept): {deployment.Path}");
}
catch
{
// 忽略取消标记失败
}
}
continue;
}
// 如果还没标记destroy的先标记
if (!deployment.IsDestroyed)
{
try
{
File.WriteAllText(Path.Combine(deployment.Path, ".destroy"), string.Empty);
Console.WriteLine($"[DeploymentLocator] Marked for deletion: {deployment.Path}");
}
catch
{
// 忽略标记失败
}
}
// 尝试删除
try
{
Directory.Delete(deployment.Path, recursive: true);
Console.WriteLine($"[DeploymentLocator] Deleted: {deployment.Path}");
}
catch
{
// 忽略删除失败(可能文件被占用),下次启动再试
Console.WriteLine($"[DeploymentLocator] Failed to delete (will retry later): {deployment.Path}");
}
}
}
catch
catch (Exception ex)
{
Console.Error.WriteLine($"[DeploymentLocator] Cleanup failed: {ex.Message}");
// 忽略清理失败
}
}
/// <summary>
/// 仅清理已标记为.destroy的部署兼容旧方法
/// </summary>
[Obsolete("Use CleanupOldDeployments instead")]
public void CleanupDestroyedDeployments()
{
CleanupOldDeployments(3);
}
public static Version ParseVersionFromDirectory(string path)
{
var text = ParseVersionTextFromDirectory(path);

View File

@@ -4,50 +4,67 @@ 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()
internal sealed class FlexibleHostLocator
{
var executable = GetExecutableName();
var searchContext = new SearchContext
{
ExecutableName = executable,
AppRoot = _appRoot,
Options = _options
};
private readonly HostDiscoveryOptions _options;
private readonly string _appRoot;
private readonly DeploymentLocator _deploymentLocator;
// ========== 第一阶段:标准路径查找(快速路径)==========
// 1. 检查环境变量指定的路径(最高优先级 - 用于调试和特殊场景)
var envPath = GetPathFromEnvironment();
if (!string.IsNullOrWhiteSpace(envPath))
public FlexibleHostLocator(string appRoot, HostDiscoveryOptions? options = null)
{
var validated = ValidateAndReturn(envPath, "environment variable");
if (validated != null) return validated;
_appRoot = appRoot;
_options = options ?? new HostDiscoveryOptions();
_deploymentLocator = new DeploymentLocator(appRoot);
}
// 2. 搜索部署目录app-*- 生产环境标准路径
var deploymentPath = SearchDeploymentDirectories(searchContext);
if (!string.IsNullOrWhiteSpace(deploymentPath))
/// <summary>
/// 解析主程序可执行文件路径
/// </summary>
public string? ResolveHostExecutablePath()
{
return deploymentPath;
}
var executable = GetExecutableName();
var searchContext = new SearchContext
{
ExecutableName = executable,
AppRoot = _appRoot,
Options = _options
};
// 3. 检查 Launcher 同级目录(便携模式)
// ========== 第一阶段:标准路径查找(快速路径)==========
// 1. 检查环境变量指定的路径(最高优先级 - 用于调试和特殊场景)
var envPath = GetPathFromEnvironment();
if (!string.IsNullOrWhiteSpace(envPath))
{
var validated = ValidateAndReturn(envPath, "environment variable");
if (validated != null) return validated;
}
// 2. 使用 DeploymentLocatorClassIsland 风格的简洁查询 - 优先)
Console.WriteLine("[FlexibleHostLocator] Trying quick path: DeploymentLocator.FindCurrentDeploymentDirectory()");
var deploymentDir = _deploymentLocator.FindCurrentDeploymentDirectory();
if (!string.IsNullOrWhiteSpace(deploymentDir))
{
var deploymentExePath = Path.Combine(deploymentDir, executable);
if (File.Exists(deploymentExePath))
{
Console.WriteLine($"[FlexibleHostLocator] Quick path found: {deploymentExePath}");
return deploymentExePath;
}
Console.WriteLine($"[FlexibleHostLocator] Quick path found dir but no exe: {deploymentExePath}");
}
// 3. 快速路径失败,尝试旧的 SearchDeploymentDirectories 作为 fallback
Console.WriteLine("[FlexibleHostLocator] Quick path failed, falling back to SearchDeploymentDirectories");
var deploymentPath = SearchDeploymentDirectories(searchContext);
if (!string.IsNullOrWhiteSpace(deploymentPath))
{
return deploymentPath;
}
// 4. 检查 Launcher 同级目录(便携模式)
var portablePath = SearchPortableLocation(searchContext);
if (!string.IsNullOrWhiteSpace(portablePath))
{
@@ -56,7 +73,7 @@ internal sealed class FlexibleHostLocator
// ========== 第二阶段:灵活查找(标准路径找不到时)==========
// 4. 检查配置文件中的路径 - 用户自定义配置
// 5. 检查配置文件中的路径 - 用户自定义配置
var configPath = GetPathFromConfigFile();
if (!string.IsNullOrWhiteSpace(configPath))
{
@@ -71,7 +88,7 @@ internal sealed class FlexibleHostLocator
return nearbyPath;
}
// 6. 开发模式:检查保存的自定义路径
// 7. 开发模式:检查保存的自定义路径
if (_options.PreferDevModeConfig && Views.ErrorWindow.CheckDevModeEnabled())
{
var savedPath = Views.ErrorWindow.GetSavedCustomHostPath();
@@ -82,21 +99,21 @@ internal sealed class FlexibleHostLocator
}
}
// 7. 搜索标准开发路径
// 8. 搜索标准开发路径
var devPath = SearchDevelopmentPaths(searchContext);
if (!string.IsNullOrWhiteSpace(devPath))
{
return devPath;
}
// 8. 搜索额外的配置路径
// 9. 搜索额外的配置路径
var additionalPath = SearchAdditionalPaths(searchContext);
if (!string.IsNullOrWhiteSpace(additionalPath))
{
return additionalPath;
}
// 9. 递归搜索(如果启用)
// 10. 递归搜索(如果启用)
if (_options.RecursiveSearch)
{
var recursivePath = SearchRecursively(searchContext);

View File

@@ -38,8 +38,8 @@ internal sealed class LauncherFlowCoordinator
{
try
{
// 清理待删除的旧版本
_deploymentLocator.CleanupDestroyedDeployments();
// 清理旧版本保留至少3个版本
_deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
// 使用传入的 Splash 窗口或创建新的
var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() =>

View File

@@ -0,0 +1,138 @@
using System.Text;
namespace LanMountainDesktop.Launcher.Services;
/// <summary>
/// 简单的日志记录器 - 同时输出到控制台和文件
/// </summary>
internal static class Logger
{
private static readonly object _lock = new();
private static string? _logFilePath;
private static bool _initialized;
/// <summary>
/// 初始化日志记录器
/// </summary>
public static void Initialize()
{
if (_initialized)
{
return;
}
try
{
var logDir = GetLogDirectory();
if (!string.IsNullOrEmpty(logDir))
{
Directory.CreateDirectory(logDir);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
_logFilePath = Path.Combine(logDir, $"launcher_{timestamp}.log");
Console.WriteLine($"[Logger] Log file initialized: {_logFilePath}");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[Logger] Failed to initialize log file: {ex.Message}");
}
_initialized = true;
}
/// <summary>
/// 获取日志文件路径
/// </summary>
public static string? GetLogFilePath()
{
return _logFilePath;
}
/// <summary>
/// 获取日志目录
/// </summary>
private static string? GetLogDirectory()
{
try
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
if (!string.IsNullOrEmpty(appData))
{
return Path.Combine(appData, "LanMountainDesktop", ".launcher", "logs");
}
}
catch
{
}
try
{
var launcherDir = AppContext.BaseDirectory;
return Path.Combine(launcherDir, ".launcher", "logs");
}
catch
{
}
return null;
}
/// <summary>
/// 记录信息日志
/// </summary>
public static void Info(string message)
{
WriteLog("INFO", message);
}
/// <summary>
/// 记录警告日志
/// </summary>
public static void Warn(string message)
{
WriteLog("WARN", message);
}
/// <summary>
/// 记录错误日志
/// </summary>
public static void Error(string message)
{
WriteLog("ERROR", message);
}
/// <summary>
/// 记录错误日志(带异常)
/// </summary>
public static void Error(string message, Exception exception)
{
WriteLog("ERROR", $"{message}\n{exception}");
}
/// <summary>
/// 写入日志
/// </summary>
private static void WriteLog(string level, string message)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
var logLine = $"[{timestamp}] [{level}] {message}";
Console.WriteLine(logLine);
if (string.IsNullOrEmpty(_logFilePath))
{
return;
}
try
{
lock (_lock)
{
File.AppendAllText(_logFilePath, logLine + Environment.NewLine, Encoding.UTF8);
}
}
catch
{
}
}
}

View File

@@ -6,29 +6,99 @@ internal sealed class OobeStateService
public OobeStateService(string appRoot)
{
// 将 OOBE 状态文件存储在用户可写的 LocalApplicationData 目录中,
// 而不是安装目录Program Files 下普通用户没有写入权限)。
var appDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
var stateDir = Path.Combine(appDataDir, ".launcher", "state");
Directory.CreateDirectory(stateDir);
// 优先使用 LocalApplicationData(用户目录,普通用户一定有权限)
string? stateDir = null;
Exception? lastException = null;
// 策略1: LocalApplicationData首选用户目录普通用户一定有写权限
try
{
var appDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
stateDir = Path.Combine(appDataDir, ".launcher", "state");
Directory.CreateDirectory(stateDir);
Console.WriteLine($"[OobeStateService] Using LocalApplicationData: {stateDir}");
}
catch (Exception ex)
{
lastException = ex;
Console.Error.WriteLine($"[OobeStateService] LocalApplicationData failed: {ex.Message}");
stateDir = null;
}
// 策略2: 如果LocalApplicationData不行使用用户的临时目录
if (stateDir == null)
{
try
{
var tempDir = Path.Combine(Path.GetTempPath(), "LanMountainDesktop", ".launcher", "state");
Directory.CreateDirectory(tempDir);
stateDir = tempDir;
Console.WriteLine($"[OobeStateService] Using TempPath: {stateDir}");
}
catch (Exception ex)
{
lastException = ex;
Console.Error.WriteLine($"[OobeStateService] TempPath failed: {ex.Message}");
stateDir = null;
}
}
// 策略3: 最后的兜底使用当前用户的应用程序数据目录和Launcher同目录
if (stateDir == null)
{
try
{
var launcherDir = AppContext.BaseDirectory;
stateDir = Path.Combine(launcherDir, ".launcher", "state");
Directory.CreateDirectory(stateDir);
Console.WriteLine($"[OobeStateService] Using Launcher directory: {stateDir}");
}
catch (Exception ex)
{
lastException = ex;
Console.Error.WriteLine($"[OobeStateService] All strategies failed! Last error: {ex.Message}");
// 如果所有策略都失败,抛出异常让上层处理
throw new InvalidOperationException("无法创建 OOBE 状态存储目录失败", lastException);
}
}
_markerPath = Path.Combine(stateDir, "first_run_completed");
Console.WriteLine($"[OobeStateService] Initialized successfully, marker path: {_markerPath}");
}
public bool IsFirstRun()
{
return !File.Exists(_markerPath);
try
{
return !File.Exists(_markerPath);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OobeStateService] Failed to check first run: {ex.Message}");
// 如果无法检查默认视为首次运行确保OOBE能显示
return true;
}
}
public void MarkCompleted()
{
var dir = Path.GetDirectoryName(_markerPath);
if (!string.IsNullOrWhiteSpace(dir))
try
{
Directory.CreateDirectory(dir);
}
var dir = Path.GetDirectoryName(_markerPath);
if (!string.IsNullOrWhiteSpace(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(_markerPath, DateTimeOffset.UtcNow.ToString("O"));
File.WriteAllText(_markerPath, DateTimeOffset.UtcNow.ToString("O"));
Console.WriteLine("[OobeStateService] Marked first run as completed");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OobeStateService] Failed to mark completed: {ex.Message}");
// 如果无法写入也没关系下次启动还会显示OOBE
}
}
}

View File

@@ -217,6 +217,7 @@ internal sealed class UpdateEngineService
snapshot.Status = "applied";
SaveSnapshot(snapshotPath, snapshot);
CleanupIncomingArtifacts();
// 清理旧版本但保留最近3个版本以支持回滚
CleanupDestroyedDeployments();
return new LauncherResult

View File

@@ -76,21 +76,30 @@
<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"
<Grid ColumnDefinitions="*,Auto">
<Button x:Name="OpenLogButton"
Grid.Column="0"
Content="打开日志"
Width="100"
Height="32"
FontSize="13"
Theme="{DynamicResource AccentButtonTheme}"/>
</StackPanel>
HorizontalAlignment="Left"/>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
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>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -2,6 +2,8 @@ using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using LanMountainDesktop.Launcher.Services;
using System.Diagnostics;
namespace LanMountainDesktop.Launcher.Views;
@@ -66,6 +68,7 @@ public partial class ErrorWindow : Window
// 按钮事件
var retryButton = this.FindControl<Button>("RetryButton");
var exitButton = this.FindControl<Button>("ExitButton");
var openLogButton = this.FindControl<Button>("OpenLogButton");
if (retryButton is not null)
{
@@ -86,6 +89,16 @@ public partial class ErrorWindow : Window
{
Console.Error.WriteLine("[ErrorWindow] Failed to find ExitButton!");
}
if (openLogButton is not null)
{
openLogButton.Click += OnOpenLogClick;
Console.WriteLine("[ErrorWindow] OpenLogButton event bound");
}
else
{
Console.Error.WriteLine("[ErrorWindow] Failed to find OpenLogButton!");
}
Console.WriteLine("[ErrorWindow] Components initialization completed");
}
@@ -210,6 +223,61 @@ public partial class ErrorWindow : Window
}
}
/// <summary>
/// 获取配置存储的基础目录
/// </summary>
private static string GetConfigBaseDirectory()
{
try
{
// 优先使用 LocalApplicationData用户状态
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
if (!string.IsNullOrEmpty(appData))
{
var configDir = Path.Combine(appData, "LanMountainDesktop", ".launcher");
return configDir;
}
}
catch
{
// LocalApplicationData 不可用,回退到 Launcher 所在目录
}
// 回退方案:使用 Launcher 所在目录
try
{
var launcherDir = AppContext.BaseDirectory;
var configDir = Path.Combine(launcherDir, ".launcher");
return configDir;
}
catch
{
// 最后的兜底:使用当前目录
return Path.Combine(Directory.GetCurrentDirectory(), ".launcher");
}
}
/// <summary>
/// 确保配置目录存在
/// </summary>
private static bool EnsureConfigDirectory(string dirPath)
{
try
{
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
Console.WriteLine($"[ErrorWindow] Created config directory: {dirPath}");
}
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"[ErrorWindow] Failed to create config directory: {ex.Message}");
return false;
}
}
/// <summary>
/// 保存开发模式状态(内部方法)
/// </summary>
@@ -217,17 +285,20 @@ public partial class ErrorWindow : Window
{
try
{
var devModeFile = GetDevModeFilePath();
var dir = Path.GetDirectoryName(devModeFile);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
var configDir = GetConfigBaseDirectory();
if (!EnsureConfigDirectory(configDir))
{
Directory.CreateDirectory(dir);
Console.Error.WriteLine("[ErrorWindow] Cannot save dev mode: config directory unavailable");
return;
}
var devModeFile = Path.Combine(configDir, "devmode.config");
File.WriteAllText(devModeFile, enabled ? "1" : "0");
Console.WriteLine($"[ErrorWindow] Dev mode state saved: {enabled}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to save dev mode state: {ex.Message}");
Console.Error.WriteLine($"[ErrorWindow] Failed to save dev mode state: {ex.Message}");
}
}
@@ -238,29 +309,24 @@ public partial class ErrorWindow : Window
{
try
{
var devModeFile = GetDevModeFilePath();
var configDir = GetConfigBaseDirectory();
var devModeFile = Path.Combine(configDir, "devmode.config");
if (File.Exists(devModeFile))
{
var content = File.ReadAllText(devModeFile).Trim();
return content == "1";
var enabled = content == "1";
Console.WriteLine($"[ErrorWindow] Dev mode state loaded: {enabled}");
return enabled;
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to load dev mode state: {ex.Message}");
Console.Error.WriteLine($"[ErrorWindow] Failed to load dev mode state: {ex.Message}");
}
return false;
}
/// <summary>
/// 获取开发模式状态文件路径
/// </summary>
private static string GetDevModeFilePath()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(appData, "LanMountainDesktop", ".launcher", "devmode.config");
}
/// <summary>
/// 保存自定义主程序路径(内部方法)
/// </summary>
@@ -268,17 +334,20 @@ public partial class ErrorWindow : Window
{
try
{
var hostPathFile = GetCustomHostPathFilePath();
var dir = Path.GetDirectoryName(hostPathFile);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
var configDir = GetConfigBaseDirectory();
if (!EnsureConfigDirectory(configDir))
{
Directory.CreateDirectory(dir);
Console.Error.WriteLine("[ErrorWindow] Cannot save custom path: config directory unavailable");
return;
}
var hostPathFile = Path.Combine(configDir, "custom-host-path.config");
File.WriteAllText(hostPathFile, path ?? string.Empty);
Console.WriteLine($"[ErrorWindow] Custom host path saved: {path}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to save custom host path: {ex.Message}");
Console.Error.WriteLine($"[ErrorWindow] Failed to save custom host path: {ex.Message}");
}
}
@@ -289,43 +358,42 @@ public partial class ErrorWindow : Window
{
try
{
var hostPathFile = GetCustomHostPathFilePath();
var configDir = GetConfigBaseDirectory();
var hostPathFile = Path.Combine(configDir, "custom-host-path.config");
if (File.Exists(hostPathFile))
{
var content = File.ReadAllText(hostPathFile).Trim();
// 验证路径是否仍然有效
if (!string.IsNullOrEmpty(content) && File.Exists(content))
{
Console.WriteLine($"[ErrorWindow] Custom host path loaded: {content}");
return content;
}
// 路径已失效,清理配置文件
try
if (!string.IsNullOrEmpty(content))
{
File.Delete(hostPathFile);
Console.WriteLine("Custom host path is no longer valid, cleared saved path.");
}
catch (Exception clearEx)
{
Console.Error.WriteLine($"Failed to clear invalid host path: {clearEx.Message}");
Console.WriteLine($"[ErrorWindow] Custom host path is no longer valid: {content}");
try
{
File.Delete(hostPathFile);
Console.WriteLine("[ErrorWindow] Cleared invalid custom host path");
}
catch (Exception clearEx)
{
Console.Error.WriteLine($"[ErrorWindow] Failed to clear invalid host path: {clearEx.Message}");
}
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to load custom host path: {ex.Message}");
Console.Error.WriteLine($"[ErrorWindow] Failed to load custom host path: {ex.Message}");
}
return null;
}
/// <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>
@@ -351,6 +419,110 @@ public partial class ErrorWindow : Window
{
_completionSource.TrySetResult(ErrorWindowResult.Exit);
}
/// <summary>
/// 打开日志文件
/// </summary>
private async void OnOpenLogClick(object? sender, RoutedEventArgs e)
{
try
{
var logFilePath = Logger.GetLogFilePath();
if (string.IsNullOrEmpty(logFilePath) || !File.Exists(logFilePath))
{
// 如果没有日志文件,打开日志目录
var logDir = Path.GetDirectoryName(logFilePath);
if (!string.IsNullOrEmpty(logDir) && Directory.Exists(logDir))
{
OpenFolder(logDir);
}
else
{
// 尝试打开配置目录
var configDir = GetConfigBaseDirectory();
if (Directory.Exists(configDir))
{
OpenFolder(configDir);
}
else
{
Console.WriteLine("[ErrorWindow] No log file or directory available");
}
}
return;
}
Console.WriteLine($"[ErrorWindow] Opening log file: {logFilePath}");
OpenFile(logFilePath);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[ErrorWindow] Failed to open log: {ex.Message}");
}
}
/// <summary>
/// 打开文件
/// </summary>
private static void OpenFile(string filePath)
{
try
{
if (OperatingSystem.IsWindows())
{
Process.Start(new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"\"{filePath}\"",
UseShellExecute = true
});
}
else if (OperatingSystem.IsMacOS())
{
Process.Start("open", filePath);
}
else if (OperatingSystem.IsLinux())
{
Process.Start("xdg-open", filePath);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[ErrorWindow] Failed to open file: {ex.Message}");
}
}
/// <summary>
/// 打开文件夹
/// </summary>
private static void OpenFolder(string folderPath)
{
try
{
if (OperatingSystem.IsWindows())
{
Process.Start(new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"\"{folderPath}\"",
UseShellExecute = true
});
}
else if (OperatingSystem.IsMacOS())
{
Process.Start("open", folderPath);
}
else if (OperatingSystem.IsLinux())
{
Process.Start("xdg-open", folderPath);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[ErrorWindow] Failed to open folder: {ex.Message}");
}
}
}
/// <summary>

View File

@@ -4,13 +4,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="220"
d:DesignWidth="480"
d:DesignHeight="320"
x:Class="LanMountainDesktop.Launcher.Views.UpdateWindow"
x:DataType="views:UpdateWindow"
Title="阑山桌面 - 更新"
Width="400"
Height="220"
Width="480"
Height="320"
CanResize="False"
WindowStartupLocation="CenterScreen"
SystemDecorations="None"
@@ -21,48 +21,88 @@
<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}" />
<Grid>
<!-- 顶部:应用名称和最小化按钮 -->
<Grid VerticalAlignment="Top" Margin="24,24,24,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center" Spacing="8">
<TextBlock x:Name="TitleText"
Text="阑山桌面"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<Border Background="{DynamicResource AccentFillColorDefaultBrush}"
CornerRadius="4"
Padding="6,2"
VerticalAlignment="Center">
<TextBlock Text="Update"
FontSize="11"
FontWeight="SemiBold"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}" />
</Border>
</StackPanel>
<!-- 状态文本 -->
<TextBlock x:Name="StatusText"
Grid.Row="1"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,16,0,0"
Text="正在更新,请稍候..." />
<!-- 最小化按钮 -->
<Button x:Name="MinimizeButton"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Width="32"
Height="32"
Background="Transparent"
BorderThickness="0">
<TextBlock Text="&#xE921;"
FontSize="12"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
</Grid>
<!-- 进度条 -->
<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 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>
<!-- 左下角:状态文字 -->
<TextBlock x:Name="StatusText"
Grid.Column="0"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Opacity="0.8"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Text="正在更新,请稍候..." />
<!-- 右下角:百分比 -->
<TextBlock x:Name="PercentText"
Grid.Column="1"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Opacity="0.8"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Text="0%" />
</Grid>
<!-- 底部:进度条 -->
<ProgressBar x:Name="ProgressIndicator"
Grid.Row="1"
Minimum="0"
Maximum="100"
Value="0"
Height="4"
IsIndeterminate="True"
Foreground="{DynamicResource AccentFillColorDefaultBrush}"
Background="{DynamicResource ControlStrokeColorDefaultBrush}" />
</Grid>
</Grid>
</Window>

View File

@@ -12,6 +12,22 @@ public partial class UpdateWindow : Window
public UpdateWindow()
{
AvaloniaXamlLoader.Load(this);
InitializeEventHandlers();
}
/// <summary>
/// 初始化事件处理程序
/// </summary>
private void InitializeEventHandlers()
{
var minimizeButton = this.FindControl<Button>("MinimizeButton");
if (minimizeButton != null)
{
minimizeButton.Click += (s, e) =>
{
this.WindowState = WindowState.Minimized;
};
}
}
/// <summary>
@@ -23,11 +39,11 @@ public partial class UpdateWindow : Window
{
var statusText = this.FindControl<TextBlock>("StatusText");
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
var detailText = this.FindControl<TextBlock>("DetailText");
var percentText = this.FindControl<TextBlock>("PercentText");
if (statusText is null || progressIndicator is null || detailText is null)
if (statusText is null || progressIndicator is null || percentText is null)
{
Console.Error.WriteLine($"[UpdateWindow] Controls not found in Report: StatusText={statusText != null}, ProgressIndicator={progressIndicator != null}, DetailText={detailText != null}");
Console.Error.WriteLine($"[UpdateWindow] Controls not found in Report: StatusText={statusText != null}, ProgressIndicator={progressIndicator != null}, PercentText={percentText != null}");
return;
}
@@ -37,23 +53,13 @@ public partial class UpdateWindow : Window
{
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = progressPercent;
percentText.Text = $"{progressPercent}%";
}
else
{
progressIndicator.IsIndeterminate = true;
percentText.Text = "";
}
// 根据阶段显示不同的底部提示
detailText.Text = stage.ToLowerInvariant() switch
{
"verify" => "正在验证更新完整性...",
"extract" => "正在解压更新包...",
"apply" => "正在应用更新文件...",
"plugins" => "正在升级插件...",
"cleanup" => "正在清理...",
"done" => "",
_ => ""
};
});
}
@@ -66,10 +72,10 @@ public partial class UpdateWindow : Window
{
var statusText = this.FindControl<TextBlock>("StatusText");
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
var detailText = this.FindControl<TextBlock>("DetailText");
var percentText = this.FindControl<TextBlock>("PercentText");
var titleText = this.FindControl<TextBlock>("TitleText");
if (statusText is null || progressIndicator is null || detailText is null || titleText is null)
if (statusText is null || progressIndicator is null || percentText is null || titleText is null)
{
Console.Error.WriteLine($"[UpdateWindow] Controls not found in ReportComplete");
return;
@@ -77,7 +83,7 @@ public partial class UpdateWindow : Window
progressIndicator.IsIndeterminate = false;
progressIndicator.Value = 100;
detailText.Text = "";
percentText.Text = "100%";
if (success)
{