mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 17:24:27 +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:
@@ -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