mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Add HostLaunchPlan/HostLaunchPlanBuilder to encapsulate host path, package root, working dir, forwarded args and env; add unit tests for builder. Refactor LauncherFlowCoordinator to use HostLaunchPlan when starting hosts, improve IPC handling and startup logic (shorter soft/hard timeouts, more frequent reconnects and shell status polling, activation recovery via existing host). Move argument formatting and environment setup into the plan, include package/working/args metadata in start attempts. Update Commands to prefer ProcessPath for launcher base directory. App and Program: start single-instance activation listener earlier and harden ActivateMainWindow to handle shell initialization state and return richer activation status codes. SingleInstanceService: signal listener readiness (ManualResetEventSlim) and wait briefly when starting, and dispose it. Various logging and minor error handling improvements.
197 lines
7.3 KiB
C#
197 lines
7.3 KiB
C#
using System.Text;
|
||
using System.Text.Json;
|
||
using LanMountainDesktop.Launcher.Models;
|
||
|
||
namespace LanMountainDesktop.Launcher.Services;
|
||
|
||
internal static class Commands
|
||
{
|
||
public static async Task<int> RunLegacyPluginInstallAsync(CommandContext context, PluginInstallerService installer)
|
||
{
|
||
var resultPath = context.GetOption("result");
|
||
LauncherResult result;
|
||
try
|
||
{
|
||
var source = context.GetOption("source") ?? string.Empty;
|
||
var pluginsDir = context.GetOption("plugins-dir") ?? string.Empty;
|
||
result = installer.InstallPackage(source, pluginsDir);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result = new LauncherResult
|
||
{
|
||
Success = false,
|
||
Stage = "plugin.install",
|
||
Code = "failed",
|
||
Message = ex.Message,
|
||
ErrorMessage = ex.Message
|
||
};
|
||
}
|
||
|
||
await WriteResultIfNeededAsync(resultPath, result).ConfigureAwait(false);
|
||
return result.Success ? 0 : 1;
|
||
}
|
||
|
||
public static async Task<int> RunCliCommandAsync(CommandContext context)
|
||
{
|
||
var appRoot = ResolveAppRoot(context);
|
||
var deploymentLocator = new DeploymentLocator(appRoot);
|
||
var updateEngine = new UpdateEngineService(deploymentLocator);
|
||
var pluginInstaller = new PluginInstallerService();
|
||
var pluginUpgrades = new PluginUpgradeQueueService(pluginInstaller);
|
||
|
||
LauncherResult result;
|
||
try
|
||
{
|
||
result = await ExecuteCoreAsync(context, updateEngine, pluginInstaller, pluginUpgrades).ConfigureAwait(false);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result = new LauncherResult
|
||
{
|
||
Success = false,
|
||
Stage = "command",
|
||
Code = "exception",
|
||
Message = ex.Message,
|
||
ErrorMessage = ex.Message
|
||
};
|
||
}
|
||
|
||
await WriteResultIfNeededAsync(context.GetOption("result"), result).ConfigureAwait(false);
|
||
return result.Success ? 0 : 1;
|
||
}
|
||
|
||
private static async Task<LauncherResult> ExecuteCoreAsync(
|
||
CommandContext context,
|
||
UpdateEngineService updateEngine,
|
||
PluginInstallerService pluginInstaller,
|
||
PluginUpgradeQueueService pluginUpgrades)
|
||
{
|
||
switch (context.Command.ToLowerInvariant())
|
||
{
|
||
case "update":
|
||
return await ExecuteUpdateAsync(context, updateEngine).ConfigureAwait(false);
|
||
case "plugin":
|
||
return ExecutePluginCommand(context, pluginInstaller, pluginUpgrades);
|
||
default:
|
||
return new LauncherResult
|
||
{
|
||
Success = false,
|
||
Stage = "command",
|
||
Code = "unsupported_command",
|
||
Message = $"Unsupported command '{context.Command}'."
|
||
};
|
||
}
|
||
}
|
||
|
||
private static async Task<LauncherResult> ExecuteUpdateAsync(CommandContext context, UpdateEngineService updateEngine)
|
||
{
|
||
return context.SubCommand.ToLowerInvariant() switch
|
||
{
|
||
"check" => updateEngine.CheckPendingUpdate(),
|
||
"apply" => await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false),
|
||
"rollback" => updateEngine.RollbackLatest(),
|
||
"download" => await DownloadUpdatePayloadAsync(context, updateEngine).ConfigureAwait(false),
|
||
_ => new LauncherResult
|
||
{
|
||
Success = false,
|
||
Stage = "update",
|
||
Code = "unsupported_subcommand",
|
||
Message = $"Unsupported update sub-command '{context.SubCommand}'."
|
||
}
|
||
};
|
||
}
|
||
|
||
private static async Task<LauncherResult> DownloadUpdatePayloadAsync(CommandContext context, UpdateEngineService updateEngine)
|
||
{
|
||
return await updateEngine.DownloadAsync(
|
||
context.GetOption("manifest-url") ?? throw new InvalidOperationException("Missing --manifest-url."),
|
||
context.GetOption("signature-url") ?? throw new InvalidOperationException("Missing --signature-url."),
|
||
context.GetOption("archive-url") ?? throw new InvalidOperationException("Missing --archive-url."),
|
||
CancellationToken.None).ConfigureAwait(false);
|
||
}
|
||
|
||
private static LauncherResult ExecutePluginCommand(
|
||
CommandContext context,
|
||
PluginInstallerService pluginInstaller,
|
||
PluginUpgradeQueueService pluginUpgrades)
|
||
{
|
||
switch (context.SubCommand.ToLowerInvariant())
|
||
{
|
||
case "install":
|
||
{
|
||
var source = context.GetOption("source") ?? throw new InvalidOperationException("Missing --source.");
|
||
var pluginsDir = context.GetOption("plugins-dir") ?? throw new InvalidOperationException("Missing --plugins-dir.");
|
||
return pluginInstaller.InstallPackage(source, pluginsDir);
|
||
}
|
||
case "update":
|
||
{
|
||
var pluginsDir = context.GetOption("plugins-dir") ?? throw new InvalidOperationException("Missing --plugins-dir.");
|
||
return pluginUpgrades.ApplyPendingUpgrades(pluginsDir);
|
||
}
|
||
default:
|
||
return new LauncherResult
|
||
{
|
||
Success = false,
|
||
Stage = "plugin",
|
||
Code = "unsupported_subcommand",
|
||
Message = $"Unsupported plugin sub-command '{context.SubCommand}'."
|
||
};
|
||
}
|
||
}
|
||
|
||
public static async Task WriteResultIfNeededAsync(string? resultPath, LauncherResult result)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(resultPath))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var fullPath = Path.GetFullPath(resultPath);
|
||
var dir = Path.GetDirectoryName(fullPath);
|
||
if (!string.IsNullOrWhiteSpace(dir))
|
||
{
|
||
Directory.CreateDirectory(dir);
|
||
}
|
||
|
||
var json = JsonSerializer.Serialize(result, AppJsonContext.Default.LauncherResult);
|
||
await File.WriteAllTextAsync(fullPath, json, Encoding.UTF8).ConfigureAwait(false);
|
||
}
|
||
|
||
public static string ResolveAppRoot(CommandContext context)
|
||
{
|
||
var configured = context.GetOption("app-root");
|
||
if (!string.IsNullOrWhiteSpace(configured))
|
||
{
|
||
return Path.GetFullPath(configured);
|
||
}
|
||
|
||
var launcherDir = Path.GetDirectoryName(Environment.ProcessPath);
|
||
var baseDir = Path.GetFullPath(!string.IsNullOrWhiteSpace(launcherDir)
|
||
? launcherDir
|
||
: AppContext.BaseDirectory);
|
||
|
||
// 发布版结构:Launcher 和 app-* 目录在同一目录
|
||
// 检查当前目录是否有 app-* 子目录(发布版)
|
||
var appDirs = Directory.GetDirectories(baseDir, "app-*", SearchOption.TopDirectoryOnly);
|
||
if (appDirs.Length > 0)
|
||
{
|
||
// 找到 app-* 目录,说明是发布版结构
|
||
return baseDir;
|
||
}
|
||
|
||
// 开发环境:检查父目录是否有主程序
|
||
var parent = Path.GetFullPath(Path.Combine(baseDir, ".."));
|
||
var parentHost = OperatingSystem.IsWindows()
|
||
? Path.Combine(parent, "LanMountainDesktop.exe")
|
||
: Path.Combine(parent, "LanMountainDesktop");
|
||
if (File.Exists(parentHost))
|
||
{
|
||
return parent;
|
||
}
|
||
|
||
// 默认返回 baseDir
|
||
return baseDir;
|
||
}
|
||
}
|