Stamp release versions and harden launcher

Add automatic release version stamping and multiple launcher reliability improvements. The Release workflow now runs scripts/Set-ReleaseVersion.ps1 in build jobs to inject tag-derived Version/AssemblyVersion into project metadata; several .csproj/Directory.Build.props and app.manifest files were changed to use a dev placeholder. Introduced AppVersionProvider (and related runtime metadata) to centralize version resolution and updated DeploymentLocator to use it and to prefer package-root/version.json. Launcher startup flow was hardened: added startup success tracking, public-activation recovery path, improved success/fallback semantics, and related IPC handling. UI/UX fixes include OOBE entrance/exit animation improvements (scaling-aware, concurrent fade+translate) and minor window lifecycle reorder in DesktopShellHost. CommandContext now recognizes restart and key=value args. New DesktopTrayService and .trae spec files (spec, checklist, tasks) document shell/tray hardening work. Miscellaneous logging, comments and housekeeping edits across launcher and shared contracts to support the above.
This commit is contained in:
lincube
2026-04-23 00:27:01 +08:00
parent e20462ac2b
commit 001d77968f
31 changed files with 1727 additions and 478 deletions

View File

@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Text.Json;
using LanMountainDesktop.Launcher.Models;
using LanMountainDesktop.Shared.Contracts.Launcher;
@@ -57,8 +57,8 @@ internal sealed class DeploymentLocator
Version = ParseVersionFromDirectory(path),
HasCurrentMarker = File.Exists(Path.Combine(path, ".current"))
})
.OrderBy(x => x.HasCurrentMarker ? 0 : 1) // .current 标记的æŽå‰<EFBFBD>é<EFBFBD>¢
.ThenByDescending(x => x.Version) // ç„¶å<EFBFBD>ŽæŒ‰ç‰ˆæœ¬å<EFBFBD>·é™<EFBFBD>åº<EFBFBD>
.OrderBy(x => x.HasCurrentMarker ? 0 : 1) // .current 鏍囪鐨勬帓鍓嶉潰
.ThenByDescending(x => x.Version) // 鐒跺悗鎸夌増鏈彿闄嶅簭
.ToList();
if (validInstallations.Count == 0)
@@ -275,7 +275,7 @@ internal sealed class DeploymentLocator
{
var executable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
// 1. 首先查找 app-{version} 目录(生产环境)
// 1. 棣栧厛鏌ユ壘 app-{version} 鐩綍锛堢敓浜х幆澧冿級
var currentDeployment = FindCurrentDeploymentDirectory();
if (!string.IsNullOrWhiteSpace(currentDeployment))
{
@@ -299,7 +299,7 @@ internal sealed class DeploymentLocator
return inParent;
}
// 4. å¼€å<EFBFBD>模å¼<EFBFBD>ï¼šå¦æžœå<EFBFBD>¯ç”¨äº†å¼€å<EFBFBD>模å¼<EFBFBD>,优先使用ä¿<EFBFBD>存的自定义路径
// 4. 寮€鍙戞ā寮忥細濡傛灉鍚敤浜嗗紑鍙戞ā寮忥紝浼樺厛浣跨敤淇濆瓨鐨勮嚜瀹氫箟璺緞
if (Views.ErrorWindow.CheckDevModeEnabled())
{
var savedCustomPath = Views.ErrorWindow.GetSavedCustomHostPath();
@@ -315,7 +315,7 @@ internal sealed class DeploymentLocator
}
}
// 5. å¼€å<EFBFBD>模å¼<EFBFBD>:查找主ç¨åº<EFBFBD>项ç®çš„输出ç®å½•
// 5. 寮€鍙戞ā寮忥細鏌ユ壘涓荤▼搴忛」鐩殑杈撳嚭鐩綍
var devPaths = GetDevelopmentPaths(executable);
foreach (var devPath in devPaths)
{
@@ -329,21 +329,21 @@ internal sealed class DeploymentLocator
}
/// <summary>
/// 扫æ<EFBFBD><EFBFBD>å¼€å<EFBFBD>路径(开å<EFBFBD>模å¼<EFBFBD>)
/// 鎵弿寮€鍙戣矾寰勶紙寮€鍙戞ā寮忥級
/// </summary>
private static string? ScanDevelopmentPaths(string executable)
{
var possiblePaths = new[]
{
// ä»?Launcher 项ç®è¿<EFBFBD>行
// ?Launcher 椤圭洰杩愯
Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
Path.Combine(AppContext.BaseDirectory, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
// ä»Žè§£å†³æ¹æ¡ˆæ ¹ç®å½•è¿<EFBFBD>行
// 浠庤В鍐虫柟妗堟牴鐩綍杩愯
Path.Combine(AppContext.BaseDirectory, "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
Path.Combine(AppContext.BaseDirectory, "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
// dev-test 目录
// dev-test 鐩綍
Path.Combine(AppContext.BaseDirectory, "..", "dev-test", "app-1.0.0-dev", executable),
};
@@ -359,22 +359,22 @@ internal sealed class DeploymentLocator
}
/// <summary>
/// 获å<EFBFBD>å¼€å<EFBFBD>环境å<EFBFBD>¯èƒ½çš„主ç¨åº<EFBFBD>è·¯å¾? /// </summary>
/// 鑾峰彇寮€鍙戠幆澧冨彲鑳界殑涓荤▼搴忚矾寰? /// </summary>
private static IEnumerable<string> GetDevelopmentPaths(string executable)
{
var launcherDir = AppContext.BaseDirectory;
var possiblePaths = new[]
{
// ä»?Launcher 项ç®è¿<EFBFBD>行ï¼?.\LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe
// ?Launcher 椤圭洰杩愯锛?.\LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
Path.Combine(launcherDir, "..", "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
// ä»Žè§£å†³æ¹æ¡ˆæ ¹ç®å½•è¿<EFBFBD>行:LanMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe
// 浠庤В鍐虫柟妗堟牴鐩綍杩愯锛歀anMountainDesktop\bin\Debug\net10.0\LanMountainDesktop.exe
Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Debug", "net10.0", executable),
Path.Combine(launcherDir, "..", "LanMountainDesktop", "bin", "Release", "net10.0", executable),
// ä»?dev-test ç®å½•è¿<EFBFBD>行
// ?dev-test 鐩綍杩愯
Path.Combine(launcherDir, "..", "dev-test", "app-1.0.0-dev", executable),
};
@@ -409,8 +409,8 @@ internal sealed class DeploymentLocator
}
/// <summary>
/// 清ç<EFBFBD>†æ—§ç‰ˆæœ¬éƒ¨ç½²ï¼Œä¿<EFBFBD>留最è¿çš„N个版æœ? /// </summary>
/// <param name="minVersionsToKeep">最å°ä¿<EFBFBD>留版本数,默è®?ä¸?/param>
/// 娓呯悊鏃х増鏈儴缃诧紝淇濈暀鏈€杩戠殑N涓増鏈? /// </summary>
/// <param name="minVersionsToKeep">鏈€灏戜繚鐣欑増鏈暟锛岄粯璁?涓?/param>
public void CleanupOldDeployments(int minVersionsToKeep = 3)
{
try
@@ -438,10 +438,10 @@ internal sealed class DeploymentLocator
Console.WriteLine($"[DeploymentLocator] Found {validDeployments.Count} valid deployments");
// 确定è¦<EFBFBD>ä¿<EFBFBD>留的版本
// 纭畾瑕佷繚鐣欑殑鐗堟湰
var versionsToKeep = new HashSet<string>();
// 1. 总是ä¿<EFBFBD>留当å‰<EFBFBD>版本
// 1. 鎬绘槸淇濈暀褰撳墠鐗堟湰
var currentVersion = validDeployments.FirstOrDefault(d => d.IsCurrent);
if (currentVersion != null)
{
@@ -449,7 +449,7 @@ internal sealed class DeploymentLocator
Console.WriteLine($"[DeploymentLocator] Keep current version: {currentVersion.Path}");
}
// 2. ä¿<EFBFBD>留最è¿çš„N个有效版本(ä¸<EFBFBD>包æ¬å·²æ ‡è®°destroy的)
// 2. 淇濈暀鏈€杩戠殑N涓湁鏁堢増鏈紙涓嶅寘鎷凡鏍囪destroy鐨勶級
var activeVersions = validDeployments
.Where(d => !d.IsDestroyed)
.Take(minVersionsToKeep)
@@ -461,7 +461,7 @@ internal sealed class DeploymentLocator
Console.WriteLine($"[DeploymentLocator] Keep recent version: {ver.Path}");
}
// 3. ä¿<EFBFBD>ç•™æœ‰å¿«ç…§çš„ç‰ˆæœ¬ï¼ˆç”¨äºŽåžæ»šï¼‰
// 3. 淇濈暀鏈夊揩鐓х殑鐗堟湰锛堢敤浜庡洖婊氾級
var snapshotDir = Path.Combine(_appRoot, ".launcher", "snapshots");
if (Directory.Exists(snapshotDir))
{
@@ -485,17 +485,17 @@ internal sealed class DeploymentLocator
}
catch
{
// 忽略快照解æž<EFBFBD>错误
// 蹇界暐蹇収瑙f瀽閿欒
}
}
}
catch
{
// 忽略快照目录访问错误
// 蹇界暐蹇収鐩綍璁块棶閿欒
}
}
// 清ç<EFBFBD>†ä¸<EFBFBD>需è¦<EFBFBD>的版本
// 娓呯悊涓嶉渶瑕佺殑鐗堟湰
foreach (var deployment in validDeployments)
{
if (versionsToKeep.Contains(deployment.Path))
@@ -509,7 +509,7 @@ internal sealed class DeploymentLocator
}
catch
{
// 忽略å<EFBFBD>消标记失败
// 蹇界暐鍙栨秷鏍囪澶辫触
}
}
continue;
@@ -524,11 +524,11 @@ internal sealed class DeploymentLocator
}
catch
{
// 忽略标记失败
// 蹇界暐鏍囪澶辫触
}
}
// å°<EFBFBD>试删除
// 灏濊瘯鍒犻櫎
try
{
Directory.Delete(deployment.Path, recursive: true);
@@ -536,7 +536,7 @@ internal sealed class DeploymentLocator
}
catch
{
// 忽略删除失败(å<>¯èƒ½æ‡ä»¶è¢«å<C2AB> ç”?,䏿¬¡å<C2A1>¯åЍå†<C3A5>试
// 蹇界暐鍒犻櫎澶辫触(鍙兘鏂囦欢琚崰鐢?,涓嬫鍚姩鍐嶈瘯
Console.WriteLine($"[DeploymentLocator] Failed to delete (will retry later): {deployment.Path}");
}
}
@@ -544,12 +544,12 @@ internal sealed class DeploymentLocator
catch (Exception ex)
{
Console.Error.WriteLine($"[DeploymentLocator] Cleanup failed: {ex.Message}");
// 忽略清ç<EFBFBD>†å¤±è´¥
// 蹇界暐娓呯悊澶辫触
}
}
/// <summary>
/// 仅清ç<EFBFBD>†å·²æ ‡è®°ä¸?destroyçš„éƒ¨ç½²ï¼ˆå…¼å®¹æ—§æ¹æ³•)
/// 浠呮竻鐞嗗凡鏍囪涓?destroy鐨勯儴缃诧紙鍏煎鏃ф柟娉曪級
/// </summary>
[Obsolete("Use CleanupOldDeployments instead")]
public void CleanupDestroyedDeployments()
@@ -581,36 +581,17 @@ internal sealed class DeploymentLocator
}
/// <summary>
/// 从部署ç®å½•读å<EFBFBD>版本信æ<EFBFBD>? /// </summary>
/// 浠庨儴缃茬洰褰曡鍙栫増鏈俊鎭? /// </summary>
public AppVersionInfo GetVersionInfo()
{
var deploymentDir = FindCurrentDeploymentDirectory();
if (!string.IsNullOrWhiteSpace(deploymentDir))
{
var versionFile = Path.Combine(deploymentDir, "version.json");
if (File.Exists(versionFile))
var executableName = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
var resolved = AppVersionProvider.ResolveFromPackageRoot(_appRoot, executableName);
return string.IsNullOrWhiteSpace(resolved.Version)
? new AppVersionInfo
{
try
{
var json = File.ReadAllText(versionFile);
var info = JsonSerializer.Deserialize(json, AppJsonContext.Default.AppVersionInfo);
if (info is not null)
{
return info;
}
}
catch
{
}
Version = GetCurrentVersion(),
Codename = "Administrate"
}
}
return new AppVersionInfo
{
Version = GetCurrentVersion(),
Codename = "Administrate"
};
}
: resolved;
}
}