fis.airapp相关运行时修复以及打包构建工作流修复

This commit is contained in:
lincube
2026-05-26 13:25:42 +08:00
parent 1d7a878d55
commit 553cee54f9
11 changed files with 332 additions and 17 deletions

View File

@@ -244,6 +244,27 @@ jobs:
-AssertClean
shell: pwsh
- name: Verify Windows app host payload
run: |
$version = "${{ needs.prepare.outputs.version }}"
$arch = "${{ matrix.arch }}"
$publishDir = "publish/windows-$arch"
$appDir = Join-Path $publishDir "app-$version"
$requiredFiles = @(
(Join-Path $publishDir "LanMountainDesktop.Launcher.exe"),
(Join-Path $appDir "LanMountainDesktop.exe"),
(Join-Path $appDir "LanMountainDesktop.AirAppHost.exe")
)
foreach ($path in $requiredFiles) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
Write-Error "Required release payload file is missing: $path"
exit 1
}
}
shell: pwsh
- name: Install Inno Setup and 7z
run: |
choco install innosetup -y --no-progress

View File

@@ -12,8 +12,8 @@ namespace LanMountainDesktop.Launcher.Services;
internal sealed class LauncherFlowCoordinator
{
private static readonly TimeSpan StartupSoftTimeout = TimeSpan.FromSeconds(10);
private static readonly TimeSpan StartupHardTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan StartupSoftTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan StartupHardTimeout = TimeSpan.FromSeconds(120);
private static readonly string SoftTimeoutStatusMessage = Strings.Coordinator_SlowDeviceMessage;
private static readonly string SoftTimeoutDetailsMessage = Strings.Coordinator_RunningHostMessage;

View File

@@ -0,0 +1,32 @@
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class LauncherStartupTimeoutPolicyTests
{
[Fact]
public void LauncherStartupTimeouts_MatchSlowStartupContract()
{
var source = ReadRepositoryFile("LanMountainDesktop.Launcher", "Services", "LauncherFlowCoordinator.cs");
Assert.Contains("StartupSoftTimeout = TimeSpan.FromSeconds(30)", source);
Assert.Contains("StartupHardTimeout = TimeSpan.FromSeconds(120)", source);
Assert.DoesNotContain("StartupHardTimeout = TimeSpan.FromSeconds(30)", source);
}
private static string ReadRepositoryFile(params string[] pathParts)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
{
directory = directory.Parent;
}
if (directory is null)
{
throw new DirectoryNotFoundException("Unable to locate repository root.");
}
return File.ReadAllText(Path.Combine([directory.FullName, .. pathParts]));
}
}

View File

