feat.尝试弄了AOT的启动器。

This commit is contained in:
lincube
2026-04-17 15:16:01 +08:00
parent 59c4824425
commit 81ee19f360
49 changed files with 4175 additions and 468 deletions

View File

@@ -1,3 +1,5 @@
using System.Buffers;
using System.Diagnostics;
using System.IO.Pipes;
using System.Text.Json;
using LanMountainDesktop.Shared.Contracts.Launcher;
@@ -6,12 +8,20 @@ namespace LanMountainDesktop.Services.Launcher;
/// <summary>
/// Launcher IPC 客户端 - 向 Launcher 报告启动进度
/// 采用持久连接 + 长度前缀协议,在同一连接上可多次发送消息。
/// 跨平台实现Windows 使用命名管道Linux/macOS 使用 Unix 域套接字
/// </summary>
public class LauncherIpcClient : IDisposable
{
private NamedPipeClientStream? _pipeClient;
private bool _isConnected;
private readonly object _writeLock = new();
/// <summary>
/// 协议:每条消息以 4 字节小端 int32 长度前缀开头,后跟 UTF-8 JSON 正文。
/// </summary>
private const int LengthPrefixSize = 4;
/// <summary>
/// 连接到 Launcher 的 IPC 服务端
/// </summary>
@@ -23,7 +33,7 @@ public class LauncherIpcClient : IDisposable
".",
LauncherIpcConstants.PipeName,
PipeDirection.Out);
await _pipeClient.ConnectAsync(5000, cancellationToken);
_isConnected = true;
return true;
@@ -39,21 +49,34 @@ public class LauncherIpcClient : IDisposable
return false;
}
}
/// <summary>
/// 报告启动进度
/// 报告启动进度(在同一连接上可多次调用)
/// </summary>
public async Task ReportProgressAsync(StartupProgressMessage message)
{
if (!_isConnected || _pipeClient?.IsConnected != true)
return;
try
{
var json = JsonSerializer.Serialize(message);
using var writer = new StreamWriter(_pipeClient, leaveOpen: true);
await writer.WriteAsync(json);
await writer.FlushAsync();
var payload = System.Text.Encoding.UTF8.GetBytes(json);
// 长度前缀协议:[4字节长度][消息正文]
var lengthPrefix = BitConverter.GetBytes(payload.Length);
Debug.Assert(lengthPrefix.Length == LengthPrefixSize);
// 加锁保证单条消息的长度前缀和正文原子写入
lock (_writeLock)
{
_pipeClient.Write(lengthPrefix, 0, LengthPrefixSize);
_pipeClient.Write(payload, 0, payload.Length);
_pipeClient.Flush();
}
// 将同步写入包装为已完成的 Task
await Task.CompletedTask;
}
catch (IOException)
{
@@ -63,9 +86,10 @@ public class LauncherIpcClient : IDisposable
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
_isConnected = false;
}
}
/// <summary>
/// 检查是否从 Launcher 启动
/// </summary>
@@ -74,9 +98,10 @@ public class LauncherIpcClient : IDisposable
return !string.IsNullOrEmpty(
Environment.GetEnvironmentVariable(LauncherIpcConstants.LauncherPidEnvVar));
}
public void Dispose()
{
_isConnected = false;
_pipeClient?.Dispose();
}
}

View File

@@ -1225,10 +1225,18 @@ internal sealed class PluginCatalogSettingsService : IPluginCatalogSettingsServi
internal sealed class ApplicationInfoService : IApplicationInfoService
{
private const string Codename = "Administrate";
private const string DefaultCodename = "Administrate";
public string GetAppVersionText()
{
// 优先从环境变量读取Launcher 传递)
var envVersion = Environment.GetEnvironmentVariable(LanMountainDesktop.Shared.Contracts.Launcher.LauncherIpcConstants.VersionEnvVar);
if (!string.IsNullOrWhiteSpace(envVersion))
{
return envVersion;
}
// 回退:从程序集读取
var assembly = typeof(App).Assembly;
var informationalVersion = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
@@ -1268,7 +1276,15 @@ internal sealed class ApplicationInfoService : IApplicationInfoService
public string GetAppCodenameText()
{
return Codename;
// 优先从环境变量读取Launcher 传递)
var envCodename = Environment.GetEnvironmentVariable(LanMountainDesktop.Shared.Contracts.Launcher.LauncherIpcConstants.CodenameEnvVar);
if (!string.IsNullOrWhiteSpace(envCodename))
{
return envCodename;
}
// 回退:使用默认开发代号
return DefaultCodename;
}
public AppRenderBackendInfo GetRenderBackendInfo()

View File

@@ -489,13 +489,17 @@ public sealed class UpdateWorkflowService
return false;
}
// For delta updates, the files are already in .launcher/update/incoming/.
// Just exit the app - the Launcher will detect and apply the update on next startup.
// For delta updates, launch the Launcher with apply-update command so it can
// apply the update immediately with a progress UI, matching the full installer experience.
if (IsPendingDeltaUpdate())
{
AppLogger.Info("UpdateWorkflow", "Delta update pending in incoming directory. Exiting to let Launcher apply on next startup.");
ClearPendingUpdate();
return true;
AppLogger.Info("UpdateWorkflow", "Delta update pending. Launching Launcher to apply update with progress UI.");
var launchResult = LaunchLauncherForApplyUpdate();
if (launchResult)
{
ClearPendingUpdate();
}
return launchResult;
}
var result = LaunchPendingInstaller(silent: true, exitApplicationAfterLaunch: false);
@@ -507,6 +511,53 @@ public sealed class UpdateWorkflowService
return result.Success;
}
/// <summary>
/// Launches the Launcher process with the apply-update command to apply a pending delta update
/// with a progress UI, providing an experience similar to a full installer.
/// </summary>
public bool LaunchLauncherForApplyUpdate()
{
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))
{
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.");
return false;
}
var startInfo = new ProcessStartInfo
{
FileName = launcherPath,
Arguments = $"apply-update --app-root \"{launcherRoot}\"",
UseShellExecute = false,
WorkingDirectory = launcherRoot
};
Process.Start(startInfo);
AppLogger.Info("UpdateWorkflow", $"Launched Launcher for apply-update: {launcherPath}");
return true;
}
catch (Exception ex)
{
AppLogger.Warn("UpdateWorkflow", $"Failed to launch Launcher for apply-update: {ex.Message}");
return false;
}
}
public void ClearPendingUpdate()
{
var state = _settingsFacade.Update.Get();