mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-25 03:04:26 +08:00
refactor(launcher): reorganize into responsibility folders
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
637
LanMountainDesktop.Launcher/Deployment/DeploymentLocator.cs
Normal file
637
LanMountainDesktop.Launcher/Deployment/DeploymentLocator.cs
Normal file
@@ -0,0 +1,637 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Deployment;
|
||||
|
||||
internal sealed class DeploymentLocator
|
||||
{
|
||||
private readonly string _appRoot;
|
||||
|
||||
public DeploymentLocator(string appRoot)
|
||||
{
|
||||
_appRoot = appRoot;
|
||||
}
|
||||
|
||||
public string GetAppRoot() => _appRoot;
|
||||
|
||||
public string? FindCurrentDeploymentDirectory()
|
||||
{
|
||||
Console.WriteLine("[DeploymentLocator] Searching for deployment directories (ClassIsland style)...");
|
||||
|
||||
if (!Directory.Exists(_appRoot))
|
||||
{
|
||||
Console.WriteLine("[DeploymentLocator] App root directory does not exist");
|
||||
return null;
|
||||
}
|
||||
|
||||
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
|
||||
|
||||
try
|
||||
{
|
||||
var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly);
|
||||
Console.WriteLine($"[DeploymentLocator] Found {candidates.Length} app-* directories");
|
||||
|
||||
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 HostResolutionResult ResolveHostExecutable(CommandContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
|
||||
var searchedPaths = new List<string>();
|
||||
var explicitAppRoot = context.ExplicitAppRoot;
|
||||
var devModeConfigIgnored = !context.IsDebugMode && Views.ErrorWindow.CheckDevModeEnabled();
|
||||
|
||||
string? resolvedPath;
|
||||
string? source;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(explicitAppRoot))
|
||||
{
|
||||
var explicitRoot = Path.GetFullPath(explicitAppRoot);
|
||||
resolvedPath = TryResolveExplicitAppRoot(explicitRoot, executable, searchedPaths, out source);
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPath = TryResolvePublishedOrPortableHost(executable, searchedPaths, out source);
|
||||
}
|
||||
|
||||
if (resolvedPath is null && context.IsDebugMode)
|
||||
{
|
||||
resolvedPath = TryResolveDebugHost(executable, searchedPaths, out source);
|
||||
}
|
||||
|
||||
if (resolvedPath is null)
|
||||
{
|
||||
resolvedPath = ResolveHostExecutablePathLegacy();
|
||||
if (!string.IsNullOrWhiteSpace(resolvedPath))
|
||||
{
|
||||
searchedPaths.Add(Path.GetFullPath(resolvedPath));
|
||||
source = "legacy_fallback";
|
||||
}
|
||||
}
|
||||
|
||||
return new HostResolutionResult
|
||||
{
|
||||
Success = !string.IsNullOrWhiteSpace(resolvedPath),
|
||||
ResolvedHostPath = resolvedPath,
|
||||
ResolutionSource = source,
|
||||
AppRoot = _appRoot,
|
||||
ExplicitAppRoot = explicitAppRoot,
|
||||
DevModeConfigIgnored = devModeConfigIgnored,
|
||||
SearchedPaths = searchedPaths
|
||||
.Where(path => !string.IsNullOrWhiteSpace(path))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public string? ResolveHostExecutablePath()
|
||||
{
|
||||
return ResolveHostExecutablePathLegacy();
|
||||
}
|
||||
|
||||
private string? TryResolveExplicitAppRoot(
|
||||
string explicitRoot,
|
||||
string executable,
|
||||
List<string> searchedPaths,
|
||||
out string? source)
|
||||
{
|
||||
var directPath = Path.Combine(explicitRoot, executable);
|
||||
searchedPaths.Add(directPath);
|
||||
if (File.Exists(directPath))
|
||||
{
|
||||
source = "explicit_app_root_direct";
|
||||
return directPath;
|
||||
}
|
||||
|
||||
var deployment = FindBestDeploymentHost(explicitRoot, executable, searchedPaths);
|
||||
if (deployment is not null)
|
||||
{
|
||||
source = "explicit_app_root_deployment";
|
||||
return deployment;
|
||||
}
|
||||
|
||||
source = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? TryResolvePublishedOrPortableHost(
|
||||
string executable,
|
||||
List<string> searchedPaths,
|
||||
out string? source)
|
||||
{
|
||||
var deployment = FindBestDeploymentHost(_appRoot, executable, searchedPaths);
|
||||
if (deployment is not null)
|
||||
{
|
||||
source = "published_deployment";
|
||||
return deployment;
|
||||
}
|
||||
|
||||
var portableCandidates = new[]
|
||||
{
|
||||
Path.Combine(_appRoot, executable),
|
||||
Path.Combine(AppContext.BaseDirectory, executable)
|
||||
};
|
||||
|
||||
foreach (var candidate in portableCandidates
|
||||
.Select(Path.GetFullPath)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
searchedPaths.Add(candidate);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
source = "portable_host";
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
source = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? TryResolveDebugHost(
|
||||
string executable,
|
||||
List<string> searchedPaths,
|
||||
out string? source)
|
||||
{
|
||||
if (Views.ErrorWindow.CheckDevModeEnabled())
|
||||
{
|
||||
var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath();
|
||||
if (!string.IsNullOrWhiteSpace(savedCustomPath))
|
||||
{
|
||||
if (TryNormalizeSavedDebugPath(savedCustomPath, out var fullSavedPath))
|
||||
{
|
||||
searchedPaths.Add(fullSavedPath);
|
||||
if (File.Exists(fullSavedPath))
|
||||
{
|
||||
source = "debug_saved_custom_path";
|
||||
return fullSavedPath;
|
||||
}
|
||||
|
||||
Logger.Warn($"Saved launcher debug host path is invalid; falling back to development paths. Path='{fullSavedPath}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var devPath in GetDevelopmentPaths(executable))
|
||||
{
|
||||
var fullPath = Path.GetFullPath(devPath);
|
||||
searchedPaths.Add(fullPath);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
source = "debug_build_output";
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
source = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryNormalizeSavedDebugPath(string savedPath, out string fullSavedPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
fullSavedPath = Path.GetFullPath(savedPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
fullSavedPath = string.Empty;
|
||||
Logger.Warn($"Saved launcher debug host path is invalid and cannot be normalized; falling back to development paths. Path='{savedPath}'; Error='{ex.Message}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? FindBestDeploymentHost(
|
||||
string root,
|
||||
string executable,
|
||||
List<string> searchedPaths)
|
||||
{
|
||||
if (!Directory.Exists(root))
|
||||
{
|
||||
searchedPaths.Add(Path.Combine(root, "app-*", executable));
|
||||
return null;
|
||||
}
|
||||
|
||||
var appDirs = Directory.GetDirectories(root, "app-*", SearchOption.TopDirectoryOnly)
|
||||
.Where(path => !File.Exists(Path.Combine(path, ".destroy")))
|
||||
.Where(path => !File.Exists(Path.Combine(path, ".partial")))
|
||||
.Select(path => new
|
||||
{
|
||||
Path = path,
|
||||
HostPath = Path.Combine(path, executable),
|
||||
HasCurrent = File.Exists(Path.Combine(path, ".current")),
|
||||
Version = ParseVersionFromDirectory(path)
|
||||
})
|
||||
.OrderByDescending(item => item.HasCurrent)
|
||||
.ThenByDescending(item => item.Version)
|
||||
.ToList();
|
||||
|
||||
foreach (var candidate in appDirs)
|
||||
{
|
||||
searchedPaths.Add(candidate.HostPath);
|
||||
if (File.Exists(candidate.HostPath))
|
||||
{
|
||||
return candidate.HostPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (appDirs.Count == 0)
|
||||
{
|
||||
searchedPaths.Add(Path.Combine(root, "app-*", executable));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ResolveHostExecutablePathLegacy()
|
||||
{
|
||||
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
|
||||
|
||||
// 1. 棣栧厛鏌ユ壘 app-{version} 鐩綍锛堢敓浜х幆澧冿級
|
||||
var currentDeployment = FindCurrentDeploymentDirectory();
|
||||
if (!string.IsNullOrWhiteSpace(currentDeployment))
|
||||
{
|
||||
var inDeployment = Path.Combine(currentDeployment, executable);
|
||||
if (File.Exists(inDeployment))
|
||||
{
|
||||
return inDeployment;
|
||||
}
|
||||
}
|
||||
|
||||
var inRoot = Path.Combine(_appRoot, executable);
|
||||
if (File.Exists(inRoot))
|
||||
{
|
||||
return inRoot;
|
||||
}
|
||||
|
||||
var parent = Path.GetFullPath(Path.Combine(_appRoot, ".."));
|
||||
var inParent = Path.Combine(parent, executable);
|
||||
if (File.Exists(inParent))
|
||||
{
|
||||
return inParent;
|
||||
}
|
||||
|
||||
// 4. 寮€鍙戞ā寮忥細濡傛灉鍚敤浜嗗紑鍙戞ā寮忥紝浼樺厛浣跨敤淇濆瓨鐨勮嚜瀹氫箟璺緞
|
||||
if (Views.ErrorWindow.CheckDevModeEnabled())
|
||||
{
|
||||
var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath();
|
||||
if (!string.IsNullOrWhiteSpace(savedCustomPath))
|
||||
{
|
||||
if (TryNormalizeSavedDebugPath(savedCustomPath, out var fullSavedPath) &&
|
||||
File.Exists(fullSavedPath))
|
||||
{
|
||||
return fullSavedPath;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(fullSavedPath))
|
||||
{
|
||||
Logger.Warn($"Saved launcher debug host path is invalid; falling back to development paths. Path='{fullSavedPath}'.");
|
||||
}
|
||||
}
|
||||
|
||||
var devPath = ScanDevelopmentPaths(executable);
|
||||
if (!string.IsNullOrWhiteSpace(devPath))
|
||||
{
|
||||
return devPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 寮€鍙戞ā寮忥細鏌ユ壘涓荤▼搴忛」鐩殑杈撳嚭鐩綍
|
||||
var devPaths = GetDevelopmentPaths(executable);
|
||||
foreach (var devPath in devPaths)
|
||||
{
|
||||
if (File.Exists(devPath))
|
||||
{
|
||||
return devPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鎵弿寮€鍙戣矾寰勶紙寮€鍙戞ā寮忥級
|
||||
/// </summary>
|
||||
private static string? ScanDevelopmentPaths(string executable)
|
||||
{
|
||||
var solutionRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
|
||||
|
||||
var possiblePaths = new[]
|
||||
{
|
||||
// 标准开发路径:解决方案根目录下的 LanMountainDesktop 项目
|
||||
Path.Combine(solutionRoot, "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
|
||||
Path.Combine(solutionRoot, "LanMountainDesktop", "bin", "Release", "net10.0", executable),
|
||||
|
||||
// 向后兼容
|
||||
Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
|
||||
Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
|
||||
|
||||
// dev-test 目录
|
||||
Path.Combine(solutionRoot, "dev-test", "app-1.0.0-dev", executable),
|
||||
};
|
||||
|
||||
foreach (var path in possiblePaths.Select(Path.GetFullPath).Distinct())
|
||||
{
|
||||
Logger.Info($"Scanning development path: {path}");
|
||||
if (File.Exists(path))
|
||||
{
|
||||
Logger.Info($"Found host at: {path}");
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鑾峰彇寮€鍙戞ā寮忥細鏌ユ壘涓荤▼搴忚経
|
||||
/// </summary>
|
||||
private static IEnumerable<string> GetDevelopmentPaths(string executable)
|
||||
{
|
||||
var launcherDir = AppContext.BaseDirectory;
|
||||
|
||||
// 计算解决方案根目录:从 LanMountainDesktop.Launcher\bin\Debug\net10.0\ 向上4级
|
||||
var solutionRoot = Path.GetFullPath(Path.Combine(launcherDir, "..", "..", "..", ".."));
|
||||
|
||||
var possiblePaths = new[]
|
||||
{
|
||||
// 标准开发路径:解决方案根目录下的 LanMountainDesktop 项目
|
||||
Path.Combine(solutionRoot, "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
|
||||
Path.Combine(solutionRoot, "LanMountainDesktop", "bin", "Release", "net10.0", executable),
|
||||
|
||||
// 向后兼容:如果 Launcher 在特殊目录结构中
|
||||
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
|
||||
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
|
||||
|
||||
// dev-test 目录
|
||||
Path.Combine(solutionRoot, "dev-test", "app-1.0.0-dev", executable),
|
||||
};
|
||||
|
||||
return possiblePaths.Select(Path.GetFullPath).Distinct();
|
||||
}
|
||||
|
||||
public string GetCurrentVersion()
|
||||
{
|
||||
var deployment = FindCurrentDeploymentDirectory();
|
||||
if (string.IsNullOrWhiteSpace(deployment))
|
||||
{
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
return ParseVersionTextFromDirectory(deployment) ?? "0.0.0";
|
||||
}
|
||||
|
||||
public string BuildNextDeploymentDirectory(string targetVersion)
|
||||
{
|
||||
var sanitized = string.IsNullOrWhiteSpace(targetVersion) ? "0.0.0" : targetVersion.Trim();
|
||||
var index = 0;
|
||||
while (true)
|
||||
{
|
||||
var candidate = Path.Combine(_appRoot, $"app-{sanitized}-{index.ToString(CultureInfo.InvariantCulture)}");
|
||||
if (!Directory.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 娓呯悊鏃х増鏈儴缃诧紝淇濈暀鏈€杩戠殑N涓増鏈? /// </summary>
|
||||
/// <param name="minVersionsToKeep">鏈€灏戜繚鐣欑増鏈暟锛岄粯璁?涓?/param>
|
||||
public void CleanupOldDeployments(int minVersionsToKeep = 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[DeploymentLocator] Starting cleanup with retention policy: keep at least {minVersionsToKeep} versions");
|
||||
|
||||
if (!Directory.Exists(_appRoot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var candidates = Directory.GetDirectories(_appRoot, "app-*", SearchOption.TopDirectoryOnly);
|
||||
|
||||
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 resolver = new DataLocationResolver(_appRoot);
|
||||
var snapshotDir = Path.Combine(resolver.ResolveLauncherDataPath(), "snapshots");
|
||||
if (Directory.Exists(snapshotDir))
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshotFiles = Directory
|
||||
.GetFiles(snapshotDir, "*.json", SearchOption.TopDirectoryOnly)
|
||||
.OrderByDescending(File.GetCreationTimeUtc)
|
||||
.Take(Math.Max(1, minVersionsToKeep))
|
||||
.ToArray();
|
||||
foreach (var snapshotFile in snapshotFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(snapshotFile);
|
||||
var snapshot = System.Text.Json.JsonSerializer.Deserialize(json, AppJsonContext.Default.SnapshotMetadata);
|
||||
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
|
||||
{
|
||||
// 蹇界暐蹇収瑙f瀽閿欒
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 蹇界暐蹇収鐩綍璁块棶閿欒
|
||||
}
|
||||
}
|
||||
|
||||
// 娓呯悊涓嶉渶瑕佺殑鐗堟湰
|
||||
foreach (var deployment in validDeployments)
|
||||
{
|
||||
if (versionsToKeep.Contains(deployment.Path))
|
||||
{
|
||||
if (deployment.IsDestroyed)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(Path.Combine(deployment.Path, ".destroy"));
|
||||
Console.WriteLine($"[DeploymentLocator] Unmarked for deletion (kept): {deployment.Path}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 蹇界暐鍙栨秷鏍囪澶辫触
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deployment.IsDestroyed)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(deployment.Path, ".destroy"), string.Empty);
|
||||
Console.WriteLine($"[DeploymentLocator] Marked for deletion: {deployment.Path}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 蹇界暐鏍囪澶辫触
|
||||
}
|
||||
}
|
||||
|
||||
// 灏濊瘯鍒犻櫎
|
||||
try
|
||||
{
|
||||
Directory.Delete(deployment.Path, recursive: true);
|
||||
Console.WriteLine($"[DeploymentLocator] Deleted: {deployment.Path}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 蹇界暐鍒犻櫎澶辫触(鍙兘鏂囦欢琚崰鐢?,涓嬫鍚姩鍐嶈瘯
|
||||
Console.WriteLine($"[DeploymentLocator] Failed to delete (will retry later): {deployment.Path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DeploymentLocator] Cleanup failed: {ex.Message}");
|
||||
// 蹇界暐娓呯悊澶辫触
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浠呮竻鐞嗗凡鏍囪涓?destroy鐨勯儴缃诧紙鍏煎鏃ф柟娉曪級
|
||||
/// </summary>
|
||||
[Obsolete("Use CleanupOldDeployments instead")]
|
||||
public void CleanupDestroyedDeployments()
|
||||
{
|
||||
CleanupOldDeployments(3);
|
||||
}
|
||||
|
||||
public static Version ParseVersionFromDirectory(string path)
|
||||
{
|
||||
var text = ParseVersionTextFromDirectory(path);
|
||||
return Version.TryParse(text, out var version) ? version : new Version(0, 0, 0);
|
||||
}
|
||||
|
||||
private static string? ParseVersionTextFromDirectory(string path)
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var segments = fileName.Split('-');
|
||||
if (segments.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return segments[1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浠庨儴缃茬洰褰曡鍙栫増鏈俊鎭? /// </summary>
|
||||
public AppVersionInfo GetVersionInfo()
|
||||
{
|
||||
var executableName = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
|
||||
var resolved = AppVersionProvider.ResolveFromPackageRoot(_appRoot, executableName);
|
||||
return string.IsNullOrWhiteSpace(resolved.Version)
|
||||
? new AppVersionInfo
|
||||
{
|
||||
Version = GetCurrentVersion(),
|
||||
Codename = "Administrate"
|
||||
}
|
||||
: resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace LanMountainDesktop.Launcher.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// 主程序发现选项
|
||||
/// </summary>
|
||||
public sealed class HostDiscoveryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 可执行文件名(Windows 下自动添加 .exe)
|
||||
/// </summary>
|
||||
public string ExecutableName { get; set; } = "LanMountainDesktop";
|
||||
|
||||
/// <summary>
|
||||
/// 额外的搜索路径(支持通配符)
|
||||
/// </summary>
|
||||
public List<string> AdditionalSearchPaths { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 是否递归搜索子目录
|
||||
/// </summary>
|
||||
public bool RecursiveSearch { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 递归搜索的最大深度
|
||||
/// </summary>
|
||||
public int MaxRecursionDepth { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 环境变量名称,用于指定自定义路径
|
||||
/// </summary>
|
||||
public string? CustomPathEnvVar { get; set; } = "LMD_HOST_PATH";
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件路径(相对于 app root)
|
||||
/// </summary>
|
||||
public string? ConfigFileName { get; set; } = "host-discovery.json";
|
||||
|
||||
/// <summary>
|
||||
/// 是否优先使用开发模式配置
|
||||
/// </summary>
|
||||
public bool PreferDevModeConfig { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 搜索超时(毫秒)
|
||||
/// </summary>
|
||||
public int SearchTimeoutMs { get; set; } = 5000;
|
||||
}
|
||||
213
LanMountainDesktop.Launcher/Deployment/HostLaunchPlan.cs
Normal file
213
LanMountainDesktop.Launcher/Deployment/HostLaunchPlan.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Deployment;
|
||||
|
||||
internal sealed record HostLaunchPlan(
|
||||
string HostPath,
|
||||
string PackageRoot,
|
||||
string WorkingDirectory,
|
||||
IReadOnlyList<string> Arguments,
|
||||
IReadOnlyDictionary<string, string> EnvironmentVariables,
|
||||
AppVersionInfo VersionInfo);
|
||||
|
||||
internal static class HostLaunchPlanBuilder
|
||||
{
|
||||
public const string DataRootOptionName = "data-root";
|
||||
|
||||
private static readonly string[] LauncherOnlyOptions =
|
||||
[
|
||||
"debug", "show-loading-details", "plugins-dir", "source", "result",
|
||||
"app-root", DataRootOptionName,
|
||||
LauncherIpcConstants.LauncherPidEnvVar,
|
||||
LauncherIpcConstants.PackageRootEnvVar,
|
||||
LauncherIpcConstants.VersionEnvVar,
|
||||
LauncherIpcConstants.CodenameEnvVar
|
||||
];
|
||||
|
||||
public static HostLaunchPlan Build(
|
||||
CommandContext context,
|
||||
DeploymentLocator deploymentLocator,
|
||||
HostResolutionResult resolution,
|
||||
string? dataRoot = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(deploymentLocator);
|
||||
ArgumentNullException.ThrowIfNull(resolution);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(resolution.ResolvedHostPath))
|
||||
{
|
||||
throw new InvalidOperationException("Host path must be resolved before building a launch plan.");
|
||||
}
|
||||
|
||||
var hostPath = Path.GetFullPath(resolution.ResolvedHostPath);
|
||||
var packageRoot = ResolvePackageRoot(hostPath, resolution.AppRoot, resolution.ResolutionSource);
|
||||
var versionInfo = deploymentLocator.GetVersionInfo();
|
||||
var arguments = BuildForwardedArguments(context, packageRoot, versionInfo, dataRoot);
|
||||
var environment = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[LauncherIpcConstants.LauncherPidEnvVar] = Environment.ProcessId.ToString(),
|
||||
[LauncherIpcConstants.PackageRootEnvVar] = packageRoot,
|
||||
[LauncherIpcConstants.VersionEnvVar] = versionInfo.Version,
|
||||
[LauncherIpcConstants.CodenameEnvVar] = versionInfo.Codename
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dataRoot))
|
||||
{
|
||||
environment["LMD_DATA_ROOT"] = dataRoot;
|
||||
}
|
||||
|
||||
return new HostLaunchPlan(
|
||||
hostPath,
|
||||
packageRoot,
|
||||
Directory.Exists(packageRoot)
|
||||
? packageRoot
|
||||
: Path.GetDirectoryName(hostPath) ?? AppContext.BaseDirectory,
|
||||
arguments,
|
||||
environment,
|
||||
versionInfo);
|
||||
}
|
||||
|
||||
public static string FormatArgumentsForLog(IReadOnlyList<string> arguments)
|
||||
{
|
||||
return string.Join(" ", arguments.Select(QuoteArgument));
|
||||
}
|
||||
|
||||
private static string ResolvePackageRoot(string hostPath, string appRoot, string? resolutionSource)
|
||||
{
|
||||
var fullAppRoot = string.IsNullOrWhiteSpace(appRoot)
|
||||
? AppContext.BaseDirectory
|
||||
: Path.GetFullPath(appRoot);
|
||||
|
||||
var hostDirectory = Path.GetDirectoryName(hostPath);
|
||||
if (hostDirectory is not null &&
|
||||
Directory.Exists(fullAppRoot) &&
|
||||
IsAppDeploymentDirectory(hostDirectory) &&
|
||||
IsParentOf(fullAppRoot, hostDirectory))
|
||||
{
|
||||
return fullAppRoot;
|
||||
}
|
||||
|
||||
if (string.Equals(resolutionSource, "published_deployment", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(resolutionSource, "explicit_app_root_deployment", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(resolutionSource, "legacy_fallback", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return fullAppRoot;
|
||||
}
|
||||
|
||||
return hostDirectory ?? fullAppRoot;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildForwardedArguments(
|
||||
CommandContext context,
|
||||
string packageRoot,
|
||||
AppVersionInfo versionInfo,
|
||||
string? dataRoot = null)
|
||||
{
|
||||
var arguments = new List<string>();
|
||||
|
||||
for (var index = 0; index < context.RawArgs.Count; index++)
|
||||
{
|
||||
var arg = context.RawArgs[index];
|
||||
|
||||
if (index == 0 &&
|
||||
!arg.StartsWith("--", StringComparison.Ordinal) &&
|
||||
string.Equals(arg, context.Command, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index == 1 &&
|
||||
!arg.StartsWith("--", StringComparison.Ordinal) &&
|
||||
string.Equals(arg, context.SubCommand, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
var key = arg[2..];
|
||||
var equalsIndex = key.IndexOf('=');
|
||||
if (equalsIndex >= 0)
|
||||
{
|
||||
key = key[..equalsIndex];
|
||||
}
|
||||
|
||||
if (LauncherOnlyOptions.Contains(key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (equalsIndex < 0 &&
|
||||
index + 1 < context.RawArgs.Count &&
|
||||
!context.RawArgs[index + 1].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
arguments.Add(arg);
|
||||
}
|
||||
|
||||
arguments.Add($"--{LauncherIpcConstants.LauncherPidEnvVar}={Environment.ProcessId}");
|
||||
arguments.Add($"--{LauncherIpcConstants.PackageRootEnvVar}={packageRoot}");
|
||||
arguments.Add($"--{LauncherIpcConstants.VersionEnvVar}={versionInfo.Version}");
|
||||
arguments.Add($"--{LauncherIpcConstants.CodenameEnvVar}={versionInfo.Codename}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dataRoot))
|
||||
{
|
||||
arguments.Add($"--{DataRootOptionName}={dataRoot}");
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
private static bool IsAppDeploymentDirectory(string path)
|
||||
{
|
||||
var fileName = Path.GetFileName(Path.TrimEndingDirectorySeparator(path));
|
||||
return fileName.StartsWith("app-", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsParentOf(string parent, string child)
|
||||
{
|
||||
var parentPath = Path.GetFullPath(parent).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
var childPath = Path.GetFullPath(child).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
if (string.Equals(parentPath, childPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return childPath.StartsWith(
|
||||
parentPath + Path.DirectorySeparatorChar,
|
||||
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string QuoteArgument(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
if (!value.Contains('"') && !value.Contains(' ') && !value.Contains('\t'))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var builder = new System.Text.StringBuilder();
|
||||
builder.Append('"');
|
||||
foreach (var ch in value)
|
||||
{
|
||||
if (ch == '"')
|
||||
{
|
||||
builder.Append("\\\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append('"');
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace LanMountainDesktop.Launcher.Deployment;
|
||||
|
||||
internal sealed class HostResolutionResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
|
||||
public string? ResolvedHostPath { get; init; }
|
||||
|
||||
public string? ResolutionSource { get; init; }
|
||||
|
||||
public string AppRoot { get; init; } = string.Empty;
|
||||
|
||||
public string? ExplicitAppRoot { get; init; }
|
||||
|
||||
public bool DevModeConfigIgnored { get; init; }
|
||||
|
||||
public List<string> SearchedPaths { get; init; } = [];
|
||||
}
|
||||
344
LanMountainDesktop.Launcher/Deployment/LegacyVersionDetector.cs
Normal file
344
LanMountainDesktop.Launcher/Deployment/LegacyVersionDetector.cs
Normal file
@@ -0,0 +1,344 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// 老版本检测器 - 检测 0.8.x 及更早的单应用模式安装
|
||||
/// </summary>
|
||||
internal sealed class LegacyVersionDetector
|
||||
{
|
||||
private const string LegacyAppName = "LanMountainDesktop";
|
||||
private const string LegacyExeName = "LanMountainDesktop.exe";
|
||||
|
||||
/// <summary>
|
||||
/// 检测是否存在老版本安装
|
||||
/// </summary>
|
||||
public static LegacyVersionInfo? DetectLegacyInstallation()
|
||||
{
|
||||
// 1. 检查注册表(安装版)
|
||||
var registryInfo = DetectFromRegistry();
|
||||
if (registryInfo != null)
|
||||
{
|
||||
return registryInfo;
|
||||
}
|
||||
|
||||
// 2. 检查常见安装目录
|
||||
var commonPaths = DetectFromCommonPaths();
|
||||
if (commonPaths != null)
|
||||
{
|
||||
return commonPaths;
|
||||
}
|
||||
|
||||
// 3. 检查便携版位置
|
||||
var portableInfo = DetectPortableInstallation();
|
||||
if (portableInfo != null)
|
||||
{
|
||||
return portableInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从注册表检测安装信息
|
||||
/// </summary>
|
||||
private static LegacyVersionInfo? DetectFromRegistry()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查 HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall
|
||||
using var key = Registry.LocalMachine.OpenSubKey(
|
||||
@$"Software\Microsoft\Windows\CurrentVersion\Uninstall\{LegacyAppName}");
|
||||
|
||||
if (key != null)
|
||||
{
|
||||
var installLocation = key.GetValue("InstallLocation") as string;
|
||||
var displayVersion = key.GetValue("DisplayVersion") as string;
|
||||
var uninstallString = key.GetValue("UninstallString") as string;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(installLocation) &&
|
||||
File.Exists(Path.Combine(installLocation, LegacyExeName)))
|
||||
{
|
||||
return new LegacyVersionInfo
|
||||
{
|
||||
Version = displayVersion ?? "0.8.x",
|
||||
InstallPath = installLocation,
|
||||
UninstallCommand = uninstallString,
|
||||
InstallType = LegacyInstallType.Registry
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 HKCU(用户级安装)
|
||||
using var userKey = Registry.CurrentUser.OpenSubKey(
|
||||
@$"Software\Microsoft\Windows\CurrentVersion\Uninstall\{LegacyAppName}");
|
||||
|
||||
if (userKey != null)
|
||||
{
|
||||
var installLocation = userKey.GetValue("InstallLocation") as string;
|
||||
var displayVersion = userKey.GetValue("DisplayVersion") as string;
|
||||
var uninstallString = userKey.GetValue("UninstallString") as string;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(installLocation) &&
|
||||
File.Exists(Path.Combine(installLocation, LegacyExeName)))
|
||||
{
|
||||
return new LegacyVersionInfo
|
||||
{
|
||||
Version = displayVersion ?? "0.8.x",
|
||||
InstallPath = installLocation,
|
||||
UninstallCommand = uninstallString,
|
||||
InstallType = LegacyInstallType.Registry
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[LegacyVersionDetector] Registry detection failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从常见安装路径检测
|
||||
/// </summary>
|
||||
private static LegacyVersionInfo? DetectFromCommonPaths()
|
||||
{
|
||||
var commonPaths = new[]
|
||||
{
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), LegacyAppName),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), LegacyAppName),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), LegacyAppName),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), LegacyAppName),
|
||||
};
|
||||
|
||||
foreach (var path in commonPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
// 检查是否存在老版本的特征文件(没有 app-* 目录)
|
||||
var exePath = Path.Combine(path, LegacyExeName);
|
||||
var hasAppDirs = Directory.GetDirectories(path, "app-*").Length > 0;
|
||||
|
||||
if (File.Exists(exePath) && !hasAppDirs)
|
||||
{
|
||||
// 尝试读取版本信息
|
||||
var version = TryGetFileVersion(exePath);
|
||||
|
||||
return new LegacyVersionInfo
|
||||
{
|
||||
Version = version ?? "0.8.x",
|
||||
InstallPath = path,
|
||||
UninstallCommand = FindUninstaller(path),
|
||||
InstallType = LegacyInstallType.CommonPath
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[LegacyVersionDetector] Path detection failed for {path}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测便携版安装
|
||||
/// </summary>
|
||||
private static LegacyVersionInfo? DetectPortableInstallation()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查启动器所在目录的父目录(便携版常见布局)
|
||||
var launcherDir = AppContext.BaseDirectory;
|
||||
var parentDir = Path.GetFullPath(Path.Combine(launcherDir, ".."));
|
||||
|
||||
if (Directory.Exists(parentDir))
|
||||
{
|
||||
var exePath = Path.Combine(parentDir, LegacyExeName);
|
||||
var hasAppDirs = Directory.GetDirectories(parentDir, "app-*").Length > 0;
|
||||
|
||||
// 如果存在 exe 且没有 app-* 目录,可能是老版本
|
||||
if (File.Exists(exePath) && !hasAppDirs)
|
||||
{
|
||||
var version = TryGetFileVersion(exePath);
|
||||
|
||||
// 检查是否真的是老版本(通过文件版本或特定标记)
|
||||
if (IsLegacyVersion(version))
|
||||
{
|
||||
return new LegacyVersionInfo
|
||||
{
|
||||
Version = version ?? "0.8.x",
|
||||
InstallPath = parentDir,
|
||||
UninstallCommand = null, // 便携版没有卸载程序
|
||||
InstallType = LegacyInstallType.Portable
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[LegacyVersionDetector] Portable detection failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找卸载程序
|
||||
/// </summary>
|
||||
private static string? FindUninstaller(string installPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 常见的卸载程序命名
|
||||
var uninstallerNames = new[] { "unins000.exe", "uninstall.exe", "Uninstall.exe" };
|
||||
|
||||
foreach (var name in uninstallerNames)
|
||||
{
|
||||
var path = Path.Combine(installPath, name);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件版本
|
||||
/// </summary>
|
||||
private static string? TryGetFileVersion(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
|
||||
return versionInfo.FileVersion;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为老版本(版本号 < 1.0.0)
|
||||
/// </summary>
|
||||
private static bool IsLegacyVersion(string? version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
return true; // 无法确定版本时,保守认为是老版本
|
||||
}
|
||||
|
||||
if (Version.TryParse(version.Split(' ')[0], out var v))
|
||||
{
|
||||
return v.Major < 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开卸载界面
|
||||
/// </summary>
|
||||
public static void OpenUninstallInterface(LegacyVersionInfo info)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.UninstallCommand))
|
||||
{
|
||||
// 有卸载命令,直接执行
|
||||
var parts = info.UninstallCommand.Split(new[] { ' ' }, 2);
|
||||
var fileName = parts[0].Trim('"');
|
||||
var arguments = parts.Length > 1 ? parts[1] : "";
|
||||
Logger.Info(
|
||||
$"Opening legacy uninstall interface with elevation reason 'legacy_uninstall'. " +
|
||||
$"InstallPath='{info.InstallPath}'; Version='{info.Version}'.");
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas" // 请求管理员权限
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有卸载命令,打开系统卸载面板
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "appwiz.cpl",
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[LegacyVersionDetector] Failed to open uninstall: {ex.Message}");
|
||||
|
||||
// 兜底:打开系统卸载面板
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "appwiz.cpl",
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在资源管理器中显示老版本位置
|
||||
/// </summary>
|
||||
public static void ShowInExplorer(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "explorer.exe",
|
||||
Arguments = $"/select,\"{path}\"",
|
||||
UseShellExecute = false
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[LegacyVersionDetector] Failed to show in explorer: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 老版本信息
|
||||
/// </summary>
|
||||
public class LegacyVersionInfo
|
||||
{
|
||||
public string Version { get; set; } = "0.8.x";
|
||||
public string InstallPath { get; set; } = "";
|
||||
public string? UninstallCommand { get; set; }
|
||||
public LegacyInstallType InstallType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 老版本安装类型
|
||||
/// </summary>
|
||||
public enum LegacyInstallType
|
||||
{
|
||||
Registry, // 注册表安装版
|
||||
CommonPath, // 常见路径安装
|
||||
Portable // 便携版
|
||||
}
|
||||
Reference in New Issue
Block a user