@@ -27,6 +27,26 @@ public sealed class PackagingRuntimePolicyTests
Assert.Contains("System.Private.CoreLib.dll", script);
}
[Fact]
public void WindowsPayloadGuard_RequiresLauncherMainAndAirAppHost()
{
var script = ReadRepositoryFile("LanMountainDesktop", "scripts", "Optimize-PublishPayload.ps1");
Assert.Contains("Assert-WindowsPayloadContainsRequiredHosts", script);
Assert.Contains("LanMountainDesktop.Launcher.exe", script);
Assert.Contains("LanMountainDesktop.exe", script);
Assert.Contains("LanMountainDesktop.AirAppHost.exe", script);
}
[Fact]
public void ReleaseWorkflow_VerifiesAirAppHostBeforePublishingInstaller()
{
var workflow = ReadRepositoryFile(".github", "workflows", "release.yml");
Assert.Contains("Verify Windows app host payload", workflow);
Assert.Contains("LanMountainDesktop.AirAppHost.exe", workflow);
}
[Fact]
public void Installer_DownloadsArchitectureSpecificDesktopRuntime()
{

View File

@@ -6,8 +6,10 @@ using System.Threading.Tasks;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Update;
using LanMountainDesktop.Services.PluginMarket;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.Shared.Contracts.Update;
namespace LanMountainDesktop.Services.Settings
{
@@ -356,8 +358,21 @@ public interface IPrivacySettingsService
public interface IUpdateSettingsService
{
UpdatePhase CurrentPhase { get; }
event Action<UpdatePhase>? PhaseChanged;
event Action<UpdateProgressReport>? ProgressChanged;
UpdateSettingsState Get();
void Save(UpdateSettingsState state);
Task<UpdateCheckReport> CheckAsync(CancellationToken cancellationToken = default);
Task<LanMountainDesktop.Services.Update.DownloadResult> DownloadAsync(CancellationToken cancellationToken = default);
Task<InstallResult> InstallAsync(CancellationToken cancellationToken = default);
Task RollbackAsync(CancellationToken cancellationToken = default);
Task PauseAsync();
Task<LanMountainDesktop.Services.Update.DownloadResult> ResumeAsync(CancellationToken cancellationToken = default);
Task CancelAsync();
Task AutoCheckIfEnabledAsync(CancellationToken cancellationToken = default);
bool TryApplyOnExit();
Task<UpdateCheckResult> CheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default);
Task<UpdateCheckResult> ForceCheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default);
Task<PlondsUpdatePayload?> GetPlondsUpdatePayloadAsync(Version currentVersion, bool includePrerelease, bool isForce = false, CancellationToken cancellationToken = default);

View File

@@ -10,8 +10,10 @@ using Avalonia.Media.Imaging;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Update;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.Services.PluginMarket;
using LanMountainDesktop.Shared.Contracts.Update;
namespace LanMountainDesktop.Services.Settings;
@@ -784,10 +786,40 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
private readonly GitHubReleaseUpdateService _githubReleaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
private readonly PlondsStaticUpdateService _plondsStaticUpdateService = new();
private readonly PlondsReleaseUpdateService _plondsReleaseUpdateService = new();
private readonly Lazy<UpdateOrchestrator> _orchestrator;
public UpdateSettingsService(ISettingsService settingsService)
public UpdateSettingsService(ISettingsService settingsService, Func<UpdateOrchestrator>? orchestratorFactory = null)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_orchestrator = new Lazy<UpdateOrchestrator>(
orchestratorFactory ?? HostUpdateOrchestratorProvider.GetOrCreate,
LazyThreadSafetyMode.ExecutionAndPublication);
}
public UpdatePhase CurrentPhase => _orchestrator.Value.CurrentPhase;
public event Action<UpdatePhase>? PhaseChanged
{
add => _orchestrator.Value.PhaseChanged += value;
remove
{
if (_orchestrator.IsValueCreated)
{
_orchestrator.Value.PhaseChanged -= value;
}
}
}
public event Action<UpdateProgressReport>? ProgressChanged
{
add => _orchestrator.Value.ProgressChanged += value;
remove
{
if (_orchestrator.IsValueCreated)
{
_orchestrator.Value.ProgressChanged -= value;
}
}
}
public UpdateSettingsState Get()
@@ -862,6 +894,51 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
]);
}
public Task<UpdateCheckReport> CheckAsync(CancellationToken cancellationToken = default)
{
return _orchestrator.Value.CheckAsync(cancellationToken);
}
public Task<LanMountainDesktop.Services.Update.DownloadResult> DownloadAsync(CancellationToken cancellationToken = default)
{
return _orchestrator.Value.DownloadAsync(cancellationToken);
}
public Task<InstallResult> InstallAsync(CancellationToken cancellationToken = default)
{
return _orchestrator.Value.InstallAsync(cancellationToken);
}
public Task RollbackAsync(CancellationToken cancellationToken = default)
{
return _orchestrator.Value.RollbackAsync(cancellationToken);
}
public Task PauseAsync()
{
return _orchestrator.Value.PauseAsync();
}
public Task<LanMountainDesktop.Services.Update.DownloadResult> ResumeAsync(CancellationToken cancellationToken = default)
{
return _orchestrator.Value.ResumeAsync(cancellationToken);
}
public Task CancelAsync()
{
return _orchestrator.Value.CancelAsync();
}
public Task AutoCheckIfEnabledAsync(CancellationToken cancellationToken = default)
{
return _orchestrator.Value.AutoCheckIfEnabledAsync(cancellationToken);
}
public bool TryApplyOnExit()
{
return _orchestrator.Value.TryApplyOnExit();
}
public Task<UpdateCheckResult> CheckForUpdatesAsync(
Version currentVersion,
bool includePrerelease,
@@ -945,6 +1022,15 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
bool isForce,
CancellationToken cancellationToken)
{
var source = UpdateSettingsValues.NormalizeDownloadSource(Get().UpdateDownloadSource);
if (string.Equals(source, UpdateSettingsValues.DownloadSourceGitHub, StringComparison.OrdinalIgnoreCase) ||
string.Equals(source, UpdateSettingsValues.DownloadSourceGhProxy, StringComparison.OrdinalIgnoreCase))
{
return isForce
? await _githubReleaseUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken)
: await _githubReleaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
}
var staticResult = isForce
? await _plondsStaticUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken)
: await _plondsStaticUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);

