mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
feat.尝试弄了AOT的启动器。
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user