mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
feat.PLONDS客户端补全
This commit is contained in:
4
.github/workflows/plonds-uploader.yml
vendored
4
.github/workflows/plonds-uploader.yml
vendored
@@ -23,8 +23,8 @@ env:
|
|||||||
PLONDS_S3_PUBLIC_BASE_KEY_PREFIX: lanmountain/update
|
PLONDS_S3_PUBLIC_BASE_KEY_PREFIX: lanmountain/update
|
||||||
PLONDS_S3_DIRECTORY_UPLOAD_CONCURRENCY: '4'
|
PLONDS_S3_DIRECTORY_UPLOAD_CONCURRENCY: '4'
|
||||||
PLONDS_S3_MULTIPART_THRESHOLD_MB: '8'
|
PLONDS_S3_MULTIPART_THRESHOLD_MB: '8'
|
||||||
PLONDS_S3_MULTIPART_PART_SIZE_MB: '8'
|
PLONDS_S3_MULTIPART_PART_SIZE_MB: '5'
|
||||||
PLONDS_S3_MULTIPART_CONCURRENCY: '4'
|
PLONDS_S3_MULTIPART_CONCURRENCY: '8'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -360,7 +360,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$version = "${{ needs.prepare.outputs.version }}"
|
$version = "${{ needs.prepare.outputs.version }}"
|
||||||
$arch = "${{ matrix.arch }}"
|
$arch = "${{ matrix.arch }}"
|
||||||
$payloadRoot = Join-Path (Join-Path $PWD "publish/windows-$arch") "app-$version"
|
$payloadRoot = Join-Path $PWD "publish/windows-$arch"
|
||||||
if (-not (Test-Path $payloadRoot)) {
|
if (-not (Test-Path $payloadRoot)) {
|
||||||
Write-Error "Payload root not found: $payloadRoot"
|
Write-Error "Payload root not found: $payloadRoot"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -374,7 +374,7 @@ jobs:
|
|||||||
|
|
||||||
Get-ChildItem -Path $payloadRoot -Recurse -File | ForEach-Object {
|
Get-ChildItem -Path $payloadRoot -Recurse -File | ForEach-Object {
|
||||||
$relative = [System.IO.Path]::GetRelativePath($payloadRoot, $_.FullName).Replace('\', '/')
|
$relative = [System.IO.Path]::GetRelativePath($payloadRoot, $_.FullName).Replace('\', '/')
|
||||||
if ($relative -eq '.current' -or $relative -eq '.partial' -or $relative -eq '.destroy' -or $relative.StartsWith('.current/') -or $relative.StartsWith('.partial/') -or $relative.StartsWith('.destroy/')) {
|
if ($relative -eq '.partial' -or $relative -eq '.destroy' -or $relative.StartsWith('.partial/') -or $relative.StartsWith('.destroy/')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,25 @@ public sealed class PlondsClientServiceTests : IDisposable
|
|||||||
Assert.Contains("full package fallback also failed", result.ErrorMessage);
|
Assert.Contains("full package fallback also failed", result.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadPlanner_WhenManifestRequiresCleanInstall_DoesNotPreparePlondsPackage()
|
||||||
|
{
|
||||||
|
var downloader = new FakeDownloader(deltaFails: false, fullFails: false);
|
||||||
|
var planner = new PlondsDownloadPlanner(downloader);
|
||||||
|
|
||||||
|
var result = await planner.PrepareAsync(
|
||||||
|
new PlondsManifestCandidate(
|
||||||
|
new("s3", "s3", "https://s3.test/PLONDS.json"),
|
||||||
|
CreateManifest("1.2.3", requiresCleanInstall: true)),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.True(result.RequiresUiHandling);
|
||||||
|
Assert.Contains("clean install", result.ErrorMessage);
|
||||||
|
Assert.Equal(0, downloader.DeltaCalls);
|
||||||
|
Assert.Equal(0, downloader.FullCalls);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PlondsService_ReadsBuiltInSources_RegistersManifestSources_AndPreparesHighestVersion()
|
public async Task PlondsService_ReadsBuiltInSources_RegistersManifestSources_AndPreparesHighestVersion()
|
||||||
{
|
{
|
||||||
@@ -411,18 +430,62 @@ public sealed class PlondsClientServiceTests : IDisposable
|
|||||||
Assert.True(File.Exists(Path.Combine(currentDeployment, ".destroy")));
|
Assert.True(File.Exists(Path.Combine(currentDeployment, ".destroy")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PreparedPackageInstaller_InstallsFullPackageFromCompleteRootLayout()
|
||||||
|
{
|
||||||
|
var launcherRoot = Path.Combine(_tempRoot, "launcher-full");
|
||||||
|
var currentDeployment = Path.Combine(launcherRoot, "app-1.0.0-0");
|
||||||
|
Directory.CreateDirectory(currentDeployment);
|
||||||
|
File.WriteAllText(Path.Combine(currentDeployment, ".current"), string.Empty);
|
||||||
|
File.WriteAllText(Path.Combine(currentDeployment, "LanMountainDesktop.exe"), "old-exe");
|
||||||
|
File.WriteAllText(Path.Combine(currentDeployment, "app.dll"), "old");
|
||||||
|
|
||||||
|
var filesRoot = Path.Combine(_tempRoot, "Files-root");
|
||||||
|
var fullAppDirectory = Path.Combine(filesRoot, "app-1.2.0");
|
||||||
|
Directory.CreateDirectory(fullAppDirectory);
|
||||||
|
File.WriteAllText(Path.Combine(filesRoot, "LanMountainDesktop.Launcher.exe"), "launcher");
|
||||||
|
File.WriteAllText(Path.Combine(filesRoot, "LanMountainDesktop.AirAppRuntime.exe"), "runtime");
|
||||||
|
File.WriteAllText(Path.Combine(fullAppDirectory, ".current"), string.Empty);
|
||||||
|
File.WriteAllText(Path.Combine(fullAppDirectory, "LanMountainDesktop.exe"), "new-exe");
|
||||||
|
File.WriteAllText(Path.Combine(fullAppDirectory, "app.dll"), "new");
|
||||||
|
|
||||||
|
var package = new PlondsPreparedPackage(
|
||||||
|
new Version(1, 2, 0),
|
||||||
|
PlondsPackageMode.Full,
|
||||||
|
Path.Combine(_tempRoot, "PLONDS.json"),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Path.Combine(_tempRoot, "Files.zip"),
|
||||||
|
filesRoot);
|
||||||
|
|
||||||
|
var result = await new PlondsPreparedPackageInstaller().InstallAsync(
|
||||||
|
package,
|
||||||
|
launcherRoot,
|
||||||
|
progress: null,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
var target = Assert.Single(Directory.GetDirectories(launcherRoot, "app-1.2.0-*"));
|
||||||
|
Assert.Equal("new-exe", File.ReadAllText(Path.Combine(target, "LanMountainDesktop.exe")));
|
||||||
|
Assert.Equal("new", File.ReadAllText(Path.Combine(target, "app.dll")));
|
||||||
|
Assert.False(File.Exists(Path.Combine(target, "LanMountainDesktop.Launcher.exe")));
|
||||||
|
Assert.True(File.Exists(Path.Combine(target, ".current")));
|
||||||
|
Assert.True(File.Exists(Path.Combine(currentDeployment, ".destroy")));
|
||||||
|
}
|
||||||
|
|
||||||
private static PlondsClientManifest CreateManifest(
|
private static PlondsClientManifest CreateManifest(
|
||||||
string version,
|
string version,
|
||||||
IReadOnlyList<PlondsSourceDescriptor>? sources = null,
|
IReadOnlyList<PlondsSourceDescriptor>? sources = null,
|
||||||
PlondsClientDownloads? downloads = null,
|
PlondsClientDownloads? downloads = null,
|
||||||
IReadOnlyDictionary<string, string>? checksums = null)
|
IReadOnlyDictionary<string, string>? checksums = null,
|
||||||
|
bool requiresCleanInstall = false)
|
||||||
{
|
{
|
||||||
return new PlondsClientManifest(
|
return new PlondsClientManifest(
|
||||||
FormatVersion: "2.0",
|
FormatVersion: "2.0",
|
||||||
CurrentVersion: version,
|
CurrentVersion: version,
|
||||||
PreviousVersion: "1.0.0",
|
PreviousVersion: "1.0.0",
|
||||||
IsFullUpdate: false,
|
IsFullUpdate: false,
|
||||||
RequiresCleanInstall: false,
|
RequiresCleanInstall: requiresCleanInstall,
|
||||||
Channel: "stable",
|
Channel: "stable",
|
||||||
Platform: "windows-x64",
|
Platform: "windows-x64",
|
||||||
UpdatedAt: DateTimeOffset.Parse("2026-06-01T00:00:00Z"),
|
UpdatedAt: DateTimeOffset.Parse("2026-06-01T00:00:00Z"),
|
||||||
|
|||||||
@@ -140,6 +140,43 @@ public sealed class UpdateSettingsInterfaceTests
|
|||||||
Assert.False(orchestratorCreated);
|
Assert.False(orchestratorCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateSettingsService_WhenPlondsManifestRequiresCleanInstall_ReportsFullInstaller()
|
||||||
|
{
|
||||||
|
var settings = new FakeSettingsService
|
||||||
|
{
|
||||||
|
Snapshot =
|
||||||
|
{
|
||||||
|
UpdateDownloadSource = UpdateSettingsValues.DownloadSourcePlonds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var plonds = new FakePlondsService
|
||||||
|
{
|
||||||
|
LatestResult = PlondsLatestResult.Available(
|
||||||
|
new Version(1, 0, 0),
|
||||||
|
new Version(9, 9, 9),
|
||||||
|
[new PlondsManifestCandidate(
|
||||||
|
new PlondsSourceDescriptor("s3", "s3", "https://s3.test/PLONDS.json", 100),
|
||||||
|
CreatePlondsManifest("9.9.9", requiresCleanInstall: true))])
|
||||||
|
};
|
||||||
|
var orchestratorCreated = false;
|
||||||
|
var service = new UpdateSettingsService(
|
||||||
|
settings,
|
||||||
|
orchestratorFactory: () =>
|
||||||
|
{
|
||||||
|
orchestratorCreated = true;
|
||||||
|
throw new InvalidOperationException("UpdateOrchestrator should not be created for PLONDS check.");
|
||||||
|
},
|
||||||
|
plondsService: plonds);
|
||||||
|
|
||||||
|
var report = await service.CheckAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(report.IsUpdateAvailable);
|
||||||
|
Assert.Equal(UpdatePayloadKind.FullInstaller, report.PayloadKind);
|
||||||
|
Assert.Equal("9.9.9", report.LatestVersion);
|
||||||
|
Assert.False(orchestratorCreated);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateSettingsService_WhenGitHubSelected_UsesOrchestrator()
|
public async Task UpdateSettingsService_WhenGitHubSelected_UsesOrchestrator()
|
||||||
{
|
{
|
||||||
@@ -226,14 +263,14 @@ public sealed class UpdateSettingsInterfaceTests
|
|||||||
new UpdateStateStore(new FakeSettingsFacade(new FakeUpdateSettingsService { State = state })));
|
new UpdateStateStore(new FakeSettingsFacade(new FakeUpdateSettingsService { State = state })));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PlondsClientManifest CreatePlondsManifest(string version)
|
private static PlondsClientManifest CreatePlondsManifest(string version, bool requiresCleanInstall = false)
|
||||||
{
|
{
|
||||||
return new PlondsClientManifest(
|
return new PlondsClientManifest(
|
||||||
FormatVersion: "2.0",
|
FormatVersion: "2.0",
|
||||||
CurrentVersion: version,
|
CurrentVersion: version,
|
||||||
PreviousVersion: "1.0.0",
|
PreviousVersion: "1.0.0",
|
||||||
IsFullUpdate: false,
|
IsFullUpdate: false,
|
||||||
RequiresCleanInstall: false,
|
RequiresCleanInstall: requiresCleanInstall,
|
||||||
Channel: "stable",
|
Channel: "stable",
|
||||||
Platform: "windows-x64",
|
Platform: "windows-x64",
|
||||||
UpdatedAt: DateTimeOffset.Parse("2026-06-01T00:00:00Z"),
|
UpdatedAt: DateTimeOffset.Parse("2026-06-01T00:00:00Z"),
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ internal sealed class PlondsDownloadPlanner(IPlondsPackageDownloader downloader)
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(candidate);
|
ArgumentNullException.ThrowIfNull(candidate);
|
||||||
|
|
||||||
|
if (candidate.Manifest.RequiresCleanInstall)
|
||||||
|
{
|
||||||
|
return PlondsPrepareResult.FailedForUi(
|
||||||
|
"PLONDS manifest requires a clean install. Use the Host Update installer flow instead.");
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var deltaPackage = await downloader
|
var deltaPackage = await downloader
|
||||||
|
|||||||
@@ -55,12 +55,13 @@ internal sealed class PlondsPreparedPackageInstaller
|
|||||||
return new PlondsInstallResult(false, "PLONDS full package directory is missing.", "staging_incomplete");
|
return new PlondsInstallResult(false, "PLONDS full package directory is missing.", "staging_incomplete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sourceAppDirectory = ResolveFullPackageAppDirectory(package.FilesDirectory, package.Version);
|
||||||
var currentDeployment = FindCurrentDeploymentDirectory(launcherRoot);
|
var currentDeployment = FindCurrentDeploymentDirectory(launcherRoot);
|
||||||
var targetDeployment = BuildNextDeploymentDirectory(launcherRoot, package.Version.ToString());
|
var targetDeployment = BuildNextDeploymentDirectory(launcherRoot, package.Version.ToString());
|
||||||
|
|
||||||
progress?.Report(new InstallProgressReport(InstallStage.CreateTarget, "Creating target deployment...", 15, null, 0, 0));
|
progress?.Report(new InstallProgressReport(InstallStage.CreateTarget, "Creating target deployment...", 15, null, 0, 0));
|
||||||
PrepareTargetDirectory(targetDeployment);
|
PrepareTargetDirectory(targetDeployment);
|
||||||
CopyDirectory(package.FilesDirectory, targetDeployment, cancellationToken);
|
CopyDirectory(sourceAppDirectory, targetDeployment, cancellationToken, skipMarkers: true);
|
||||||
|
|
||||||
progress?.Report(new InstallProgressReport(InstallStage.ActivateDeployment, "Activating deployment...", 85, null, 0, 0));
|
progress?.Report(new InstallProgressReport(InstallStage.ActivateDeployment, "Activating deployment...", 85, null, 0, 0));
|
||||||
ActivateDeployment(currentDeployment, targetDeployment);
|
ActivateDeployment(currentDeployment, targetDeployment);
|
||||||
@@ -288,6 +289,40 @@ internal sealed class PlondsPreparedPackageInstaller
|
|||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ResolveFullPackageAppDirectory(string filesDirectory, Version version)
|
||||||
|
{
|
||||||
|
var resolvedRoot = Path.GetFullPath(filesDirectory);
|
||||||
|
if (File.Exists(Path.Combine(resolvedRoot, "LanMountainDesktop.exe")))
|
||||||
|
{
|
||||||
|
return resolvedRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exactAppDirectory = Path.Combine(resolvedRoot, $"app-{version}");
|
||||||
|
if (Directory.Exists(exactAppDirectory) &&
|
||||||
|
File.Exists(Path.Combine(exactAppDirectory, "LanMountainDesktop.exe")))
|
||||||
|
{
|
||||||
|
return exactAppDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
var appDirectory = Directory.GetDirectories(resolvedRoot, "app-*", SearchOption.TopDirectoryOnly)
|
||||||
|
.Where(path => File.Exists(Path.Combine(path, "LanMountainDesktop.exe")))
|
||||||
|
.Select(path => new
|
||||||
|
{
|
||||||
|
Path = path,
|
||||||
|
Version = ParseVersionFromDirectory(path)
|
||||||
|
})
|
||||||
|
.OrderByDescending(item => item.Version)
|
||||||
|
.Select(item => item.Path)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(appDirectory))
|
||||||
|
{
|
||||||
|
return appDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("PLONDS full package does not contain an app deployment directory.");
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildNextDeploymentDirectory(string launcherRoot, string targetVersion)
|
private static string BuildNextDeploymentDirectory(string launcherRoot, string targetVersion)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(launcherRoot);
|
Directory.CreateDirectory(launcherRoot);
|
||||||
|
|||||||
@@ -791,8 +791,11 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
private readonly GitHubReleaseUpdateService _githubReleaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
|
private readonly GitHubReleaseUpdateService _githubReleaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
|
||||||
private readonly IPlondsService _plondsService;
|
private readonly IPlondsService _plondsService;
|
||||||
private readonly PlondsPreparedPackageInstaller _plondsInstaller = new();
|
private readonly PlondsPreparedPackageInstaller _plondsInstaller = new();
|
||||||
|
private readonly UpdateInstallGateway _plondsUpdateInstallGateway = new();
|
||||||
private readonly Lazy<UpdateOrchestrator> _orchestrator;
|
private readonly Lazy<UpdateOrchestrator> _orchestrator;
|
||||||
private PlondsLatestResult? _pendingPlondsLatest;
|
private PlondsLatestResult? _pendingPlondsLatest;
|
||||||
|
private PlondsManifestCandidate? _pendingPlondsCleanInstallCandidate;
|
||||||
|
private UpdateManifest? _pendingPlondsInstallerManifest;
|
||||||
private PlondsPreparedPackage? _pendingPlondsPackage;
|
private PlondsPreparedPackage? _pendingPlondsPackage;
|
||||||
private UpdatePhase _plondsPhase = UpdatePhase.Idle;
|
private UpdatePhase _plondsPhase = UpdatePhase.Idle;
|
||||||
private bool _orchestratorEventsSubscribed;
|
private bool _orchestratorEventsSubscribed;
|
||||||
@@ -957,6 +960,8 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
if (IsPlondsSelected())
|
if (IsPlondsSelected())
|
||||||
{
|
{
|
||||||
_pendingPlondsLatest = null;
|
_pendingPlondsLatest = null;
|
||||||
|
_pendingPlondsCleanInstallCandidate = null;
|
||||||
|
_pendingPlondsInstallerManifest = null;
|
||||||
_pendingPlondsPackage = null;
|
_pendingPlondsPackage = null;
|
||||||
TransitionPlonds(UpdatePhase.Idle);
|
TransitionPlonds(UpdatePhase.Idle);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -979,7 +984,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
{
|
{
|
||||||
if (IsPlondsSelected())
|
if (IsPlondsSelected())
|
||||||
{
|
{
|
||||||
return false;
|
return TryApplyPlondsOnExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetOrchestrator().TryApplyOnExit();
|
return GetOrchestrator().TryApplyOnExit();
|
||||||
@@ -1087,6 +1092,9 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
|
|
||||||
var latest = await _plondsService.FindLatestAsync(currentVersion, cancellationToken).ConfigureAwait(false);
|
var latest = await _plondsService.FindLatestAsync(currentVersion, cancellationToken).ConfigureAwait(false);
|
||||||
_pendingPlondsLatest = latest.Success && latest.IsUpdateAvailable ? latest : null;
|
_pendingPlondsLatest = latest.Success && latest.IsUpdateAvailable ? latest : null;
|
||||||
|
_pendingPlondsCleanInstallCandidate = _pendingPlondsLatest?.Candidates
|
||||||
|
.FirstOrDefault(candidate => candidate.Manifest.RequiresCleanInstall);
|
||||||
|
_pendingPlondsInstallerManifest = null;
|
||||||
_pendingPlondsPackage = null;
|
_pendingPlondsPackage = null;
|
||||||
TransitionPlonds(UpdatePhase.Checked);
|
TransitionPlonds(UpdatePhase.Checked);
|
||||||
SaveLastChecked();
|
SaveLastChecked();
|
||||||
@@ -1096,11 +1104,17 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
return new UpdateCheckReport(false, null, currentVersionText, null, null, null, null, null, null, latest.ErrorMessage);
|
return new UpdateCheckReport(false, null, currentVersionText, null, null, null, null, null, null, latest.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var payloadKind = latest.IsUpdateAvailable
|
||||||
|
? _pendingPlondsCleanInstallCandidate is not null
|
||||||
|
? UpdatePayloadKind.FullInstaller
|
||||||
|
: UpdatePayloadKind.DeltaPlonds
|
||||||
|
: (UpdatePayloadKind?)null;
|
||||||
|
|
||||||
return new UpdateCheckReport(
|
return new UpdateCheckReport(
|
||||||
latest.IsUpdateAvailable,
|
latest.IsUpdateAvailable,
|
||||||
latest.LatestVersion?.ToString(),
|
latest.LatestVersion?.ToString(),
|
||||||
currentVersionText,
|
currentVersionText,
|
||||||
latest.IsUpdateAvailable ? UpdatePayloadKind.DeltaPlonds : null,
|
payloadKind,
|
||||||
latest.Candidates.FirstOrDefault()?.Source.Id,
|
latest.Candidates.FirstOrDefault()?.Source.Id,
|
||||||
Get().UpdateChannel,
|
Get().UpdateChannel,
|
||||||
DateTimeOffset.UtcNow,
|
DateTimeOffset.UtcNow,
|
||||||
@@ -1111,7 +1125,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
|
|
||||||
private async Task<LanMountainDesktop.Services.Update.DownloadResult> DownloadPlondsAsync(CancellationToken cancellationToken)
|
private async Task<LanMountainDesktop.Services.Update.DownloadResult> DownloadPlondsAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_plondsPhase is not UpdatePhase.Checked)
|
if (_plondsPhase is not (UpdatePhase.Checked or UpdatePhase.PausedDownloading))
|
||||||
{
|
{
|
||||||
return new LanMountainDesktop.Services.Update.DownloadResult(false, null, $"Cannot download in phase {_plondsPhase}.", false);
|
return new LanMountainDesktop.Services.Update.DownloadResult(false, null, $"Cannot download in phase {_plondsPhase}.", false);
|
||||||
}
|
}
|
||||||
@@ -1121,8 +1135,13 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
return new LanMountainDesktop.Services.Update.DownloadResult(false, null, "No PLONDS update is pending.", false);
|
return new LanMountainDesktop.Services.Update.DownloadResult(false, null, "No PLONDS update is pending.", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TransitionPlonds(UpdatePhase.Downloading);
|
|
||||||
var currentVersion = _pendingPlondsLatest.CurrentVersion;
|
var currentVersion = _pendingPlondsLatest.CurrentVersion;
|
||||||
|
if (_pendingPlondsCleanInstallCandidate is not null)
|
||||||
|
{
|
||||||
|
return await DownloadPlondsCleanInstallAsync(_pendingPlondsCleanInstallCandidate, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransitionPlonds(UpdatePhase.Downloading);
|
||||||
var result = await _plondsService.FindAndPrepareLatestAsync(currentVersion, cancellationToken).ConfigureAwait(false);
|
var result = await _plondsService.FindAndPrepareLatestAsync(currentVersion, cancellationToken).ConfigureAwait(false);
|
||||||
if (!result.Success || result.Package is null)
|
if (!result.Success || result.Package is null)
|
||||||
{
|
{
|
||||||
@@ -1136,6 +1155,95 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
return new LanMountainDesktop.Services.Update.DownloadResult(true, result.Package.ManifestPath, null, true);
|
return new LanMountainDesktop.Services.Update.DownloadResult(true, result.Package.ManifestPath, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<LanMountainDesktop.Services.Update.DownloadResult> DownloadPlondsCleanInstallAsync(
|
||||||
|
PlondsManifestCandidate candidate,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
TransitionPlonds(UpdatePhase.Downloading);
|
||||||
|
|
||||||
|
var manifest = await ResolveGitHubInstallerManifestForPlondsAsync(candidate.Manifest, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (manifest is null)
|
||||||
|
{
|
||||||
|
TransitionPlonds(UpdatePhase.Failed);
|
||||||
|
return new LanMountainDesktop.Services.Update.DownloadResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
$"PLONDS {candidate.Manifest.CurrentVersion} requires clean install, but no matching GitHub installer release was found.",
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mirror = manifest.InstallerMirrors?
|
||||||
|
.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item.Url));
|
||||||
|
if (mirror is null || string.IsNullOrWhiteSpace(mirror.Url))
|
||||||
|
{
|
||||||
|
TransitionPlonds(UpdatePhase.Failed);
|
||||||
|
return new LanMountainDesktop.Services.Update.DownloadResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
$"PLONDS {candidate.Manifest.CurrentVersion} requires clean install, but GitHub release has no usable installer asset.",
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = string.IsNullOrWhiteSpace(mirror.Name)
|
||||||
|
? $"{manifest.DistributionId}-{manifest.ToVersion}-installer.exe"
|
||||||
|
: mirror.Name!;
|
||||||
|
var asset = new GitHubReleaseAsset(fileName, mirror.Url!, mirror.Size, mirror.Sha256);
|
||||||
|
var destinationPath = CreateInstallerDestinationPath(manifest, fileName);
|
||||||
|
var maxThreads = UpdateSettingsValues.NormalizeDownloadThreads(Get().UpdateDownloadThreads);
|
||||||
|
var progress = new Progress<double>(fraction =>
|
||||||
|
{
|
||||||
|
var downloadReport = new DownloadProgressReport(
|
||||||
|
fileName,
|
||||||
|
0,
|
||||||
|
Math.Max(0, mirror.Size),
|
||||||
|
0,
|
||||||
|
fraction >= 1 ? 1 : 0,
|
||||||
|
1,
|
||||||
|
Math.Clamp(fraction, 0, 1));
|
||||||
|
|
||||||
|
_progressChanged?.Invoke(new UpdateProgressReport(
|
||||||
|
UpdatePhase.Downloading,
|
||||||
|
$"Downloading {fileName}",
|
||||||
|
Math.Clamp(fraction, 0, 1),
|
||||||
|
downloadReport,
|
||||||
|
null));
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _githubReleaseUpdateService.DownloadAssetAsync(
|
||||||
|
asset,
|
||||||
|
destinationPath,
|
||||||
|
UpdateSettingsValues.DownloadSourceGitHub,
|
||||||
|
maxThreads,
|
||||||
|
progress,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!result.Success || string.IsNullOrWhiteSpace(result.FilePath))
|
||||||
|
{
|
||||||
|
TransitionPlonds(UpdatePhase.Failed);
|
||||||
|
return new LanMountainDesktop.Services.Update.DownloadResult(
|
||||||
|
false,
|
||||||
|
result.FilePath,
|
||||||
|
result.ErrorMessage ?? "Failed to download GitHub installer for PLONDS clean install.",
|
||||||
|
result.HashVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
var launcherRoot = UpdatePaths.ResolveLauncherRoot(AppContext.BaseDirectory);
|
||||||
|
DeploymentLockService.WriteLock(launcherRoot, new DeploymentLock(
|
||||||
|
SchemaVersion: 1,
|
||||||
|
Kind: "full",
|
||||||
|
TargetVersion: manifest.ToVersion,
|
||||||
|
PayloadPath: result.FilePath,
|
||||||
|
PayloadSha256: result.ExpectedHash,
|
||||||
|
CreatedAtUtc: DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
_pendingPlondsInstallerManifest = manifest;
|
||||||
|
_pendingPlondsPackage = null;
|
||||||
|
TransitionPlonds(UpdatePhase.Downloaded);
|
||||||
|
SavePendingPlondsInstaller(manifest, result.FilePath, result.ExpectedHash);
|
||||||
|
return new LanMountainDesktop.Services.Update.DownloadResult(true, result.FilePath, null, result.HashVerified);
|
||||||
|
}
|
||||||
|
|
||||||
private Task PausePlondsAsync()
|
private Task PausePlondsAsync()
|
||||||
{
|
{
|
||||||
if (_plondsPhase.CanPause())
|
if (_plondsPhase.CanPause())
|
||||||
@@ -1160,6 +1268,11 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
return new InstallResult(false, $"Cannot install in phase {_plondsPhase}.", false, "invalid_phase");
|
return new InstallResult(false, $"Cannot install in phase {_plondsPhase}.", false, "invalid_phase");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_pendingPlondsInstallerManifest is not null)
|
||||||
|
{
|
||||||
|
return await InstallPlondsCleanInstallAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (_pendingPlondsPackage is null)
|
if (_pendingPlondsPackage is null)
|
||||||
{
|
{
|
||||||
return new InstallResult(false, "No PLONDS package has been prepared.", false, "staging_incomplete");
|
return new InstallResult(false, "No PLONDS package has been prepared.", false, "staging_incomplete");
|
||||||
@@ -1188,6 +1301,37 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
return new InstallResult(true, null, false);
|
return new InstallResult(true, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<InstallResult> InstallPlondsCleanInstallAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
TransitionPlonds(UpdatePhase.Installing);
|
||||||
|
var launcherRoot = UpdatePaths.ResolveLauncherRoot(AppContext.BaseDirectory);
|
||||||
|
var progress = new Progress<InstallProgressReport>(report =>
|
||||||
|
{
|
||||||
|
_progressChanged?.Invoke(new UpdateProgressReport(
|
||||||
|
UpdatePhase.Installing,
|
||||||
|
report.Message,
|
||||||
|
report.ProgressPercent / 100.0,
|
||||||
|
null,
|
||||||
|
report));
|
||||||
|
});
|
||||||
|
|
||||||
|
var install = await _plondsUpdateInstallGateway.InstallAsync(
|
||||||
|
UpdatePayloadKind.FullInstaller,
|
||||||
|
launcherRoot,
|
||||||
|
progress,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!install.Success)
|
||||||
|
{
|
||||||
|
TransitionPlonds(UpdatePhase.Failed);
|
||||||
|
return install;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransitionPlonds(UpdatePhase.Installed);
|
||||||
|
return install;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AutoCheckPlondsIfEnabledAsync(CancellationToken cancellationToken)
|
private async Task AutoCheckPlondsIfEnabledAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var settings = Get();
|
var settings = Get();
|
||||||
@@ -1269,6 +1413,160 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SavePendingPlondsInstaller(UpdateManifest manifest, string installerPath, string? sha256)
|
||||||
|
{
|
||||||
|
var state = Get();
|
||||||
|
Save(state with
|
||||||
|
{
|
||||||
|
PendingUpdateInstallerPath = installerPath,
|
||||||
|
PendingUpdateVersion = manifest.ToVersion,
|
||||||
|
PendingUpdatePublishedAtUtcMs = manifest.PublishedAt.ToUnixTimeMilliseconds(),
|
||||||
|
PendingUpdateSha256 = string.IsNullOrWhiteSpace(sha256) ? null : sha256.Trim().ToLowerInvariant(),
|
||||||
|
LastUpdateCheckUtcMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryApplyPlondsOnExit()
|
||||||
|
{
|
||||||
|
var settings = Get();
|
||||||
|
if (!string.Equals(
|
||||||
|
UpdateSettingsValues.NormalizeMode(settings.UpdateMode),
|
||||||
|
UpdateSettingsValues.ModeSilentOnExit,
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var launcherRoot = UpdatePaths.ResolveLauncherRoot(AppContext.BaseDirectory);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_pendingPlondsPackage is not null)
|
||||||
|
{
|
||||||
|
AppLogger.Info("UpdateWorkflow", "PLONDS package pending. Applying from Host on exit.");
|
||||||
|
var result = _plondsInstaller.InstallAsync(
|
||||||
|
_pendingPlondsPackage,
|
||||||
|
launcherRoot,
|
||||||
|
progress: null,
|
||||||
|
CancellationToken.None)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
return result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deploymentLock = DeploymentLockService.ReadLock(launcherRoot);
|
||||||
|
if (!string.Equals(deploymentLock?.Kind, "full", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(settings.PendingUpdateInstallerPath) ||
|
||||||
|
!File.Exists(settings.PendingUpdateInstallerPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.Info("UpdateWorkflow", "PLONDS clean-install installer pending. Launching from Host Update on exit.");
|
||||||
|
var install = _plondsUpdateInstallGateway.InstallAsync(
|
||||||
|
UpdatePayloadKind.FullInstaller,
|
||||||
|
launcherRoot,
|
||||||
|
progress: null,
|
||||||
|
CancellationToken.None)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
return install.Success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("UpdateWorkflow", "Failed to apply pending PLONDS update on exit.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<UpdateManifest?> ResolveGitHubInstallerManifestForPlondsAsync(
|
||||||
|
PlondsClientManifest plondsManifest,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var tag in BuildReleaseTagCandidates(plondsManifest.CurrentVersion))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var release = await _githubReleaseUpdateService
|
||||||
|
.GetReleaseByTagAsync(tag, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (release is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateManifestMapper.FromFullInstaller(release, Get().UpdateChannel, ResolveCurrentPlatform());
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("UpdateWorkflow", $"Failed to resolve GitHub installer release '{tag}' for PLONDS clean install: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> BuildReleaseTagCandidates(string? version)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = version.Trim();
|
||||||
|
if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
yield return trimmed;
|
||||||
|
yield return trimmed[1..];
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return $"v{trimmed}";
|
||||||
|
yield return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateInstallerDestinationPath(UpdateManifest manifest, string fileName)
|
||||||
|
{
|
||||||
|
var safeFileName = string.Join(
|
||||||
|
"_",
|
||||||
|
fileName.Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(safeFileName))
|
||||||
|
{
|
||||||
|
safeFileName = $"{manifest.DistributionId}-{manifest.ToVersion}-installer.exe";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"LanMountainDesktop",
|
||||||
|
"Updates",
|
||||||
|
safeFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveCurrentPlatform()
|
||||||
|
{
|
||||||
|
var os = OperatingSystem.IsWindows()
|
||||||
|
? "windows"
|
||||||
|
: OperatingSystem.IsLinux()
|
||||||
|
? "linux"
|
||||||
|
: OperatingSystem.IsMacOS()
|
||||||
|
? "macos"
|
||||||
|
: "unknown";
|
||||||
|
var arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture switch
|
||||||
|
{
|
||||||
|
System.Runtime.InteropServices.Architecture.Arm64 => "arm64",
|
||||||
|
System.Runtime.InteropServices.Architecture.X86 => "x86",
|
||||||
|
_ => "x64"
|
||||||
|
};
|
||||||
|
return $"{os}-{arch}";
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryParseVersion(string? value, out Version version)
|
private static bool TryParseVersion(string? value, out Version version)
|
||||||
{
|
{
|
||||||
version = new Version(0, 0, 0);
|
version = new Version(0, 0, 0);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ internal sealed class UpdateInstallGateway
|
|||||||
0,
|
0,
|
||||||
0));
|
0));
|
||||||
|
|
||||||
if (!VerifyDeploymentLock(payloadKind, launcherRoot, out var lockErrorCode, out var lockError))
|
if (!VerifyDeploymentLock(payloadKind, launcherRoot, out var deploymentLock, out var lockErrorCode, out var lockError))
|
||||||
{
|
{
|
||||||
return new InstallResult(false, lockError, false, lockErrorCode);
|
return new InstallResult(false, lockError, false, lockErrorCode);
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ internal sealed class UpdateInstallGateway
|
|||||||
return new InstallResult(true, null, false);
|
return new InstallResult(true, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var installerPath = FindPendingInstaller(launcherRoot, payloadKind, ct);
|
var installerPath = FindPendingInstaller(launcherRoot, deploymentLock!, ct);
|
||||||
if (installerPath is null)
|
if (installerPath is null)
|
||||||
{
|
{
|
||||||
return new InstallResult(false, "No pending installer found.", false, "staging_incomplete");
|
return new InstallResult(false, "No pending installer found.", false, "staging_incomplete");
|
||||||
@@ -92,19 +92,25 @@ internal sealed class UpdateInstallGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool VerifyDeploymentLock(UpdatePayloadKind payloadKind, string launcherRoot, out string? errorCode, out string? error)
|
private static bool VerifyDeploymentLock(
|
||||||
|
UpdatePayloadKind payloadKind,
|
||||||
|
string launcherRoot,
|
||||||
|
out DeploymentLock? deploymentLock,
|
||||||
|
out string? errorCode,
|
||||||
|
out string? error)
|
||||||
{
|
{
|
||||||
|
deploymentLock = null;
|
||||||
errorCode = null;
|
errorCode = null;
|
||||||
error = null;
|
error = null;
|
||||||
var deploymentLock = DeploymentLockService.ReadLock(launcherRoot);
|
var currentLock = DeploymentLockService.ReadLock(launcherRoot);
|
||||||
if (deploymentLock is null)
|
if (currentLock is null)
|
||||||
{
|
{
|
||||||
errorCode = "lock_conflict";
|
errorCode = "lock_conflict";
|
||||||
error = "Deployment lock is missing. Please redownload the update.";
|
error = "Deployment lock is missing. Please redownload the update.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deploymentLock.SchemaVersion != 1)
|
if (currentLock.SchemaVersion != 1)
|
||||||
{
|
{
|
||||||
errorCode = "lock_conflict";
|
errorCode = "lock_conflict";
|
||||||
error = "Deployment lock schema is unsupported. Please redownload the update.";
|
error = "Deployment lock schema is unsupported. Please redownload the update.";
|
||||||
@@ -112,22 +118,23 @@ internal sealed class UpdateInstallGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expectedKind = payloadKind is UpdatePayloadKind.DeltaPlonds ? "delta" : "full";
|
var expectedKind = payloadKind is UpdatePayloadKind.DeltaPlonds ? "delta" : "full";
|
||||||
if (!string.Equals(deploymentLock.Kind, expectedKind, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(currentLock.Kind, expectedKind, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
errorCode = "lock_conflict";
|
errorCode = "lock_conflict";
|
||||||
error = "Deployment lock payload type mismatch. Please redownload the update.";
|
error = "Deployment lock payload type mismatch. Please redownload the update.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(deploymentLock.PayloadPath) ||
|
if (string.IsNullOrWhiteSpace(currentLock.PayloadPath) ||
|
||||||
!File.Exists(deploymentLock.PayloadPath) &&
|
!File.Exists(currentLock.PayloadPath) &&
|
||||||
!Directory.Exists(deploymentLock.PayloadPath))
|
!Directory.Exists(currentLock.PayloadPath))
|
||||||
{
|
{
|
||||||
errorCode = "staging_incomplete";
|
errorCode = "staging_incomplete";
|
||||||
error = "Deployment lock payload path is missing. Please redownload the update.";
|
error = "Deployment lock payload path is missing. Please redownload the update.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deploymentLock = currentLock;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,10 +247,17 @@ internal sealed class UpdateInstallGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FindPendingInstaller(string launcherRoot, UpdatePayloadKind payloadKind, CancellationToken ct)
|
private static string? FindPendingInstaller(string launcherRoot, DeploymentLock deploymentLock, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(deploymentLock.PayloadPath) &&
|
||||||
|
File.Exists(deploymentLock.PayloadPath) &&
|
||||||
|
Path.GetExtension(deploymentLock.PayloadPath).Equals(".exe", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return deploymentLock.PayloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
var incomingDir = UpdatePaths.GetIncomingDirectory(launcherRoot);
|
var incomingDir = UpdatePaths.GetIncomingDirectory(launcherRoot);
|
||||||
if (!Directory.Exists(incomingDir))
|
if (!Directory.Exists(incomingDir))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public sealed class PlondsCommitDeltaBuilder
|
|||||||
|
|
||||||
Directory.CreateDirectory(outputRoot);
|
Directory.CreateDirectory(outputRoot);
|
||||||
PayloadUtilities.ExtractZip(currentPayloadZip, currentExtractRoot);
|
PayloadUtilities.ExtractZip(currentPayloadZip, currentExtractRoot);
|
||||||
|
var currentAppRoot = PlondsDeltaBuilder.ResolvePayloadAppRoot(currentExtractRoot, options.CurrentVersion);
|
||||||
|
|
||||||
var changedSourceFiles = GetChangedSourceFiles(options.BaselineTag, options.CurrentTag, sourceDirs);
|
var changedSourceFiles = GetChangedSourceFiles(options.BaselineTag, options.CurrentTag, sourceDirs);
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ public sealed class PlondsCommitDeltaBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
var artifactFiles = MapSourceToArtifacts(changedSourceFiles, sourceDirs);
|
var artifactFiles = MapSourceToArtifacts(changedSourceFiles, sourceDirs);
|
||||||
var currentManifest = PayloadUtilities.ScanDirectory(currentExtractRoot);
|
var currentManifest = PayloadUtilities.ScanDirectory(currentAppRoot);
|
||||||
|
|
||||||
var filesMap = new Dictionary<string, PlondsFileEntry>(StringComparer.OrdinalIgnoreCase);
|
var filesMap = new Dictionary<string, PlondsFileEntry>(StringComparer.OrdinalIgnoreCase);
|
||||||
var changedFilesMap = new Dictionary<string, PlondsChangedFileEntry>(StringComparer.OrdinalIgnoreCase);
|
var changedFilesMap = new Dictionary<string, PlondsChangedFileEntry>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -97,7 +98,7 @@ public sealed class PlondsCommitDeltaBuilder
|
|||||||
changedFilesMap[normalizedPath] = new PlondsChangedFileEntry(normalizedPath, fileHash, fingerprint.Size, hashAlgorithm);
|
changedFilesMap[normalizedPath] = new PlondsChangedFileEntry(normalizedPath, fileHash, fingerprint.Size, hashAlgorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
var changedZipPath = CreateChangedZipFromList(currentExtractRoot, artifactFiles, outputRoot, options.Platform);
|
var changedZipPath = CreateChangedZipFromList(currentAppRoot, artifactFiles, outputRoot, options.Platform);
|
||||||
var changedZipMd5 = ComputeMd5Hex(changedZipPath);
|
var changedZipMd5 = ComputeMd5Hex(changedZipPath);
|
||||||
|
|
||||||
var launcherInChanges = artifactFiles.Any(f =>
|
var launcherInChanges = artifactFiles.Any(f =>
|
||||||
|
|||||||
@@ -47,17 +47,26 @@ public sealed class PlondsDeltaBuilder
|
|||||||
PayloadUtilities.ExtractZip(baselinePayloadZip!, baselineExtractRoot);
|
PayloadUtilities.ExtractZip(baselinePayloadZip!, baselineExtractRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentAppRoot = ResolvePayloadAppRoot(currentExtractRoot, options.CurrentVersion);
|
||||||
|
var baselineAppRoot = isFullUpdate
|
||||||
|
? null
|
||||||
|
: ResolvePayloadAppRoot(baselineExtractRoot, options.BaselineVersion);
|
||||||
|
|
||||||
var previousManifest = isFullUpdate
|
var previousManifest = isFullUpdate
|
||||||
? new Dictionary<string, PayloadUtilities.FileFingerprint>(StringComparer.OrdinalIgnoreCase)
|
? new Dictionary<string, PayloadUtilities.FileFingerprint>(StringComparer.OrdinalIgnoreCase)
|
||||||
: PayloadUtilities.ScanDirectory(baselineExtractRoot);
|
: PayloadUtilities.ScanDirectory(baselineAppRoot);
|
||||||
var currentManifest = PayloadUtilities.ScanDirectory(currentExtractRoot);
|
var currentManifest = PayloadUtilities.ScanDirectory(currentAppRoot);
|
||||||
|
|
||||||
var filesMap = BuildFilesMap(previousManifest, currentManifest, hashAlgorithm);
|
var filesMap = BuildFilesMap(previousManifest, currentManifest, hashAlgorithm);
|
||||||
var changedFilesMap = BuildChangedFilesMap(filesMap, hashAlgorithm);
|
var changedFilesMap = BuildChangedFilesMap(filesMap, hashAlgorithm);
|
||||||
|
|
||||||
var changedZipPath = CreateChangedZip(currentExtractRoot, filesMap, outputRoot, options.Platform);
|
var changedZipPath = CreateChangedZip(currentAppRoot, filesMap, outputRoot, options.Platform);
|
||||||
|
|
||||||
var launcherChanged = DetectLauncherChange(previousManifest, currentManifest, options.LauncherRelativePath);
|
var previousRootManifest = isFullUpdate
|
||||||
|
? new Dictionary<string, PayloadUtilities.FileFingerprint>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
: PayloadUtilities.ScanDirectory(baselineExtractRoot);
|
||||||
|
var currentRootManifest = PayloadUtilities.ScanDirectory(currentExtractRoot);
|
||||||
|
var launcherChanged = DetectLauncherChange(previousRootManifest, currentRootManifest, options.LauncherRelativePath);
|
||||||
var requiresCleanInstall = launcherChanged && !isFullUpdate;
|
var requiresCleanInstall = launcherChanged && !isFullUpdate;
|
||||||
|
|
||||||
var changedZipMd5 = ComputeMd5Hex(changedZipPath);
|
var changedZipMd5 = ComputeMd5Hex(changedZipPath);
|
||||||
@@ -216,6 +225,34 @@ public sealed class PlondsDeltaBuilder
|
|||||||
return !string.Equals(current.Sha256, previous.Sha256, StringComparison.OrdinalIgnoreCase);
|
return !string.Equals(current.Sha256, previous.Sha256, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static string ResolvePayloadAppRoot(string extractRoot, string? version)
|
||||||
|
{
|
||||||
|
var resolvedRoot = Path.GetFullPath(extractRoot);
|
||||||
|
if (File.Exists(Path.Combine(resolvedRoot, "LanMountainDesktop.exe")))
|
||||||
|
{
|
||||||
|
return resolvedRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
var versionedAppRoot = Path.Combine(resolvedRoot, $"app-{version.Trim().TrimStart('v', 'V')}");
|
||||||
|
if (Directory.Exists(versionedAppRoot) &&
|
||||||
|
File.Exists(Path.Combine(versionedAppRoot, "LanMountainDesktop.exe")))
|
||||||
|
{
|
||||||
|
return versionedAppRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var appRoots = Directory.Exists(resolvedRoot)
|
||||||
|
? Directory.GetDirectories(resolvedRoot, "app-*", SearchOption.TopDirectoryOnly)
|
||||||
|
.Where(path => File.Exists(Path.Combine(path, "LanMountainDesktop.exe")))
|
||||||
|
.OrderByDescending(Path.GetFileName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return appRoots.FirstOrDefault() ?? resolvedRoot;
|
||||||
|
}
|
||||||
|
|
||||||
internal static string ComputeHash(string filePath, string hashAlgorithm)
|
internal static string ComputeHash(string filePath, string hashAlgorithm)
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead(filePath);
|
using var stream = File.OpenRead(filePath);
|
||||||
|
|||||||
Reference in New Issue
Block a user