View File

@@ -0,0 +1,60 @@
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Shared.Contracts.Update;
namespace LanMountainDesktop.Services.Update;
internal sealed class SettingsUpdateManifestProvider : IUpdateManifestProvider
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly IUpdateManifestProvider _plondsWithFallback;
private readonly IUpdateManifestProvider _github;
public SettingsUpdateManifestProvider(
ISettingsFacadeService settingsFacade,
IUpdateManifestProvider plonds,
IUpdateManifestProvider github)
{
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_github = github ?? throw new ArgumentNullException(nameof(github));
_plondsWithFallback = new CompositeManifestProvider(plonds ?? throw new ArgumentNullException(nameof(plonds)), _github);
}
public string ProviderName => "settings-selected-update-source";
public Task<UpdateManifest?> GetLatestAsync(
string channel,
string platform,
Version currentVersion,
CancellationToken ct)
{
return SelectProvider().GetLatestAsync(channel, platform, currentVersion, ct);
}
public Task<UpdateManifest?> GetByVersionAsync(
string version,
string channel,
string platform,
CancellationToken ct)
{
return SelectProvider().GetByVersionAsync(version, channel, platform, ct);
}
public Task<IReadOnlyList<UpdateManifest>> GetIncrementalChainAsync(
string channel,
string platform,
Version fromVersion,
Version toVersion,
CancellationToken ct)
{
return SelectProvider().GetIncrementalChainAsync(channel, platform, fromVersion, toVersion, ct);
}
private IUpdateManifestProvider SelectProvider()
{
var source = UpdateSettingsValues.NormalizeDownloadSource(_settingsFacade.Update.Get().UpdateDownloadSource);
return string.Equals(source, UpdateSettingsValues.DownloadSourceGitHub, StringComparison.OrdinalIgnoreCase) ||
string.Equals(source, UpdateSettingsValues.DownloadSourceGhProxy, StringComparison.OrdinalIgnoreCase)
? _github
: _plondsWithFallback;
}
}

View File

@@ -232,6 +232,7 @@ internal sealed class UpdateDownloadEngine
UpdateManifest manifest,
string destinationPath,
int maxThreads,
string? downloadSource,
IProgress<DownloadProgressReport>? progress,
CancellationToken ct)
{
@@ -281,7 +282,7 @@ internal sealed class UpdateDownloadEngine
ct.ThrowIfCancellationRequested();
var result = await _downloadService.DownloadAsync(
mirror.Url,
ApplyDownloadSource(mirror.Url, downloadSource),
destinationPath,
new DownloadOptions(MaxParallelSegments: Math.Max(1, maxThreads)),
downloadProgress,
@@ -386,6 +387,22 @@ internal sealed class UpdateDownloadEngine
throw lastError!;
}
internal static string ApplyDownloadSource(string browserDownloadUrl, string? downloadSource)
{
if (!string.Equals(
UpdateSettingsValues.NormalizeDownloadSource(downloadSource),
UpdateSettingsValues.DownloadSourceGhProxy,
StringComparison.OrdinalIgnoreCase))
{
return browserDownloadUrl;
}
var normalizedBase = UpdateSettingsValues.DefaultGhProxyBaseUrl.TrimEnd('/') + "/";
return browserDownloadUrl.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase)
? browserDownloadUrl
: normalizedBase + browserDownloadUrl;
}
private static async Task<string> ComputeFileSha256Async(string filePath, CancellationToken ct)
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, true);

