mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
* 激进的更新 * 试试 * fix.可爱的我一直在修CI( * fix.启动器一定要能够启动 * feat.尝试弄了AOT的启动器。 * fix.修CI,好像是因为Linux那边有个问题,反正修就对了。 * fix.ci难修,为什么liunx跑不起来呢? * Update build.yml * Update LanMountainDesktop.csproj * changed.调整了启动逻辑,优化了更新页面。 * changed.优化了更新体验 * feat.依旧试增量更新这一块,看看velopack * fix.我们试验性地修复了启动器无法正常启动的问题,原因可能是这个画面没有启动,就GUI没显示。然后还把编译问题修了一下。 * fix.继续修ci,ci怎么天天炸 * changed.velopack,试试rust * fix.修ci,修融合桌面,修启动器 * fix.GitHub Action工作流怎么天天出问题 * feat.引入velopack,不好,是rust(至少内存很安全了。 * chore: migrate release pipeline to signed filemap and wire rainyun s3 * fix: make optional s3 upload step workflow-parse safe * fix: make delta pack generation robust for empty diffs and linux paths * chore: rotate launcher update public key for pdc signing * fix: restore stable launcher update public key * fix: sync launcher public key with update signing secret * fix: normalize PEM line endings in signing key validation * fix: rotate launcher public key to match ci signing secret * fix: compare signing keys by SPKI instead of PEM text * refactor update backend to host-managed PDC pipeline * fix release workflow env key collisions * relax publish-pdc precheck to require S3 only * set GH_TOKEN for PDCC installer step * ci: add local pdc mock fallback for release publish * ci: fix pdc mock process log redirection * ci: fallback pdcc signing key to update private key * ci: ensure pdcc signing passphrase env is always set * ci: create pdcc publish root before invoking client * ci: set pdcc version variable from release version * ci: decouple pdcc installer version from publish config version * ci: package pdcc subchannels with generated filemap and changelog * ci: make local pdc mock diff return empty for fast fallback * ci: fix pdcc variable mapping and pdc signing prechecks * Update App.axaml.cs * ci: wire aws cli credentials for rainyun s3 * ci: pin pdcc client version separately from app version * ci: harden local pdc mock transport handling * ci: publish pdcc subchannels in one pass * ci: add pdcc publish heartbeat and timeout * ci: fix pdcc publish workdir bootstrap * feat.Penguin Logistics Online Network Distribution System * ci: fix plonds s3 probe and signing fallback * ci: validate signing key and quiet missing baselines * ci: relax aws checksum mode for rainyun s3 * ci: avoid multipart uploads to rainyun s3 * ci: handle empty plonds baselines safely * ci.plonds * Rebuild release pipeline around PLONDS and DDSS * Fix Windows installer script path in release workflow
630 lines
19 KiB
C#
630 lines
19 KiB
C#
using System.Diagnostics;
|
||
using System.Text.Json;
|
||
|
||
namespace LanMountainDesktop.Launcher.Services;
|
||
|
||
/// <summary>
|
||
/// 灵活的主程序定位器
|
||
/// </summary>
|
||
internal sealed class FlexibleHostLocator
|
||
{
|
||
private readonly HostDiscoveryOptions _options;
|
||
private readonly string _appRoot;
|
||
private readonly DeploymentLocator _deploymentLocator;
|
||
|
||
public FlexibleHostLocator(string appRoot, HostDiscoveryOptions? options = null)
|
||
{
|
||
_appRoot = appRoot;
|
||
_options = options ?? new HostDiscoveryOptions();
|
||
_deploymentLocator = new DeploymentLocator(appRoot);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析主程序可执行文件路径
|
||
/// </summary>
|
||
public string? ResolveHostExecutablePath()
|
||
{
|
||
var executable = GetExecutableName();
|
||
var searchContext = new SearchContext
|
||
{
|
||
ExecutableName = executable,
|
||
AppRoot = _appRoot,
|
||
Options = _options
|
||
};
|
||
|
||
// ========== 第一阶段:标准路径查找(快速路径)==========
|
||
|
||
// 1. 检查环境变量指定的路径(最高优先级 - 用于调试和特殊场景)
|
||
var envPath = GetPathFromEnvironment();
|
||
if (!string.IsNullOrWhiteSpace(envPath))
|
||
{
|
||
var validated = ValidateAndReturn(envPath, "environment variable");
|
||
if (validated != null) return validated;
|
||
}
|
||
|
||
// 2. 使用 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);
|
||
if (!string.IsNullOrWhiteSpace(portablePath))
|
||
{
|
||
return portablePath;
|
||
}
|
||
|
||
// ========== 第二阶段:灵活查找(标准路径找不到时)==========
|
||
|
||
// 5. 检查配置文件中的路径 - 用户自定义配置
|
||
var configPath = GetPathFromConfigFile();
|
||
if (!string.IsNullOrWhiteSpace(configPath))
|
||
{
|
||
var validated = ValidateAndReturn(configPath, "config file");
|
||
if (validated != null) return validated;
|
||
}
|
||
|
||
// 5. 搜索附近目录(向上、向下各一层)
|
||
var nearbyPath = SearchNearbyDirectories(searchContext);
|
||
if (!string.IsNullOrWhiteSpace(nearbyPath))
|
||
{
|
||
return nearbyPath;
|
||
}
|
||
|
||
// 7. 开发模式:检查保存的自定义路径
|
||
if (_options.PreferDevModeConfig && Views.ErrorWindow.CheckDevModeEnabled())
|
||
{
|
||
var savedPath = Views.ErrorWindow.GetSavedCustomHostPath();
|
||
if (!string.IsNullOrWhiteSpace(savedPath))
|
||
{
|
||
var validated = ValidateAndReturn(savedPath, "saved dev mode path");
|
||
if (validated != null) return validated;
|
||
}
|
||
}
|
||
|
||
// 8. 搜索标准开发路径
|
||
var devPath = SearchDevelopmentPaths(searchContext);
|
||
if (!string.IsNullOrWhiteSpace(devPath))
|
||
{
|
||
return devPath;
|
||
}
|
||
|
||
// 9. 搜索额外的配置路径
|
||
var additionalPath = SearchAdditionalPaths(searchContext);
|
||
if (!string.IsNullOrWhiteSpace(additionalPath))
|
||
{
|
||
return additionalPath;
|
||
}
|
||
|
||
// 10. 递归搜索(如果启用)
|
||
if (_options.RecursiveSearch)
|
||
{
|
||
var recursivePath = SearchRecursively(searchContext);
|
||
if (!string.IsNullOrWhiteSpace(recursivePath))
|
||
{
|
||
return recursivePath;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从环境变量获取路径
|
||
/// </summary>
|
||
private string? GetPathFromEnvironment()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_options.CustomPathEnvVar))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var path = Environment.GetEnvironmentVariable(_options.CustomPathEnvVar);
|
||
return path;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从配置文件获取路径
|
||
/// </summary>
|
||
private string? GetPathFromConfigFile()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_options.ConfigFileName))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var configPath = Path.Combine(_appRoot, _options.ConfigFileName);
|
||
if (!File.Exists(configPath))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
var json = File.ReadAllText(configPath);
|
||
var config = JsonSerializer.Deserialize(json, AppJsonContext.Default.HostDiscoveryConfig);
|
||
if (config?.HostPath != null && File.Exists(config.HostPath))
|
||
{
|
||
return config.HostPath;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略配置文件读取错误
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搜索部署目录
|
||
/// </summary>
|
||
private string? SearchDeploymentDirectories(SearchContext context)
|
||
{
|
||
if (!Directory.Exists(_appRoot))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 查找 app-* 目录
|
||
var appDirs = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly)
|
||
.Where(dir => !File.Exists(Path.Combine(dir, ".destroy")))
|
||
.Where(dir => !File.Exists(Path.Combine(dir, ".partial")))
|
||
.ToList();
|
||
|
||
// 优先选择带 .current 标记的
|
||
var currentMarked = appDirs
|
||
.Where(dir => File.Exists(Path.Combine(dir, ".current")))
|
||
.Select(dir => Path.Combine(dir, context.ExecutableName))
|
||
.FirstOrDefault(File.Exists);
|
||
|
||
if (currentMarked != null)
|
||
{
|
||
return currentMarked;
|
||
}
|
||
|
||
// 选择版本号最高的
|
||
var latest = appDirs
|
||
.Select(dir => new
|
||
{
|
||
Dir = dir,
|
||
Version = ParseVersionFromDirectoryName(dir)
|
||
})
|
||
.OrderByDescending(x => x.Version)
|
||
.Select(x => Path.Combine(x.Dir, context.ExecutableName))
|
||
.FirstOrDefault(File.Exists);
|
||
|
||
return latest;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搜索便携模式位置(Launcher 同级目录)
|
||
/// </summary>
|
||
private string? SearchPortableLocation(SearchContext context)
|
||
{
|
||
try
|
||
{
|
||
var launcherDir = AppContext.BaseDirectory;
|
||
var portablePath = Path.Combine(launcherDir, context.ExecutableName);
|
||
|
||
if (File.Exists(portablePath))
|
||
{
|
||
return portablePath;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略错误
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搜索附近目录(灵活查找,适用于各种部署场景)
|
||
/// </summary>
|
||
private string? SearchNearbyDirectories(SearchContext context)
|
||
{
|
||
try
|
||
{
|
||
var searchDirs = new List<string>();
|
||
|
||
// Launcher 所在目录
|
||
var launcherDir = AppContext.BaseDirectory;
|
||
searchDirs.Add(launcherDir);
|
||
|
||
// 上级目录
|
||
var parentDir = Path.GetFullPath(Path.Combine(launcherDir, ".."));
|
||
if (Directory.Exists(parentDir))
|
||
{
|
||
searchDirs.Add(parentDir);
|
||
}
|
||
|
||
// 上上级目录
|
||
var grandparentDir = Path.GetFullPath(Path.Combine(launcherDir, "..", ".."));
|
||
if (Directory.Exists(grandparentDir))
|
||
{
|
||
searchDirs.Add(grandparentDir);
|
||
}
|
||
|
||
// AppRoot 及其上级
|
||
if (!string.IsNullOrWhiteSpace(_appRoot) && Directory.Exists(_appRoot))
|
||
{
|
||
searchDirs.Add(_appRoot);
|
||
var appParent = Path.GetFullPath(Path.Combine(_appRoot, ".."));
|
||
if (Directory.Exists(appParent))
|
||
{
|
||
searchDirs.Add(appParent);
|
||
}
|
||
}
|
||
|
||
// 去重后搜索
|
||
foreach (var dir in searchDirs.Distinct(StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
// 直接搜索
|
||
var directPath = Path.Combine(dir, context.ExecutableName);
|
||
if (File.Exists(directPath))
|
||
{
|
||
return directPath;
|
||
}
|
||
|
||
// 搜索子目录(一层)
|
||
if (Directory.Exists(dir))
|
||
{
|
||
foreach (var subDir in Directory.GetDirectories(dir))
|
||
{
|
||
var subPath = Path.Combine(subDir, context.ExecutableName);
|
||
if (File.Exists(subPath))
|
||
{
|
||
return subPath;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略搜索错误
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搜索开发路径
|
||
/// </summary>
|
||
private string? SearchDevelopmentPaths(SearchContext context)
|
||
{
|
||
// 获取 Launcher 所在目录
|
||
var launcherDir = AppContext.BaseDirectory;
|
||
|
||
// 动态构建可能的开发路径(支持不同的项目结构)
|
||
var possiblePaths = new List<string>();
|
||
|
||
// 从解决方案根目录搜索(支持不同的解决方案结构)
|
||
var solutionRoot = FindSolutionRoot(launcherDir);
|
||
if (!string.IsNullOrWhiteSpace(solutionRoot))
|
||
{
|
||
// 搜索所有可能的 bin 目录
|
||
possiblePaths.AddRange(SearchBinDirectories(solutionRoot, context.ExecutableName));
|
||
}
|
||
|
||
// 添加硬编码的备用路径
|
||
possiblePaths.AddRange(new[]
|
||
{
|
||
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", context.ExecutableName),
|
||
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", context.ExecutableName),
|
||
Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Debug", "net10.0", context.ExecutableName),
|
||
Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Release", "net10.0", context.ExecutableName),
|
||
});
|
||
|
||
foreach (var path in possiblePaths.Select(Path.GetFullPath).Distinct())
|
||
{
|
||
if (File.Exists(path))
|
||
{
|
||
return path;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搜索额外的配置路径
|
||
/// </summary>
|
||
private string? SearchAdditionalPaths(SearchContext context)
|
||
{
|
||
if (_options.AdditionalSearchPaths == null || !_options.AdditionalSearchPaths.Any())
|
||
{
|
||
return null;
|
||
}
|
||
|
||
foreach (var pattern in _options.AdditionalSearchPaths)
|
||
{
|
||
try
|
||
{
|
||
// 替换变量
|
||
var expandedPattern = ExpandVariables(pattern);
|
||
|
||
// 支持通配符
|
||
if (expandedPattern.Contains('*') || expandedPattern.Contains('?'))
|
||
{
|
||
var dir = Path.GetDirectoryName(expandedPattern) ?? _appRoot;
|
||
var filePattern = Path.GetFileName(expandedPattern);
|
||
|
||
if (Directory.Exists(dir))
|
||
{
|
||
var matches = Directory.GetFiles(dir, filePattern, SearchOption.TopDirectoryOnly);
|
||
var validMatch = matches.FirstOrDefault(File.Exists);
|
||
if (validMatch != null)
|
||
{
|
||
return validMatch;
|
||
}
|
||
}
|
||
}
|
||
else if (File.Exists(expandedPattern))
|
||
{
|
||
return expandedPattern;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略搜索错误
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归搜索
|
||
/// </summary>
|
||
private string? SearchRecursively(SearchContext context)
|
||
{
|
||
try
|
||
{
|
||
var searchDirs = new[] { _appRoot, Path.GetFullPath(Path.Combine(_appRoot, "..")) };
|
||
|
||
foreach (var searchDir in searchDirs.Where(Directory.Exists))
|
||
{
|
||
var result = SearchDirectoryRecursively(searchDir, context.ExecutableName, 0);
|
||
if (result != null)
|
||
{
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略递归搜索错误
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归搜索目录
|
||
/// </summary>
|
||
private string? SearchDirectoryRecursively(string dir, string executableName, int depth)
|
||
{
|
||
if (depth > _options.MaxRecursionDepth)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 检查当前目录
|
||
var directPath = Path.Combine(dir, executableName);
|
||
if (File.Exists(directPath))
|
||
{
|
||
return directPath;
|
||
}
|
||
|
||
// 检查子目录
|
||
foreach (var subDir in Directory.GetDirectories(dir))
|
||
{
|
||
// 跳过某些目录
|
||
var dirName = Path.GetFileName(subDir).ToLowerInvariant();
|
||
if (dirName is ".git" or "node_modules" or ".vs" or "obj" or ".launcher")
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var result = SearchDirectoryRecursively(subDir, executableName, depth + 1);
|
||
if (result != null)
|
||
{
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略访问错误
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查找解决方案根目录
|
||
/// </summary>
|
||
private string? FindSolutionRoot(string startDir)
|
||
{
|
||
var current = new DirectoryInfo(startDir);
|
||
while (current != null)
|
||
{
|
||
// 查找 .sln 文件
|
||
if (current.GetFiles("*.sln").Any())
|
||
{
|
||
return current.FullName;
|
||
}
|
||
|
||
// 查找 .git 目录作为备选
|
||
if (current.GetDirectories(".git").Any())
|
||
{
|
||
return current.FullName;
|
||
}
|
||
|
||
current = current.Parent;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搜索 bin 目录
|
||
/// </summary>
|
||
private IEnumerable<string> SearchBinDirectories(string root, string executableName)
|
||
{
|
||
var results = new List<string>();
|
||
|
||
try
|
||
{
|
||
// 查找所有 bin 目录
|
||
var binDirs = Directory.GetDirectories(root, "bin", SearchOption.AllDirectories);
|
||
|
||
foreach (var binDir in binDirs)
|
||
{
|
||
// 检查 Debug 和 Release 子目录
|
||
var configDirs = new[] { "Debug", "Release" };
|
||
foreach (var config in configDirs)
|
||
{
|
||
var configPath = Path.Combine(binDir, config);
|
||
if (Directory.Exists(configPath))
|
||
{
|
||
// 检查所有 net* 子目录
|
||
var frameworkDirs = Directory.GetDirectories(configPath, "net*");
|
||
foreach (var fwDir in frameworkDirs)
|
||
{
|
||
var exePath = Path.Combine(fwDir, executableName);
|
||
if (File.Exists(exePath))
|
||
{
|
||
results.Add(exePath);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略搜索错误
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证路径并返回
|
||
/// </summary>
|
||
private string? ValidateAndReturn(string path, string source)
|
||
{
|
||
if (File.Exists(path))
|
||
{
|
||
Debug.WriteLine($"Found host executable from {source}: {path}");
|
||
return path;
|
||
}
|
||
|
||
// 尝试添加 .exe(Windows)
|
||
if (OperatingSystem.IsWindows() && !path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var withExe = path + ".exe";
|
||
if (File.Exists(withExe))
|
||
{
|
||
Debug.WriteLine($"Found host executable from {source}: {withExe}");
|
||
return withExe;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取可执行文件名
|
||
/// </summary>
|
||
private string GetExecutableName()
|
||
{
|
||
var name = _options.ExecutableName;
|
||
if (OperatingSystem.IsWindows() && !name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
name += ".exe";
|
||
}
|
||
return name;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 展开路径变量
|
||
/// </summary>
|
||
private string ExpandVariables(string path)
|
||
{
|
||
return path
|
||
.Replace("${AppRoot}", _appRoot)
|
||
.Replace("${BaseDirectory}", AppContext.BaseDirectory)
|
||
.Replace("${UserProfile}", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
|
||
.Replace("${LocalAppData}", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从目录名解析版本
|
||
/// </summary>
|
||
private static Version ParseVersionFromDirectoryName(string path)
|
||
{
|
||
var fileName = Path.GetFileName(path);
|
||
if (string.IsNullOrWhiteSpace(fileName))
|
||
{
|
||
return new Version(0, 0, 0);
|
||
}
|
||
|
||
var segments = fileName.Split('-');
|
||
if (segments.Length < 2)
|
||
{
|
||
return new Version(0, 0, 0);
|
||
}
|
||
|
||
return Version.TryParse(segments[1], out var version) ? version : new Version(0, 0, 0);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搜索上下文
|
||
/// </summary>
|
||
private class SearchContext
|
||
{
|
||
public required string ExecutableName { get; set; }
|
||
public required string AppRoot { get; set; }
|
||
public required HostDiscoveryOptions Options { get; set; }
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发现配置文件
|
||
/// </summary>
|
||
internal class HostDiscoveryConfig
|
||
{
|
||
public string? HostPath { get; set; }
|
||
public List<string>? AdditionalPaths { get; set; }
|
||
}
|