2026-04-16 01:59:21 +08:00
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using Avalonia.Threading;
|
|
|
|
|
|
using LanMountainDesktop.Launcher.Models;
|
2026-04-16 19:28:58 +08:00
|
|
|
|
using LanMountainDesktop.Launcher.Services.Ipc;
|
2026-04-16 01:59:21 +08:00
|
|
|
|
using LanMountainDesktop.Launcher.Views;
|
2026-04-16 19:28:58 +08:00
|
|
|
|
using LanMountainDesktop.Shared.Contracts.Launcher;
|
2026-04-16 01:59:21 +08:00
|
|
|
|
|
|
|
|
|
|
namespace LanMountainDesktop.Launcher.Services;
|
|
|
|
|
|
|
|
|
|
|
|
internal sealed class LauncherFlowCoordinator
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly CommandContext _context;
|
|
|
|
|
|
private readonly DeploymentLocator _deploymentLocator;
|
|
|
|
|
|
private readonly OobeStateService _oobeStateService;
|
|
|
|
|
|
private readonly UpdateEngineService _updateEngine;
|
|
|
|
|
|
private readonly UpdateCheckService _updateCheckService;
|
|
|
|
|
|
private readonly PluginInstallerService _pluginInstallerService;
|
|
|
|
|
|
private readonly IReadOnlyList<IOobeStep> _oobeSteps;
|
|
|
|
|
|
|
|
|
|
|
|
public LauncherFlowCoordinator(
|
|
|
|
|
|
CommandContext context,
|
|
|
|
|
|
DeploymentLocator deploymentLocator,
|
|
|
|
|
|
OobeStateService oobeStateService,
|
|
|
|
|
|
UpdateEngineService updateEngine,
|
|
|
|
|
|
UpdateCheckService updateCheckService,
|
|
|
|
|
|
PluginInstallerService pluginInstallerService)
|
|
|
|
|
|
{
|
|
|
|
|
|
_context = context;
|
|
|
|
|
|
_deploymentLocator = deploymentLocator;
|
|
|
|
|
|
_oobeStateService = oobeStateService;
|
|
|
|
|
|
_updateEngine = updateEngine;
|
|
|
|
|
|
_updateCheckService = updateCheckService;
|
|
|
|
|
|
_pluginInstallerService = pluginInstallerService;
|
|
|
|
|
|
_oobeSteps = [new WelcomeOobeStep(_oobeStateService)];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<LauncherResult> RunAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 清理待删除的旧版本
|
|
|
|
|
|
_deploymentLocator.CleanupDestroyedDeployments();
|
|
|
|
|
|
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// 显示 Splash 窗口
|
2026-04-16 01:59:21 +08:00
|
|
|
|
var splashWindow = await Dispatcher.UIThread.InvokeAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var window = new SplashWindow();
|
|
|
|
|
|
window.Show();
|
|
|
|
|
|
return window;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-16 14:17:46 +08:00
|
|
|
|
var reporter = (ISplashStageReporter)splashWindow;
|
2026-04-16 19:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 启动 IPC 服务端监听主程序进度
|
|
|
|
|
|
using var ipcServer = new LauncherIpcServer(msg =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
reporter.Report(msg.Stage.ToString().ToLower(), msg.Message ?? "");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
ipcServer.Start();
|
2026-04-16 14:17:46 +08:00
|
|
|
|
|
2026-04-16 01:59:21 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// 检查并安装待处理的更新(主程序下载的)
|
|
|
|
|
|
reporter.Report("update", "检查更新...");
|
2026-04-16 14:17:46 +08:00
|
|
|
|
var updateResult = await _updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
|
2026-04-16 01:59:21 +08:00
|
|
|
|
if (!updateResult.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
return updateResult;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// 检查并安装待处理的插件更新
|
|
|
|
|
|
reporter.Report("plugins", "检查插件更新...");
|
2026-04-16 01:59:21 +08:00
|
|
|
|
var pluginsDir = _context.GetOption("plugins-dir")
|
|
|
|
|
|
?? Path.Combine(_deploymentLocator.GetAppRoot(), "plugins");
|
|
|
|
|
|
var queueResult = new PluginUpgradeQueueService(_pluginInstallerService).ApplyPendingUpgrades(pluginsDir);
|
|
|
|
|
|
if (!queueResult.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
return queueResult;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// OOBE(首次运行引导)
|
|
|
|
|
|
if (_oobeStateService.IsFirstRun())
|
|
|
|
|
|
{
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Hide());
|
|
|
|
|
|
foreach (var step in _oobeSteps)
|
|
|
|
|
|
{
|
|
|
|
|
|
await step.RunAsync(CancellationToken.None).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Show());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 启动主程序
|
|
|
|
|
|
reporter.Report("launch", "正在启动...");
|
|
|
|
|
|
var hostResult = await LaunchHostWithIpcAsync();
|
2026-04-16 01:59:21 +08:00
|
|
|
|
if (!hostResult.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
return hostResult;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// 等待主程序就绪或超时
|
|
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(30));
|
|
|
|
|
|
|
2026-04-16 01:59:21 +08:00
|
|
|
|
return new LauncherResult
|
|
|
|
|
|
{
|
|
|
|
|
|
Success = true,
|
|
|
|
|
|
Stage = "exit",
|
|
|
|
|
|
Code = "ok",
|
|
|
|
|
|
Message = "Launcher completed successfully."
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => splashWindow.Close());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new LauncherResult
|
|
|
|
|
|
{
|
|
|
|
|
|
Success = false,
|
|
|
|
|
|
Stage = "launch",
|
|
|
|
|
|
Code = "exception",
|
|
|
|
|
|
Message = ex.Message,
|
|
|
|
|
|
ErrorMessage = ex.Message
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 19:28:58 +08:00
|
|
|
|
private async Task<LauncherResult> LaunchHostWithIpcAsync(string? customHostPath = null)
|
2026-04-16 01:59:21 +08:00
|
|
|
|
{
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// 优先使用自定义路径(调试模式选择的路径)
|
|
|
|
|
|
var hostPath = customHostPath ?? _deploymentLocator.ResolveHostExecutablePath();
|
|
|
|
|
|
|
2026-04-16 01:59:21 +08:00
|
|
|
|
if (string.IsNullOrWhiteSpace(hostPath))
|
|
|
|
|
|
{
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// 关闭 Splash 窗口
|
|
|
|
|
|
// 显示错误窗口而不是直接退出
|
|
|
|
|
|
var (errorResult, selectedPath) = await ShowHostNotFoundErrorAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (errorResult == ErrorWindowResult.Retry)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 用户选择重试,如果有选择路径则使用,否则重新尝试
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(selectedPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return await LaunchHostWithIpcAsync(selectedPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
return await LaunchHostWithIpcAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户选择退出
|
2026-04-16 01:59:21 +08:00
|
|
|
|
return new LauncherResult
|
|
|
|
|
|
{
|
|
|
|
|
|
Success = false,
|
|
|
|
|
|
Stage = "launchHost",
|
|
|
|
|
|
Code = "host_not_found",
|
|
|
|
|
|
Message = "LanMountainDesktop host executable not found."
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureExecutable(hostPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var processStartInfo = new ProcessStartInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
FileName = hostPath,
|
|
|
|
|
|
UseShellExecute = true,
|
|
|
|
|
|
WorkingDirectory = Path.GetDirectoryName(hostPath) ?? _deploymentLocator.GetAppRoot()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 19:28:58 +08:00
|
|
|
|
// 传递环境变量供 IPC 使用
|
|
|
|
|
|
processStartInfo.EnvironmentVariables[LauncherIpcConstants.LauncherPidEnvVar] =
|
|
|
|
|
|
Environment.ProcessId.ToString();
|
|
|
|
|
|
processStartInfo.EnvironmentVariables[LauncherIpcConstants.PackageRootEnvVar] =
|
|
|
|
|
|
_deploymentLocator.GetAppRoot();
|
|
|
|
|
|
|
2026-04-16 01:59:21 +08:00
|
|
|
|
Process.Start(processStartInfo);
|
|
|
|
|
|
return new LauncherResult
|
|
|
|
|
|
{
|
|
|
|
|
|
Success = true,
|
|
|
|
|
|
Stage = "launchHost",
|
|
|
|
|
|
Code = "ok",
|
|
|
|
|
|
Message = "Host launched."
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 19:28:58 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 显示找不到主程序的错误窗口
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<(ErrorWindowResult Result, string? CustomPath)> ShowHostNotFoundErrorAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var errorWindow = new ErrorWindow();
|
|
|
|
|
|
errorWindow.SetErrorMessage("找不到阑山桌面应用程序。");
|
|
|
|
|
|
errorWindow.Show();
|
|
|
|
|
|
|
|
|
|
|
|
var result = await errorWindow.WaitForChoiceAsync();
|
|
|
|
|
|
var customPath = errorWindow.GetCustomHostPath();
|
|
|
|
|
|
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => errorWindow.Close());
|
|
|
|
|
|
|
|
|
|
|
|
return (result, customPath);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 01:59:21 +08:00
|
|
|
|
private static void EnsureExecutable(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var mode = File.GetUnixFileMode(path);
|
|
|
|
|
|
mode |= UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
|
|
|
|
|
|
File.SetUnixFileMode(path, mode);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class WelcomeOobeStep : IOobeStep
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly OobeStateService _stateService;
|
|
|
|
|
|
|
|
|
|
|
|
public WelcomeOobeStep(OobeStateService stateService)
|
|
|
|
|
|
{
|
|
|
|
|
|
_stateService = stateService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task RunAsync(CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
var window = await Dispatcher.UIThread.InvokeAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var oobeWindow = new OobeWindow();
|
|
|
|
|
|
oobeWindow.Show();
|
|
|
|
|
|
return oobeWindow;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var _ = cancellationToken.Register(() => window.Close());
|
|
|
|
|
|
await window.WaitForEnterAsync().ConfigureAwait(false);
|
|
|
|
|
|
_stateService.MarkCompleted();
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => window.Close());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|