mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9283da5940 |
@@ -13,6 +13,10 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
|
// 初始化日志记录器
|
||||||
|
Logger.Initialize();
|
||||||
|
Logger.Info("Launcher starting...");
|
||||||
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,27 +54,12 @@ public partial class App : Application
|
|||||||
}
|
}
|
||||||
else
|
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 窗口,确保应用程序不会立即退出
|
// 先显示 Splash 窗口,确保应用程序不会立即退出
|
||||||
var splashWindow = new SplashWindow();
|
var splashWindow = new SplashWindow();
|
||||||
splashWindow.Show();
|
splashWindow.Show();
|
||||||
|
|
||||||
// 启动协调器流程
|
// 在 try-catch 块中实例化所有服务,确保任何异常都能被捕获
|
||||||
_ = RunCoordinatorWithSplashAsync(desktop, coordinator, splashWindow);
|
_ = RunCoordinatorWithSplashAsync(desktop, context, splashWindow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,14 +200,30 @@ public partial class App : Application
|
|||||||
|
|
||||||
private static async Task RunCoordinatorWithSplashAsync(
|
private static async Task RunCoordinatorWithSplashAsync(
|
||||||
IClassicDesktopStyleApplicationLifetime desktop,
|
IClassicDesktopStyleApplicationLifetime desktop,
|
||||||
LauncherFlowCoordinator coordinator,
|
CommandContext context,
|
||||||
SplashWindow splashWindow)
|
SplashWindow splashWindow)
|
||||||
{
|
{
|
||||||
LauncherResult result;
|
LauncherResult result;
|
||||||
ErrorWindow? errorWindow = null;
|
ErrorWindow? errorWindow = null;
|
||||||
|
LauncherFlowCoordinator? coordinator = null;
|
||||||
|
|
||||||
try
|
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);
|
result = await coordinator.RunAsync(splashWindow).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -344,11 +349,11 @@ public partial class App : Application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 清理旧版本
|
// 3. 清理旧版本,保留至少3个版本以支持回滚
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "正在清理...", 90));
|
await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "正在清理...", 90));
|
||||||
deploymentLocator.CleanupDestroyedDeployments();
|
deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using LanMountainDesktop.Launcher.Models;
|
||||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Launcher.Services;
|
namespace LanMountainDesktop.Launcher.Services;
|
||||||
@@ -17,44 +18,65 @@ internal sealed class DeploymentLocator
|
|||||||
|
|
||||||
public string? FindCurrentDeploymentDirectory()
|
public string? FindCurrentDeploymentDirectory()
|
||||||
{
|
{
|
||||||
var candidates = Directory.Exists(_appRoot)
|
Console.WriteLine("[DeploymentLocator] Searching for deployment directories (ClassIsland style)...");
|
||||||
? Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// 过滤掉无效的部署目录
|
if (!Directory.Exists(_appRoot))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
return withMarkers[0].Path;
|
Console.WriteLine("[DeploymentLocator] App root directory does not exist");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有 .current 标记,选择最新版本
|
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
|
||||||
var byVersion = validCandidates
|
|
||||||
.Select(path => new
|
|
||||||
{
|
|
||||||
Path = path,
|
|
||||||
Version = ParseVersionFromDirectory(path)
|
|
||||||
})
|
|
||||||
.OrderByDescending(item => item.Version)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
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()
|
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
|
try
|
||||||
{
|
{
|
||||||
var candidates = Directory.Exists(_appRoot)
|
Console.WriteLine($"[DeploymentLocator] Starting cleanup with retention policy: keep at least {minVersionsToKeep} versions");
|
||||||
? Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
var destroyedDirs = candidates
|
if (!Directory.Exists(_appRoot))
|
||||||
.Where(path => File.Exists(Path.Combine(path, ".destroy")));
|
{
|
||||||
|
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
|
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
|
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)
|
public static Version ParseVersionFromDirectory(string path)
|
||||||
{
|
{
|
||||||
var text = ParseVersionTextFromDirectory(path);
|
var text = ParseVersionTextFromDirectory(path);
|
||||||
|
|||||||
@@ -4,50 +4,67 @@ using System.Text.Json;
|
|||||||
namespace LanMountainDesktop.Launcher.Services;
|
namespace LanMountainDesktop.Launcher.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public string? ResolveHostExecutablePath()
|
internal sealed class FlexibleHostLocator
|
||||||
{
|
{
|
||||||
var executable = GetExecutableName();
|
private readonly HostDiscoveryOptions _options;
|
||||||
var searchContext = new SearchContext
|
private readonly string _appRoot;
|
||||||
{
|
private readonly DeploymentLocator _deploymentLocator;
|
||||||
ExecutableName = executable,
|
|
||||||
AppRoot = _appRoot,
|
|
||||||
Options = _options
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== 第一阶段:标准路径查找(快速路径)==========
|
public FlexibleHostLocator(string appRoot, HostDiscoveryOptions? options = null)
|
||||||
|
|
||||||
// 1. 检查环境变量指定的路径(最高优先级 - 用于调试和特殊场景)
|
|
||||||
var envPath = GetPathFromEnvironment();
|
|
||||||
if (!string.IsNullOrWhiteSpace(envPath))
|
|
||||||
{
|
{
|
||||||
var validated = ValidateAndReturn(envPath, "environment variable");
|
_appRoot = appRoot;
|
||||||
if (validated != null) return validated;
|
_options = options ?? new HostDiscoveryOptions();
|
||||||
|
_deploymentLocator = new DeploymentLocator(appRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 搜索部署目录(app-*)- 生产环境标准路径
|
/// <summary>
|
||||||
var deploymentPath = SearchDeploymentDirectories(searchContext);
|
/// 解析主程序可执行文件路径
|
||||||
if (!string.IsNullOrWhiteSpace(deploymentPath))
|
/// </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. 使用 DeploymentLocator(ClassIsland 风格的简洁查询 - 优先)
|
||||||
|
Console.WriteLine("[FlexibleHostLocator] Trying quick path: DeploymentLocator.FindCurrentDeploymentDirectory()");
|
||||||
|
var deploymentDir = _deploymentLocator.FindCurrentDeploymentDirectory();
|
||||||
|
if (!string.IsNullOrWhiteSpace(deploymentDir))
|
||||||
|
{
|
||||||
|
var deploymentExePath = Path.Combine(deploymentDir, executable);
|
||||||
|
if (File.Exists(deploymentExePath))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[FlexibleHostLocator] Quick path found: {deploymentExePath}");
|
||||||
|
return deploymentExePath;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[FlexibleHostLocator] Quick path found dir but no exe: {deploymentExePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 快速路径失败,尝试旧的 SearchDeploymentDirectories 作为 fallback
|
||||||
|
Console.WriteLine("[FlexibleHostLocator] Quick path failed, falling back to SearchDeploymentDirectories");
|
||||||
|
var deploymentPath = SearchDeploymentDirectories(searchContext);
|
||||||
|
if (!string.IsNullOrWhiteSpace(deploymentPath))
|
||||||
|
{
|
||||||
|
return deploymentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查 Launcher 同级目录(便携模式)
|
||||||
var portablePath = SearchPortableLocation(searchContext);
|
var portablePath = SearchPortableLocation(searchContext);
|
||||||
if (!string.IsNullOrWhiteSpace(portablePath))
|
if (!string.IsNullOrWhiteSpace(portablePath))
|
||||||
{
|
{
|
||||||
@@ -56,7 +73,7 @@ internal sealed class FlexibleHostLocator
|
|||||||
|
|
||||||
// ========== 第二阶段:灵活查找(标准路径找不到时)==========
|
// ========== 第二阶段:灵活查找(标准路径找不到时)==========
|
||||||
|
|
||||||
// 4. 检查配置文件中的路径 - 用户自定义配置
|
// 5. 检查配置文件中的路径 - 用户自定义配置
|
||||||
var configPath = GetPathFromConfigFile();
|
var configPath = GetPathFromConfigFile();
|
||||||
if (!string.IsNullOrWhiteSpace(configPath))
|
if (!string.IsNullOrWhiteSpace(configPath))
|
||||||
{
|
{
|
||||||
@@ -71,7 +88,7 @@ internal sealed class FlexibleHostLocator
|
|||||||
return nearbyPath;
|
return nearbyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 开发模式:检查保存的自定义路径
|
// 7. 开发模式:检查保存的自定义路径
|
||||||
if (_options.PreferDevModeConfig && Views.ErrorWindow.CheckDevModeEnabled())
|
if (_options.PreferDevModeConfig && Views.ErrorWindow.CheckDevModeEnabled())
|
||||||
{
|
{
|
||||||
var savedPath = Views.ErrorWindow.GetSavedCustomHostPath();
|
var savedPath = Views.ErrorWindow.GetSavedCustomHostPath();
|
||||||
@@ -82,21 +99,21 @@ internal sealed class FlexibleHostLocator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 搜索标准开发路径
|
// 8. 搜索标准开发路径
|
||||||
var devPath = SearchDevelopmentPaths(searchContext);
|
var devPath = SearchDevelopmentPaths(searchContext);
|
||||||
if (!string.IsNullOrWhiteSpace(devPath))
|
if (!string.IsNullOrWhiteSpace(devPath))
|
||||||
{
|
{
|
||||||
return devPath;
|
return devPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 搜索额外的配置路径
|
// 9. 搜索额外的配置路径
|
||||||
var additionalPath = SearchAdditionalPaths(searchContext);
|
var additionalPath = SearchAdditionalPaths(searchContext);
|
||||||
if (!string.IsNullOrWhiteSpace(additionalPath))
|
if (!string.IsNullOrWhiteSpace(additionalPath))
|
||||||
{
|
{
|
||||||
return additionalPath;
|
return additionalPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 递归搜索(如果启用)
|
// 10. 递归搜索(如果启用)
|
||||||
if (_options.RecursiveSearch)
|
if (_options.RecursiveSearch)
|
||||||
{
|
{
|
||||||
var recursivePath = SearchRecursively(searchContext);
|
var recursivePath = SearchRecursively(searchContext);
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ internal sealed class LauncherFlowCoordinator
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 清理待删除的旧版本
|
// 清理旧版本,保留至少3个版本
|
||||||
_deploymentLocator.CleanupDestroyedDeployments();
|
_deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
|
||||||
|
|
||||||
// 使用传入的 Splash 窗口或创建新的
|
// 使用传入的 Splash 窗口或创建新的
|
||||||
var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() =>
|
var splashWindow = existingSplashWindow ?? await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
|||||||
138
LanMountainDesktop.Launcher/Services/Logger.cs
Normal file
138
LanMountainDesktop.Launcher/Services/Logger.cs
Normal 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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,29 +6,99 @@ internal sealed class OobeStateService
|
|||||||
|
|
||||||
public OobeStateService(string appRoot)
|
public OobeStateService(string appRoot)
|
||||||
{
|
{
|
||||||
// 将 OOBE 状态文件存储在用户可写的 LocalApplicationData 目录中,
|
// 优先使用 LocalApplicationData(用户目录,普通用户一定有权限)
|
||||||
// 而不是安装目录(Program Files 下普通用户没有写入权限)。
|
string? stateDir = null;
|
||||||
var appDataDir = Path.Combine(
|
Exception? lastException = null;
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"LanMountainDesktop");
|
// 策略1: LocalApplicationData(首选,用户目录,普通用户一定有写权限)
|
||||||
var stateDir = Path.Combine(appDataDir, ".launcher", "state");
|
try
|
||||||
Directory.CreateDirectory(stateDir);
|
{
|
||||||
|
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");
|
_markerPath = Path.Combine(stateDir, "first_run_completed");
|
||||||
|
Console.WriteLine($"[OobeStateService] Initialized successfully, marker path: {_markerPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsFirstRun()
|
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()
|
public void MarkCompleted()
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(_markerPath);
|
try
|
||||||
if (!string.IsNullOrWhiteSpace(dir))
|
|
||||||
{
|
{
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ internal sealed class UpdateEngineService
|
|||||||
snapshot.Status = "applied";
|
snapshot.Status = "applied";
|
||||||
SaveSnapshot(snapshotPath, snapshot);
|
SaveSnapshot(snapshotPath, snapshot);
|
||||||
CleanupIncomingArtifacts();
|
CleanupIncomingArtifacts();
|
||||||
|
// 清理旧版本,但保留最近3个版本以支持回滚
|
||||||
CleanupDestroyedDeployments();
|
CleanupDestroyedDeployments();
|
||||||
|
|
||||||
return new LauncherResult
|
return new LauncherResult
|
||||||
|
|||||||
@@ -76,21 +76,30 @@
|
|||||||
<Border Grid.Row="1"
|
<Border Grid.Row="1"
|
||||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||||
Padding="24,16">
|
Padding="24,16">
|
||||||
<StackPanel Orientation="Horizontal"
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
HorizontalAlignment="Right"
|
<Button x:Name="OpenLogButton"
|
||||||
Spacing="8">
|
Grid.Column="0"
|
||||||
<Button x:Name="ExitButton"
|
Content="打开日志"
|
||||||
Content="退出"
|
Width="100"
|
||||||
Width="80"
|
|
||||||
Height="32"
|
|
||||||
FontSize="13"/>
|
|
||||||
<Button x:Name="RetryButton"
|
|
||||||
Content="重试"
|
|
||||||
Width="80"
|
|
||||||
Height="32"
|
Height="32"
|
||||||
FontSize="13"
|
FontSize="13"
|
||||||
Theme="{DynamicResource AccentButtonTheme}"/>
|
HorizontalAlignment="Left"/>
|
||||||
</StackPanel>
|
<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>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
using LanMountainDesktop.Launcher.Services;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Launcher.Views;
|
namespace LanMountainDesktop.Launcher.Views;
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ public partial class ErrorWindow : Window
|
|||||||
// 按钮事件
|
// 按钮事件
|
||||||
var retryButton = this.FindControl<Button>("RetryButton");
|
var retryButton = this.FindControl<Button>("RetryButton");
|
||||||
var exitButton = this.FindControl<Button>("ExitButton");
|
var exitButton = this.FindControl<Button>("ExitButton");
|
||||||
|
var openLogButton = this.FindControl<Button>("OpenLogButton");
|
||||||
|
|
||||||
if (retryButton is not null)
|
if (retryButton is not null)
|
||||||
{
|
{
|
||||||
@@ -87,6 +90,16 @@ public partial class ErrorWindow : Window
|
|||||||
Console.Error.WriteLine("[ErrorWindow] Failed to find ExitButton!");
|
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");
|
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>
|
||||||
/// 保存开发模式状态(内部方法)
|
/// 保存开发模式状态(内部方法)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -217,17 +285,20 @@ public partial class ErrorWindow : Window
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var devModeFile = GetDevModeFilePath();
|
var configDir = GetConfigBaseDirectory();
|
||||||
var dir = Path.GetDirectoryName(devModeFile);
|
if (!EnsureConfigDirectory(configDir))
|
||||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
|
||||||
{
|
{
|
||||||
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");
|
File.WriteAllText(devModeFile, enabled ? "1" : "0");
|
||||||
|
Console.WriteLine($"[ErrorWindow] Dev mode state saved: {enabled}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
try
|
||||||
{
|
{
|
||||||
var devModeFile = GetDevModeFilePath();
|
var configDir = GetConfigBaseDirectory();
|
||||||
|
var devModeFile = Path.Combine(configDir, "devmode.config");
|
||||||
|
|
||||||
if (File.Exists(devModeFile))
|
if (File.Exists(devModeFile))
|
||||||
{
|
{
|
||||||
var content = File.ReadAllText(devModeFile).Trim();
|
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)
|
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;
|
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>
|
||||||
/// 保存自定义主程序路径(内部方法)
|
/// 保存自定义主程序路径(内部方法)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -268,17 +334,20 @@ public partial class ErrorWindow : Window
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var hostPathFile = GetCustomHostPathFilePath();
|
var configDir = GetConfigBaseDirectory();
|
||||||
var dir = Path.GetDirectoryName(hostPathFile);
|
if (!EnsureConfigDirectory(configDir))
|
||||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
|
||||||
{
|
{
|
||||||
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);
|
File.WriteAllText(hostPathFile, path ?? string.Empty);
|
||||||
|
Console.WriteLine($"[ErrorWindow] Custom host path saved: {path}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
try
|
||||||
{
|
{
|
||||||
var hostPathFile = GetCustomHostPathFilePath();
|
var configDir = GetConfigBaseDirectory();
|
||||||
|
var hostPathFile = Path.Combine(configDir, "custom-host-path.config");
|
||||||
|
|
||||||
if (File.Exists(hostPathFile))
|
if (File.Exists(hostPathFile))
|
||||||
{
|
{
|
||||||
var content = File.ReadAllText(hostPathFile).Trim();
|
var content = File.ReadAllText(hostPathFile).Trim();
|
||||||
// 验证路径是否仍然有效
|
// 验证路径是否仍然有效
|
||||||
if (!string.IsNullOrEmpty(content) && File.Exists(content))
|
if (!string.IsNullOrEmpty(content) && File.Exists(content))
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"[ErrorWindow] Custom host path loaded: {content}");
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 路径已失效,清理配置文件
|
// 路径已失效,清理配置文件
|
||||||
try
|
if (!string.IsNullOrEmpty(content))
|
||||||
{
|
{
|
||||||
File.Delete(hostPathFile);
|
Console.WriteLine($"[ErrorWindow] Custom host path is no longer valid: {content}");
|
||||||
Console.WriteLine("Custom host path is no longer valid, cleared saved path.");
|
try
|
||||||
}
|
{
|
||||||
catch (Exception clearEx)
|
File.Delete(hostPathFile);
|
||||||
{
|
Console.WriteLine("[ErrorWindow] Cleared invalid custom host path");
|
||||||
Console.Error.WriteLine($"Failed to clear invalid host path: {clearEx.Message}");
|
}
|
||||||
|
catch (Exception clearEx)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[ErrorWindow] Failed to clear invalid host path: {clearEx.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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;
|
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>
|
||||||
/// 检查是否启用了开发模式(静态方法,启动时调用)
|
/// 检查是否启用了开发模式(静态方法,启动时调用)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -351,6 +419,110 @@ public partial class ErrorWindow : Window
|
|||||||
{
|
{
|
||||||
_completionSource.TrySetResult(ErrorWindowResult.Exit);
|
_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>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignWidth="400"
|
d:DesignWidth="480"
|
||||||
d:DesignHeight="220"
|
d:DesignHeight="320"
|
||||||
x:Class="LanMountainDesktop.Launcher.Views.UpdateWindow"
|
x:Class="LanMountainDesktop.Launcher.Views.UpdateWindow"
|
||||||
x:DataType="views:UpdateWindow"
|
x:DataType="views:UpdateWindow"
|
||||||
Title="阑山桌面 - 更新"
|
Title="阑山桌面 - 更新"
|
||||||
Width="400"
|
Width="480"
|
||||||
Height="220"
|
Height="320"
|
||||||
CanResize="False"
|
CanResize="False"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
SystemDecorations="None"
|
SystemDecorations="None"
|
||||||
@@ -21,48 +21,88 @@
|
|||||||
<views:UpdateWindow />
|
<views:UpdateWindow />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*,Auto,Auto">
|
<Grid>
|
||||||
<!-- 应用名称 -->
|
<!-- 顶部:应用名称和最小化按钮 -->
|
||||||
<TextBlock x:Name="TitleText"
|
<Grid VerticalAlignment="Top" Margin="24,24,24,0">
|
||||||
Text="阑山桌面"
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center" Spacing="8">
|
||||||
FontSize="36"
|
<TextBlock x:Name="TitleText"
|
||||||
FontWeight="Light"
|
Text="阑山桌面"
|
||||||
VerticalAlignment="Center"
|
FontSize="24"
|
||||||
HorizontalAlignment="Center"
|
FontWeight="SemiBold"
|
||||||
Grid.Row="0"
|
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||||
Margin="0,30,0,0"
|
<Border Background="{DynamicResource AccentFillColorDefaultBrush}"
|
||||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
CornerRadius="4"
|
||||||
|
Padding="6,2"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Update"
|
||||||
|
FontSize="11"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- 状态文本 -->
|
<!-- 最小化按钮 -->
|
||||||
<TextBlock x:Name="StatusText"
|
<Button x:Name="MinimizeButton"
|
||||||
Grid.Row="1"
|
HorizontalAlignment="Right"
|
||||||
FontSize="13"
|
VerticalAlignment="Center"
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
Width="32"
|
||||||
HorizontalAlignment="Center"
|
Height="32"
|
||||||
VerticalAlignment="Center"
|
Background="Transparent"
|
||||||
Margin="0,16,0,0"
|
BorderThickness="0">
|
||||||
Text="正在更新,请稍候..." />
|
<TextBlock Text=""
|
||||||
|
FontSize="12"
|
||||||
|
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||||
|
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- 进度条 -->
|
<!-- 底部区域:进度条和状态 -->
|
||||||
<ProgressBar x:Name="ProgressIndicator"
|
<Grid VerticalAlignment="Bottom" Margin="24,0,24,24">
|
||||||
Grid.Row="2"
|
<Grid.RowDefinitions>
|
||||||
Minimum="0"
|
<RowDefinition Height="Auto"/>
|
||||||
Maximum="100"
|
<RowDefinition Height="Auto"/>
|
||||||
Value="0"
|
</Grid.RowDefinitions>
|
||||||
Height="3"
|
|
||||||
Width="200"
|
|
||||||
Margin="0,16,0,0"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
Foreground="{DynamicResource AccentFillColorDefaultBrush}"
|
|
||||||
Background="{DynamicResource ControlStrokeColorDefaultBrush}" />
|
|
||||||
|
|
||||||
<!-- 底部提示 -->
|
<!-- 第一行:左下角状态,右下角百分比 -->
|
||||||
<TextBlock x:Name="DetailText"
|
<Grid Grid.Row="0" Margin="0,0,0,8">
|
||||||
Grid.Row="3"
|
<Grid.ColumnDefinitions>
|
||||||
FontSize="11"
|
<ColumnDefinition Width="*"/>
|
||||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
<ColumnDefinition Width="Auto"/>
|
||||||
HorizontalAlignment="Center"
|
</Grid.ColumnDefinitions>
|
||||||
Margin="0,12,0,24"
|
|
||||||
Text="" />
|
<!-- 左下角:状态文字 -->
|
||||||
|
<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>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -12,6 +12,22 @@ public partial class UpdateWindow : Window
|
|||||||
public UpdateWindow()
|
public UpdateWindow()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
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>
|
/// <summary>
|
||||||
@@ -23,11 +39,11 @@ public partial class UpdateWindow : Window
|
|||||||
{
|
{
|
||||||
var statusText = this.FindControl<TextBlock>("StatusText");
|
var statusText = this.FindControl<TextBlock>("StatusText");
|
||||||
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,23 +53,13 @@ public partial class UpdateWindow : Window
|
|||||||
{
|
{
|
||||||
progressIndicator.IsIndeterminate = false;
|
progressIndicator.IsIndeterminate = false;
|
||||||
progressIndicator.Value = progressPercent;
|
progressIndicator.Value = progressPercent;
|
||||||
|
percentText.Text = $"{progressPercent}%";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
progressIndicator.IsIndeterminate = true;
|
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 statusText = this.FindControl<TextBlock>("StatusText");
|
||||||
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
|
var progressIndicator = this.FindControl<ProgressBar>("ProgressIndicator");
|
||||||
var detailText = this.FindControl<TextBlock>("DetailText");
|
var percentText = this.FindControl<TextBlock>("PercentText");
|
||||||
var titleText = this.FindControl<TextBlock>("TitleText");
|
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");
|
Console.Error.WriteLine($"[UpdateWindow] Controls not found in ReportComplete");
|
||||||
return;
|
return;
|
||||||
@@ -77,7 +83,7 @@ public partial class UpdateWindow : Window
|
|||||||
|
|
||||||
progressIndicator.IsIndeterminate = false;
|
progressIndicator.IsIndeterminate = false;
|
||||||
progressIndicator.Value = 100;
|
progressIndicator.Value = 100;
|
||||||
detailText.Text = "";
|
percentText.Text = "100%";
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user