From 75c7aece4fefcdb4d593c12aff7066d405e4be41 Mon Sep 17 00:00:00 2001 From: lincube Date: Wed, 3 Jun 2026 07:30:54 +0800 Subject: [PATCH] =?UTF-8?q?fix.=E5=9C=A8=E7=BA=BF=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LanDesktopPLONDS.installer/App.axaml.cs | 4 +- .../LanDesktopPLONDS.installer.csproj | 3 +- .../Services/InstallerJsonContext.cs | 5 + .../Services/InstallerPlondsClient.cs | 120 +++++++++++------- LanDesktopPLONDS.installer/app.Debug.manifest | 18 +++ .../OnlineInstallerCoreTests.cs | 106 +++++++++++++++- .../Plonds/PlondsClientServiceFactory.cs | 2 +- 7 files changed, 211 insertions(+), 47 deletions(-) create mode 100644 LanDesktopPLONDS.installer/app.Debug.manifest diff --git a/LanDesktopPLONDS.installer/App.axaml.cs b/LanDesktopPLONDS.installer/App.axaml.cs index f0ce791..ce932ce 100644 --- a/LanDesktopPLONDS.installer/App.axaml.cs +++ b/LanDesktopPLONDS.installer/App.axaml.cs @@ -22,10 +22,12 @@ public partial class App : Application var privacyIdentity = new PrivacyDeviceIdentityProvider(); var installService = OnlineInstallService.CreateDefault(privacyIdentity); var consentStore = new InstallerPrivacyConsentStore(); - desktop.MainWindow = new MainWindow + var mainWindow = new MainWindow { DataContext = new MainWindowViewModel(installService, privacyIdentity, consentStore) }; + desktop.MainWindow = mainWindow; + mainWindow.Show(); } base.OnFrameworkInitializationCompleted(); diff --git a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj index 2c756bb..5733597 100644 --- a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj +++ b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj @@ -10,7 +10,8 @@ $(Version) true Assets\logo.ico - app.manifest + app.Debug.manifest + app.manifest diff --git a/LanDesktopPLONDS.installer/Services/InstallerJsonContext.cs b/LanDesktopPLONDS.installer/Services/InstallerJsonContext.cs index 8beaef4..ed2e98f 100644 --- a/LanDesktopPLONDS.installer/Services/InstallerJsonContext.cs +++ b/LanDesktopPLONDS.installer/Services/InstallerJsonContext.cs @@ -2,5 +2,10 @@ using System.Text.Json.Serialization; namespace LanDesktopPLONDS.Installer.Services; +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + ReadCommentHandling = System.Text.Json.JsonCommentHandling.Skip, + AllowTrailingCommas = true)] [JsonSerializable(typeof(InstallerPlondsManifest))] internal sealed partial class InstallerJsonContext : JsonSerializerContext; diff --git a/LanDesktopPLONDS.installer/Services/InstallerPlondsClient.cs b/LanDesktopPLONDS.installer/Services/InstallerPlondsClient.cs index 79175e8..aed75a6 100644 --- a/LanDesktopPLONDS.installer/Services/InstallerPlondsClient.cs +++ b/LanDesktopPLONDS.installer/Services/InstallerPlondsClient.cs @@ -10,7 +10,7 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin { private const string S3ManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_S3_MANIFEST_URL"; private const string GitHubManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_GITHUB_MANIFEST_URL"; - private const string DefaultS3ManifestUrl = "https://cn-nb1.rains3.com/lmdesktop/plonds/PLONDS.json"; + private const string DefaultS3ManifestUrl = "https://cn-nb1.rains3.com/lmdesktop/lanmountain/update/plonds/PLONDS.json"; private const string DefaultGitHubManifestUrl = "https://github.com/wwiinnddyy/LanMountainDesktop/releases/latest/download/PLONDS.json"; private static readonly JsonSerializerOptions JsonOptions = new() @@ -78,30 +78,54 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin { var version = ParseVersion(candidate.Manifest.CurrentVersion).ToString(); var packageRoot = Path.Combine(stagingRoot, SanitizePathSegment(version), SanitizePathSegment(candidate.Source.Id), "full"); - if (Directory.Exists(packageRoot)) + var urls = new[] { candidate.FilesZipUrl } + .Concat(InstallerPlondsUrlResolver.ResolveFilesZipUrls(candidate.Manifest, candidate.Source)) + .DistinctBy(uri => uri.AbsoluteUri, StringComparer.OrdinalIgnoreCase) + .ToArray(); + Exception? lastError = null; + + foreach (var filesZipUrl in urls) { - Directory.Delete(packageRoot, recursive: true); + cancellationToken.ThrowIfCancellationRequested(); + if (Directory.Exists(packageRoot)) + { + Directory.Delete(packageRoot, recursive: true); + } + + Directory.CreateDirectory(packageRoot); + var zipPath = Path.Combine(packageRoot, "Files.zip"); + var extractDirectory = Path.Combine(packageRoot, "Files"); + Directory.CreateDirectory(extractDirectory); + var attempt = candidate with { FilesZipUrl = filesZipUrl }; + + try + { + await DownloadToFileAsync(attempt, zipPath, progress, cancellationToken).ConfigureAwait(false); + await VerifyPackageAsync(zipPath, attempt.Manifest, filesZipUrl, cancellationToken).ConfigureAwait(false); + ExtractZip(zipPath, extractDirectory); + + progress?.Report(new InstallerDeployProgress( + "Files package prepared", + version, + 1, + 0.10, + "Files.zip", + new FileInfo(zipPath).Length, + new FileInfo(zipPath).Length)); + + return new PreparedFilesPackage(version, candidate.Source.Id, zipPath, extractDirectory, candidate.Manifest); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + lastError = ex; + } } - Directory.CreateDirectory(packageRoot); - var zipPath = Path.Combine(packageRoot, "Files.zip"); - var extractDirectory = Path.Combine(packageRoot, "Files"); - Directory.CreateDirectory(extractDirectory); - - await DownloadToFileAsync(candidate, zipPath, progress, cancellationToken).ConfigureAwait(false); - await VerifyPackageAsync(zipPath, candidate.Manifest, candidate.FilesZipUrl, cancellationToken).ConfigureAwait(false); - ExtractZip(zipPath, extractDirectory); - - progress?.Report(new InstallerDeployProgress( - "Files package prepared", - version, - 1, - 0.10, - "Files.zip", - new FileInfo(zipPath).Length, - new FileInfo(zipPath).Length)); - - return new PreparedFilesPackage(version, candidate.Source.Id, zipPath, extractDirectory, candidate.Manifest); + throw new InvalidOperationException("Failed to download and prepare the PLONDS Files package.", lastError); } public static long EstimateInstallBytes(InstallerPlondsManifest manifest) @@ -140,33 +164,43 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin var totalBytes = response.Content.Headers.ContentLength; var partialPath = $"{destinationPath}.partial"; long downloaded = 0; - await using (var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) - await using (var target = File.Create(partialPath)) + try { - var buffer = new byte[128 * 1024]; - while (true) + await using (var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) + await using (var target = File.Create(partialPath)) { - var read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - if (read == 0) + var buffer = new byte[128 * 1024]; + while (true) { - break; - } + var read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (read == 0) + { + break; + } - await target.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); - downloaded += read; - var fraction = totalBytes is > 0 ? Math.Clamp((double)downloaded / totalBytes.Value, 0, 1) : 0; - progress?.Report(new InstallerDeployProgress( - "Downloading Files.zip", - candidate.Manifest.CurrentVersion, - fraction, - 0, - "Files.zip", - downloaded, - totalBytes)); + await target.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); + downloaded += read; + var fraction = totalBytes is > 0 ? Math.Clamp((double)downloaded / totalBytes.Value, 0, 1) : 0; + progress?.Report(new InstallerDeployProgress( + "Downloading Files.zip", + candidate.Manifest.CurrentVersion, + fraction, + 0, + "Files.zip", + downloaded, + totalBytes)); + } + } + + File.Move(partialPath, destinationPath, overwrite: true); + } + finally + { + if (File.Exists(partialPath)) + { + File.Delete(partialPath); } } - - File.Move(partialPath, destinationPath, overwrite: true); } private static async Task VerifyPackageAsync( diff --git a/LanDesktopPLONDS.installer/app.Debug.manifest b/LanDesktopPLONDS.installer/app.Debug.manifest new file mode 100644 index 0000000..db6b639 --- /dev/null +++ b/LanDesktopPLONDS.installer/app.Debug.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs b/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs index 3504637..493ee20 100644 --- a/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs +++ b/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs @@ -84,6 +84,45 @@ public sealed class OnlineInstallerCoreTests : IDisposable Assert.Contains(urls, uri => uri.AbsoluteUri == "https://github.test/Files.zip"); } + [Fact] + public async Task FindLatest_ParsesCamelCasePlondsManifest() + { + var client = new InstallerPlondsClient( + new HttpClient(new ManifestHandler(""" + { + "formatVersion": "2.0", + "currentVersion": "1.2.4", + "previousVersion": "1.2.3", + "isFullUpdate": false, + "requiresCleanInstall": true, + "channel": "preview", + "platform": "windows-x64", + "updatedAt": "2026-06-03T00:00:00Z", + "filesMap": {}, + "changedFilesMap": {}, + "checksums": { + "Files.zip": "md5:00000000000000000000000000000000" + }, + "downloads": { + "s3": { + "filesZipUrl": "https://s3.test/Files.zip" + }, + "github": { + "filesZipUrl": "https://github.test/files-windows-x64.zip" + } + } + } + """)), + Path.Combine(_tempRoot, "staging")); + + var candidate = await client.FindLatestAsync(CancellationToken.None); + + Assert.Equal("1.2.4", candidate.Manifest.CurrentVersion); + Assert.Equal("preview", candidate.Manifest.Channel); + Assert.Equal("https://s3.test/Files.zip", candidate.FilesZipUrl.AbsoluteUri); + } + + [Theory] [InlineData("")] [InlineData("C:\\")] @@ -141,7 +180,43 @@ public sealed class OnlineInstallerCoreTests : IDisposable manifest, new Uri("https://s3.test/Files.zip")); - await Assert.ThrowsAsync(() => client.DownloadAndPrepareFullPackageAsync(candidate, null, CancellationToken.None)); + var exception = await Assert.ThrowsAsync( + () => client.DownloadAndPrepareFullPackageAsync(candidate, null, CancellationToken.None)); + Assert.IsType(exception.InnerException); + } + + [Fact] + public async Task DownloadAndPrepareFullPackage_FallsBackWhenFirstPackageUrlFails() + { + var zipPath = Path.Combine(_tempRoot, "Files.zip"); + Directory.CreateDirectory(_tempRoot); + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + { + var entry = archive.CreateEntry("LanMountainDesktop.exe"); + await using var stream = entry.Open(); + await using var writer = new StreamWriter(stream); + await writer.WriteAsync("host"); + } + + var manifest = CreateManifest( + downloads: new InstallerPlondsDownloads( + new InstallerPlondsGitHubDownloads(null, null, null, "https://github.test/files-windows-x64.zip"), + new InstallerPlondsS3Downloads(null, null, null, null, null, null, null, null, null, "https://s3.test/Files.zip", null, null)), + checksums: new Dictionary + { + ["Files.zip"] = "sha256:" + Sha256(zipPath) + }); + var client = new InstallerPlondsClient( + new HttpClient(new FallbackPackageHandler(zipPath)), + Path.Combine(_tempRoot, "staging")); + var candidate = new InstallerPlondsCandidate( + new InstallerPlondsSource("s3", "s3", "https://origin.test/releases/PLONDS.json", 100), + manifest, + new Uri("https://s3.test/Files.zip")); + + var package = await client.DownloadAndPrepareFullPackageAsync(candidate, null, CancellationToken.None); + + Assert.True(File.Exists(Path.Combine(package.ExtractDirectory, "LanMountainDesktop.exe"))); } private static InstallerPlondsManifest CreateManifest( @@ -205,4 +280,33 @@ public sealed class OnlineInstallerCoreTests : IDisposable return Task.FromResult(response); } } + + private sealed class FallbackPackageHandler(string zipPath) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri?.AbsoluteUri == "https://github.test/files-windows-x64.zip") + { + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new ByteArrayContent(File.ReadAllBytes(zipPath)) + }; + response.Content.Headers.ContentLength = new FileInfo(zipPath).Length; + return Task.FromResult(response); + } + + return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.NotFound)); + } + } + + private sealed class ManifestHandler(string manifestJson) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(manifestJson) + }); + } + } } diff --git a/LanMountainDesktop/Services/Plonds/PlondsClientServiceFactory.cs b/LanMountainDesktop/Services/Plonds/PlondsClientServiceFactory.cs index 1c0f51d..b2fe4cf 100644 --- a/LanMountainDesktop/Services/Plonds/PlondsClientServiceFactory.cs +++ b/LanMountainDesktop/Services/Plonds/PlondsClientServiceFactory.cs @@ -4,7 +4,7 @@ internal static class PlondsClientServiceFactory { private const string S3ManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_S3_MANIFEST_URL"; private const string GitHubManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_GITHUB_MANIFEST_URL"; - private const string DefaultS3ManifestUrl = "https://cn-nb1.rains3.com/lmdesktop/plonds/PLONDS.json"; + private const string DefaultS3ManifestUrl = "https://cn-nb1.rains3.com/lmdesktop/lanmountain/update/plonds/PLONDS.json"; private const string DefaultGitHubManifestUrl = "https://github.com/wwiinnddyy/LanMountainDesktop/releases/latest/download/PLONDS.json"; public static IPlondsService CreateDefault(HttpClient? httpClient = null)