mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Add LauncherPathResolver and refactor data paths
Introduce LauncherPathResolver to centralize resolving the launcher executable and .Launcher data directory (with write-permission fallback). Refactor DataLocationResolver to use a fixed ".Launcher" launcher data folder, expose explicit ResolveLauncherDataPath/ResolveDesktopDataPath/ResolveConfigPath/ResolveLauncherLogsPath/ResolveLauncherStatePath methods, and fix config load/save and directory creation to avoid dependency cycles. Update LauncherClient, UpdateWorkflowService and PluginMarketInstallService to use the new resolver (remove duplicated ResolveLauncherPath logic) and improve error messages when the launcher is missing.
This commit is contained in:
@@ -3,11 +3,30 @@ using LanMountainDesktop.Launcher.Models;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 解析应用数据目录位置。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 安装后的目录结构:
|
||||
/// <code>
|
||||
/// {AppRoot}/ ← 应用安装根目录
|
||||
/// LanMountainDesktop.Launcher.exe ← Launcher 可执行文件
|
||||
/// .Launcher/ ← Launcher 数据目录(日志、状态、配置等)
|
||||
/// app-{version}/ ← Host 部署目录
|
||||
/// LanMountainDesktop.exe
|
||||
/// ...
|
||||
/// </code>
|
||||
///
|
||||
/// Launcher 数据目录固定位于应用安装根目录下的 <c>.Launcher</c> 文件夹中,
|
||||
/// 与 app-* 部署目录同级。此目录不随数据位置模式改变。
|
||||
///
|
||||
/// Desktop(Host)数据目录则根据用户选择可位于系统目录或便携目录。
|
||||
/// </remarks>
|
||||
internal sealed class DataLocationResolver
|
||||
{
|
||||
private const string ConfigFileName = "data-location.config.json";
|
||||
private const string LauncherFolderName = "Launcher";
|
||||
private const string DesktopFolderName = "Desktop";
|
||||
private const string LauncherDataFolderName = ".Launcher";
|
||||
|
||||
private readonly string _appRoot;
|
||||
private readonly string _defaultSystemDataPath;
|
||||
@@ -28,13 +47,49 @@ internal sealed class DataLocationResolver
|
||||
public string DefaultSystemDataPath => _defaultSystemDataPath;
|
||||
|
||||
/// <summary>
|
||||
/// 默认便携模式数据路径(应用目录下的 AppData)
|
||||
/// 默认便携模式数据路径(应用目录下的 Desktop 文件夹)
|
||||
/// </summary>
|
||||
public string DefaultPortableDataPath => Path.Combine(_appRoot, "AppData");
|
||||
public string DefaultPortableDataPath => Path.Combine(_appRoot, DesktopFolderName);
|
||||
|
||||
private string ResolveBootstrapLauncherDataPath()
|
||||
/// <summary>
|
||||
/// Launcher 数据目录,固定位于应用安装根目录下的 .Launcher 文件夹。
|
||||
/// 该目录与 app-* 部署目录同级,不随数据位置模式改变。
|
||||
/// </summary>
|
||||
public string ResolveLauncherDataPath()
|
||||
{
|
||||
return Path.Combine(_defaultSystemDataPath, LauncherFolderName);
|
||||
return Path.Combine(_appRoot, LauncherDataFolderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 桌面应用数据目录(组件、设置、插件等)
|
||||
/// </summary>
|
||||
public string ResolveDesktopDataPath()
|
||||
{
|
||||
return Path.Combine(ResolveDataRoot(), DesktopFolderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据位置配置文件路径(保存在 Launcher 数据目录下)
|
||||
/// </summary>
|
||||
public string ResolveConfigPath()
|
||||
{
|
||||
return Path.Combine(ResolveLauncherDataPath(), ConfigFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动器日志目录
|
||||
/// </summary>
|
||||
public string ResolveLauncherLogsPath()
|
||||
{
|
||||
return Path.Combine(ResolveLauncherDataPath(), "logs");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动器状态目录
|
||||
/// </summary>
|
||||
public string ResolveLauncherStatePath()
|
||||
{
|
||||
return Path.Combine(ResolveLauncherDataPath(), "state");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,6 +110,19 @@ internal sealed class DataLocationResolver
|
||||
}
|
||||
}
|
||||
|
||||
public DataLocationMode ResolveMode()
|
||||
{
|
||||
var config = LoadConfig();
|
||||
if (config is null)
|
||||
{
|
||||
return DataLocationMode.System;
|
||||
}
|
||||
|
||||
return string.Equals(config.DataLocationMode, "Portable", StringComparison.OrdinalIgnoreCase)
|
||||
? DataLocationMode.Portable
|
||||
: DataLocationMode.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析数据根目录(用户选择的位置)
|
||||
/// </summary>
|
||||
@@ -84,66 +152,11 @@ internal sealed class DataLocationResolver
|
||||
: _defaultSystemDataPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动器数据目录(日志、配置、状态等)
|
||||
/// </summary>
|
||||
public string ResolveLauncherDataPath()
|
||||
{
|
||||
return Path.Combine(ResolveDataRoot(), LauncherFolderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 桌面应用数据目录(组件、设置、插件等)
|
||||
/// </summary>
|
||||
public string ResolveDesktopDataPath()
|
||||
{
|
||||
return Path.Combine(ResolveDataRoot(), DesktopFolderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据位置配置文件路径(保存在 Launcher 目录下)
|
||||
/// </summary>
|
||||
public string ResolveConfigPath()
|
||||
{
|
||||
return Path.Combine(ResolveBootstrapLauncherDataPath(), ConfigFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动器日志目录
|
||||
/// </summary>
|
||||
public string ResolveLauncherLogsPath()
|
||||
{
|
||||
return Path.Combine(ResolveLauncherDataPath(), "logs");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动器状态目录
|
||||
/// </summary>
|
||||
public string ResolveLauncherStatePath()
|
||||
{
|
||||
return Path.Combine(ResolveLauncherDataPath(), "state");
|
||||
}
|
||||
|
||||
public DataLocationMode ResolveMode()
|
||||
{
|
||||
var config = LoadConfig();
|
||||
if (config is null)
|
||||
{
|
||||
return DataLocationMode.System;
|
||||
}
|
||||
|
||||
return string.Equals(config.DataLocationMode, "Portable", StringComparison.OrdinalIgnoreCase)
|
||||
? DataLocationMode.Portable
|
||||
: DataLocationMode.System;
|
||||
}
|
||||
|
||||
public DataLocationConfig? LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 配置文件必须位于默认系统数据路径下的 Launcher 目录中
|
||||
// 避免循环依赖:不能调用 ResolveConfigPath() -> ResolveLauncherDataPath() -> ResolveDataRoot() -> LoadConfig()
|
||||
var configPath = Path.Combine(_defaultSystemDataPath, LauncherFolderName, ConfigFileName);
|
||||
var configPath = ResolveConfigPath();
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return null;
|
||||
@@ -163,8 +176,8 @@ internal sealed class DataLocationResolver
|
||||
{
|
||||
try
|
||||
{
|
||||
var launcherPath = ResolveBootstrapLauncherDataPath();
|
||||
Directory.CreateDirectory(launcherPath);
|
||||
var launcherDataPath = ResolveLauncherDataPath();
|
||||
Directory.CreateDirectory(launcherDataPath);
|
||||
|
||||
var configPath = ResolveConfigPath();
|
||||
var json = JsonSerializer.Serialize(config, AppJsonContext.Default.DataLocationConfig);
|
||||
@@ -194,9 +207,8 @@ internal sealed class DataLocationResolver
|
||||
// 先创建目录结构
|
||||
try
|
||||
{
|
||||
var resolvedDataRoot = ResolveDataRoot(config);
|
||||
Directory.CreateDirectory(Path.Combine(resolvedDataRoot, LauncherFolderName));
|
||||
Directory.CreateDirectory(Path.Combine(resolvedDataRoot, DesktopFolderName));
|
||||
Directory.CreateDirectory(ResolveLauncherDataPath());
|
||||
Directory.CreateDirectory(Path.Combine(ResolveDataRoot(config), DesktopFolderName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace LanMountainDesktop.Services;
|
||||
internal sealed class LauncherClient
|
||||
{
|
||||
private const int UserCanceledUacErrorCode = 1223;
|
||||
private const string LauncherExecutableName = "LanMountainDesktop.Launcher.exe";
|
||||
|
||||
public async Task<LauncherInstallResult> InstallPackageAsync(
|
||||
string packagePath,
|
||||
@@ -34,13 +33,13 @@ internal sealed class LauncherClient
|
||||
"failed");
|
||||
}
|
||||
|
||||
var launcherPath = ResolveLauncherPath();
|
||||
if (!File.Exists(launcherPath))
|
||||
var launcherPath = LauncherPathResolver.ResolveLauncherExecutablePath();
|
||||
if (string.IsNullOrWhiteSpace(launcherPath) || !File.Exists(launcherPath))
|
||||
{
|
||||
return new LauncherInstallResult(
|
||||
false,
|
||||
null,
|
||||
$"Launcher executable was not found at '{launcherPath}'.",
|
||||
"Launcher executable was not found. Expected it to be located in the application root directory (sibling to the app-* deployment folder).",
|
||||
"failed");
|
||||
}
|
||||
|
||||
@@ -129,21 +128,6 @@ internal sealed class LauncherClient
|
||||
return await JsonSerializer.DeserializeAsync<HelperResultFile>(stream, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static string ResolveLauncherPath()
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(baseDirectory, "Launcher", LauncherExecutableName),
|
||||
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "LanMountainDesktop.Launcher", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName))
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||||
}
|
||||
|
||||
private static string QuoteArgument(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
|
||||
90
LanMountainDesktop/Services/LauncherPathResolver.cs
Normal file
90
LanMountainDesktop/Services/LauncherPathResolver.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 统一解析 Launcher 可执行文件路径的工具类。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 安装后的目录结构:
|
||||
/// <code>
|
||||
/// {AppRoot}/ ← 应用安装根目录
|
||||
/// LanMountainDesktop.Launcher.exe ← Launcher 可执行文件
|
||||
/// .Launcher/ ← Launcher 数据目录(日志、状态、配置等)
|
||||
/// app-{version}/ ← Host 部署目录
|
||||
/// LanMountainDesktop.exe
|
||||
/// ...
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
internal static class LauncherPathResolver
|
||||
{
|
||||
private const string WindowsLauncherExeName = "LanMountainDesktop.Launcher.exe";
|
||||
private const string UnixLauncherExeName = "LanMountainDesktop.Launcher";
|
||||
|
||||
private static string LauncherExecutableName =>
|
||||
OperatingSystem.IsWindows() ? WindowsLauncherExeName : UnixLauncherExeName;
|
||||
|
||||
/// <summary>
|
||||
/// 解析 Launcher 可执行文件的完整路径。如果找不到则返回 null。
|
||||
/// </summary>
|
||||
public static string? ResolveLauncherExecutablePath()
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
|
||||
var candidates = new[]
|
||||
{
|
||||
// 1. 发布版(安装版):Host 在 app-* 子目录中,Launcher 在父目录(应用根目录)
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", LauncherExecutableName)),
|
||||
|
||||
// 2. 便携版 / 单文件发布:Launcher 与 Host 在同一目录
|
||||
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||
|
||||
// 3. 开发环境:Launcher 项目输出目录与 Host 项目输出目录同级
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName)),
|
||||
};
|
||||
|
||||
return candidates
|
||||
.Select(Path.GetFullPath)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 Launcher 数据目录(.Launcher)的路径。
|
||||
/// 该目录与 app-* 文件夹同级,位于应用安装根目录下。
|
||||
/// </summary>
|
||||
public static string ResolveLauncherDataDirectory()
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
|
||||
// 优先尝试应用安装根目录(Host 的父目录)
|
||||
var appRootCandidate = Path.GetFullPath(Path.Combine(baseDirectory, ".."));
|
||||
var launcherDataDir = Path.Combine(appRootCandidate, ".Launcher");
|
||||
|
||||
if (Directory.Exists(launcherDataDir) || CanWriteToDirectory(appRootCandidate))
|
||||
{
|
||||
return launcherDataDir;
|
||||
}
|
||||
|
||||
// 回退到 Host 所在目录(便携模式或开发环境)
|
||||
return Path.Combine(baseDirectory, ".Launcher");
|
||||
}
|
||||
|
||||
private static bool CanWriteToDirectory(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var testFile = Path.Combine(path, $".write-test-{Guid.NewGuid():N}.tmp");
|
||||
File.WriteAllText(testFile, string.Empty);
|
||||
File.Delete(testFile);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1431,26 +1431,15 @@ public sealed class UpdateWorkflowService
|
||||
{
|
||||
try
|
||||
{
|
||||
var launcherExeName = OperatingSystem.IsWindows()
|
||||
? "LanMountainDesktop.Launcher.exe"
|
||||
: "LanMountainDesktop.Launcher";
|
||||
|
||||
// The Launcher is in the parent directory of the app's base directory
|
||||
// (app runs from app-{version}/ subdirectory, Launcher is at root)
|
||||
var appBaseDir = AppContext.BaseDirectory;
|
||||
var launcherRoot = Path.GetDirectoryName(appBaseDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
if (string.IsNullOrWhiteSpace(launcherRoot))
|
||||
var launcherPath = LauncherPathResolver.ResolveLauncherExecutablePath();
|
||||
if (string.IsNullOrWhiteSpace(launcherPath) || !File.Exists(launcherPath))
|
||||
{
|
||||
launcherRoot = appBaseDir;
|
||||
}
|
||||
|
||||
var launcherPath = Path.Combine(launcherRoot, launcherExeName);
|
||||
if (!File.Exists(launcherPath))
|
||||
{
|
||||
AppLogger.Warn("UpdateWorkflow", $"Launcher executable not found at '{launcherPath}'. Falling back to next-startup apply.");
|
||||
AppLogger.Warn("UpdateWorkflow", "Launcher executable not found. Falling back to next-startup apply.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var launcherRoot = Path.GetDirectoryName(launcherPath)!;
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = launcherPath,
|
||||
|
||||
@@ -14,8 +14,6 @@ namespace LanMountainDesktop.Services.PluginMarket;
|
||||
|
||||
internal sealed class AirAppMarketInstallService : IDisposable
|
||||
{
|
||||
private const string LauncherExecutableName = "LanMountainDesktop.Launcher.exe";
|
||||
|
||||
private readonly PluginRuntimeService _runtime;
|
||||
private readonly LauncherClient _launcherClient = new();
|
||||
private readonly HttpClient _httpClient;
|
||||
@@ -83,13 +81,13 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var launcherPath = ResolveLauncherPath();
|
||||
if (!File.Exists(launcherPath))
|
||||
var launcherPath = LauncherPathResolver.ResolveLauncherExecutablePath();
|
||||
if (string.IsNullOrWhiteSpace(launcherPath) || !File.Exists(launcherPath))
|
||||
{
|
||||
return new AirAppMarketInstallResult(
|
||||
false,
|
||||
null,
|
||||
$"Launcher executable was not found at '{launcherPath}'.");
|
||||
"Launcher executable was not found. Expected it to be located in the application root directory (sibling to the app-* deployment folder).");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,21 +362,6 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
return new AirAppMarketVerificationResult(true, null);
|
||||
}
|
||||
|
||||
private static string ResolveLauncherPath()
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(baseDirectory, "Launcher", LauncherExecutableName),
|
||||
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "LanMountainDesktop.Launcher", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName))
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string path)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user