View File

@@ -96,6 +96,13 @@ internal static class UpdateManifestMapper
ArchiveSha256: null,
Metadata: null));
mirrors.Add(new UpdateMirrorAsset(
Platform: platform,
Url: installerAsset.BrowserDownloadUrl,
Name: installerAsset.Name,
Sha256: installerAsset.Sha256,
Size: installerAsset.SizeBytes));
foreach (var asset in release.Assets)
{
if (IsInstallerAsset(asset) && asset != installerAsset)

View File

@@ -25,13 +25,13 @@ internal static class HostUpdateOrchestratorProvider
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
var githubProvider = new GithubReleaseManifestProvider("wwiinnddyy", "LanMountainDesktop");
var staticProvider = new PlondsApiManifestProvider("https://api.classisland.tech");
var compositeProvider = new CompositeManifestProvider(staticProvider, githubProvider);
var plondsProvider = new PlondsApiManifestProvider("https://api.classisland.tech");
var manifestProvider = new SettingsUpdateManifestProvider(settingsFacade, plondsProvider, githubProvider);
var httpClient = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromSeconds(30) };
var downloadEngine = new UpdateDownloadEngine(compositeProvider, new ResumableDownloadService(httpClient));
var downloadEngine = new UpdateDownloadEngine(manifestProvider, new ResumableDownloadService(httpClient));
var installGateway = new UpdateInstallGateway();
var stateStore = new UpdateStateStore(settingsFacade);
_instance = new UpdateOrchestrator(compositeProvider, downloadEngine, installGateway, stateStore);
_instance = new UpdateOrchestrator(manifestProvider, downloadEngine, installGateway, stateStore);
return _instance;
}
}
@@ -106,8 +106,7 @@ public sealed class UpdateOrchestrator : IDisposable
var settings = _stateStore.GetSettings();
var channel = UpdateSettingsValues.NormalizeChannel(settings.UpdateChannel);
var currentVersionText = _stateStore.GetSettings().PendingUpdateVersion
?? AppVersionProvider.ResolveForCurrentProcess().Version;
var currentVersionText = AppVersionProvider.ResolveForCurrentProcess().Version;
if (!TryParseVersion(currentVersionText, out var currentVersion))
{
@@ -166,12 +165,14 @@ public sealed class UpdateOrchestrator : IDisposable
if (manifest is null)
{
_stateStore.TransitionTo(UpdatePhase.Checked);
SaveLastChecked();
return new UpdateCheckReport(
false, null, currentVersionText, null, null, null, null, null, null, null);
}
_stateStore.PendingManifest = manifest;
_stateStore.TransitionTo(UpdatePhase.Checked);
SaveLastChecked();
long? totalBytes = manifest.IsDelta ? manifest.EstimatedDeltaBytes : null;
long? installerBytes = manifest.InstallerMirrors?.Count > 0
@@ -262,6 +263,7 @@ public sealed class UpdateOrchestrator : IDisposable
manifest,
destinationPath,
maxThreads,
settings.UpdateDownloadSource,
downloadProgress,
operationToken);
}
@@ -569,15 +571,12 @@ public sealed class UpdateOrchestrator : IDisposable
return false;
}
var manifest = _stateStore.PendingManifest;
if (manifest is null)
{
return false;
}
var launcherRoot = UpdatePaths.ResolveLauncherRoot(AppContext.BaseDirectory);
var manifest = _stateStore.PendingManifest;
var deploymentLock = DeploymentLockService.ReadLock(launcherRoot);
if (manifest.IsDelta)
if (manifest?.IsDelta == true ||
string.Equals(deploymentLock?.Kind, "delta", StringComparison.OrdinalIgnoreCase))
{
AppLogger.Info("UpdateOrchestrator", "Delta update pending. Launching Launcher to apply on exit.");
var launcherPath = LauncherPathResolver.ResolveLauncherExecutablePath();
@@ -638,6 +637,15 @@ public sealed class UpdateOrchestrator : IDisposable
}
}
private void SaveLastChecked()
{
var state = _stateStore.GetSettings();
_stateStore.SaveSettings(state with
{
LastUpdateCheckUtcMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
}
private static void CleanupIncomingArtifacts(string launcherRoot)
{
var incomingDir = UpdatePaths.GetIncomingDirectory(launcherRoot);

View File

@@ -201,6 +201,52 @@ function Assert-WindowsPayloadClean {
Write-Host "Windows payload guard passed for $Rid."
}
function Assert-WindowsPayloadContainsRequiredHosts {
param([Parameter(Mandatory = $true)][string]$Root)
$violations = [System.Collections.Generic.List[string]]::new()
$launcherPath = Join-Path $Root "LanMountainDesktop.Launcher.exe"
if (-not (Test-Path -LiteralPath $launcherPath -PathType Leaf)) {
$violations.Add("LanMountainDesktop.Launcher.exe")
}
$deploymentDirs = @(Get-ChildItem -LiteralPath $Root -Directory -Filter "app-*" -ErrorAction SilentlyContinue |
Where-Object {
-not (Test-Path -LiteralPath (Join-Path $_.FullName ".partial")) -and
-not (Test-Path -LiteralPath (Join-Path $_.FullName ".destroy"))
})
if ($deploymentDirs.Count -eq 0) {
$violations.Add("app-*/")
}
foreach ($deploymentDir in $deploymentDirs) {
$mainHostPath = Join-Path $deploymentDir.FullName "LanMountainDesktop.exe"
if (-not (Test-Path -LiteralPath $mainHostPath -PathType Leaf)) {
$violations.Add((Join-Path $deploymentDir.Name "LanMountainDesktop.exe"))
}
$airAppHostCandidates = @(
(Join-Path $deploymentDir.FullName "LanMountainDesktop.AirAppHost.exe"),
(Join-Path $deploymentDir.FullName "LanMountainDesktop.AirAppHost.dll"),
(Join-Path (Join-Path $deploymentDir.FullName "AirAppHost") "LanMountainDesktop.AirAppHost.exe"),
(Join-Path (Join-Path $deploymentDir.FullName "AirAppHost") "LanMountainDesktop.AirAppHost.dll")
)
if (-not ($airAppHostCandidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1)) {
$violations.Add((Join-Path $deploymentDir.Name "LanMountainDesktop.AirAppHost.exe"))
}
}
if ($violations.Count -gt 0) {
$sample = ($violations | Select-Object -First 50) -join [Environment]::NewLine
throw "Windows publish payload is missing required Launcher/Main/AirAppHost files:$([Environment]::NewLine)$sample"
}
Write-Host "Windows required host guard passed."
}
$resolvedPublishDir = [System.IO.Path]::GetFullPath($PublishDir)
if (-not (Test-Path -LiteralPath $resolvedPublishDir)) {
throw "Publish directory not found: $resolvedPublishDir"
@@ -213,4 +259,7 @@ Write-PayloadAudit -Root $resolvedPublishDir
if ($AssertClean) {
Assert-WindowsPayloadClean -Root $resolvedPublishDir -Rid $RuntimeIdentifier
if ($RuntimeIdentifier -like "win-*") {
Assert-WindowsPayloadContainsRequiredHosts -Root $resolvedPublishDir
}
}