mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
fix.在线安装器
This commit is contained in:
@@ -22,10 +22,12 @@ public partial class App : Application
|
|||||||
var privacyIdentity = new PrivacyDeviceIdentityProvider();
|
var privacyIdentity = new PrivacyDeviceIdentityProvider();
|
||||||
var installService = OnlineInstallService.CreateDefault(privacyIdentity);
|
var installService = OnlineInstallService.CreateDefault(privacyIdentity);
|
||||||
var consentStore = new InstallerPrivacyConsentStore();
|
var consentStore = new InstallerPrivacyConsentStore();
|
||||||
desktop.MainWindow = new MainWindow
|
var mainWindow = new MainWindow
|
||||||
{
|
{
|
||||||
DataContext = new MainWindowViewModel(installService, privacyIdentity, consentStore)
|
DataContext = new MainWindowViewModel(installService, privacyIdentity, consentStore)
|
||||||
};
|
};
|
||||||
|
desktop.MainWindow = mainWindow;
|
||||||
|
mainWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
<PackageVersion>$(Version)</PackageVersion>
|
<PackageVersion>$(Version)</PackageVersion>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<ApplicationIcon>Assets\logo.ico</ApplicationIcon>
|
<ApplicationIcon>Assets\logo.ico</ApplicationIcon>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest Condition="'$(Configuration)' == 'Debug'">app.Debug.manifest</ApplicationManifest>
|
||||||
|
<ApplicationManifest Condition="'$(Configuration)' != 'Debug'">app.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,5 +2,10 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace LanDesktopPLONDS.Installer.Services;
|
namespace LanDesktopPLONDS.Installer.Services;
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(
|
||||||
|
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
ReadCommentHandling = System.Text.Json.JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true)]
|
||||||
[JsonSerializable(typeof(InstallerPlondsManifest))]
|
[JsonSerializable(typeof(InstallerPlondsManifest))]
|
||||||
internal sealed partial class InstallerJsonContext : JsonSerializerContext;
|
internal sealed partial class InstallerJsonContext : JsonSerializerContext;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin
|
|||||||
{
|
{
|
||||||
private const string S3ManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_S3_MANIFEST_URL";
|
private const string S3ManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_S3_MANIFEST_URL";
|
||||||
private const string GitHubManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_GITHUB_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 const string DefaultGitHubManifestUrl = "https://github.com/wwiinnddyy/LanMountainDesktop/releases/latest/download/PLONDS.json";
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
@@ -78,6 +78,15 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin
|
|||||||
{
|
{
|
||||||
var version = ParseVersion(candidate.Manifest.CurrentVersion).ToString();
|
var version = ParseVersion(candidate.Manifest.CurrentVersion).ToString();
|
||||||
var packageRoot = Path.Combine(stagingRoot, SanitizePathSegment(version), SanitizePathSegment(candidate.Source.Id), "full");
|
var packageRoot = Path.Combine(stagingRoot, SanitizePathSegment(version), SanitizePathSegment(candidate.Source.Id), "full");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
if (Directory.Exists(packageRoot))
|
if (Directory.Exists(packageRoot))
|
||||||
{
|
{
|
||||||
Directory.Delete(packageRoot, recursive: true);
|
Directory.Delete(packageRoot, recursive: true);
|
||||||
@@ -87,9 +96,12 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin
|
|||||||
var zipPath = Path.Combine(packageRoot, "Files.zip");
|
var zipPath = Path.Combine(packageRoot, "Files.zip");
|
||||||
var extractDirectory = Path.Combine(packageRoot, "Files");
|
var extractDirectory = Path.Combine(packageRoot, "Files");
|
||||||
Directory.CreateDirectory(extractDirectory);
|
Directory.CreateDirectory(extractDirectory);
|
||||||
|
var attempt = candidate with { FilesZipUrl = filesZipUrl };
|
||||||
|
|
||||||
await DownloadToFileAsync(candidate, zipPath, progress, cancellationToken).ConfigureAwait(false);
|
try
|
||||||
await VerifyPackageAsync(zipPath, candidate.Manifest, candidate.FilesZipUrl, cancellationToken).ConfigureAwait(false);
|
{
|
||||||
|
await DownloadToFileAsync(attempt, zipPath, progress, cancellationToken).ConfigureAwait(false);
|
||||||
|
await VerifyPackageAsync(zipPath, attempt.Manifest, filesZipUrl, cancellationToken).ConfigureAwait(false);
|
||||||
ExtractZip(zipPath, extractDirectory);
|
ExtractZip(zipPath, extractDirectory);
|
||||||
|
|
||||||
progress?.Report(new InstallerDeployProgress(
|
progress?.Report(new InstallerDeployProgress(
|
||||||
@@ -103,6 +115,18 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin
|
|||||||
|
|
||||||
return new PreparedFilesPackage(version, candidate.Source.Id, zipPath, extractDirectory, candidate.Manifest);
|
return new PreparedFilesPackage(version, candidate.Source.Id, zipPath, extractDirectory, candidate.Manifest);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastError = ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Failed to download and prepare the PLONDS Files package.", lastError);
|
||||||
|
}
|
||||||
|
|
||||||
public static long EstimateInstallBytes(InstallerPlondsManifest manifest)
|
public static long EstimateInstallBytes(InstallerPlondsManifest manifest)
|
||||||
{
|
{
|
||||||
@@ -140,6 +164,8 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin
|
|||||||
var totalBytes = response.Content.Headers.ContentLength;
|
var totalBytes = response.Content.Headers.ContentLength;
|
||||||
var partialPath = $"{destinationPath}.partial";
|
var partialPath = $"{destinationPath}.partial";
|
||||||
long downloaded = 0;
|
long downloaded = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
await using (var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
|
await using (var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
|
||||||
await using (var target = File.Create(partialPath))
|
await using (var target = File.Create(partialPath))
|
||||||
{
|
{
|
||||||
@@ -168,6 +194,14 @@ internal sealed class InstallerPlondsClient(HttpClient httpClient, string stagin
|
|||||||
|
|
||||||
File.Move(partialPath, destinationPath, overwrite: true);
|
File.Move(partialPath, destinationPath, overwrite: true);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(partialPath))
|
||||||
|
{
|
||||||
|
File.Delete(partialPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task VerifyPackageAsync(
|
private static async Task VerifyPackageAsync(
|
||||||
string zipPath,
|
string zipPath,
|
||||||
|
|||||||
18
LanDesktopPLONDS.installer/app.Debug.manifest
Normal file
18
LanDesktopPLONDS.installer/app.Debug.manifest
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="0.0.0.0" name="LanDesktopPLONDS.Installer"/>
|
||||||
|
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
@@ -84,6 +84,45 @@ public sealed class OnlineInstallerCoreTests : IDisposable
|
|||||||
Assert.Contains(urls, uri => uri.AbsoluteUri == "https://github.test/Files.zip");
|
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]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
[InlineData("C:\\")]
|
[InlineData("C:\\")]
|
||||||
@@ -141,7 +180,43 @@ public sealed class OnlineInstallerCoreTests : IDisposable
|
|||||||
manifest,
|
manifest,
|
||||||
new Uri("https://s3.test/Files.zip"));
|
new Uri("https://s3.test/Files.zip"));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidDataException>(() => client.DownloadAndPrepareFullPackageAsync(candidate, null, CancellationToken.None));
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => client.DownloadAndPrepareFullPackageAsync(candidate, null, CancellationToken.None));
|
||||||
|
Assert.IsType<InvalidDataException>(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<string, string>
|
||||||
|
{
|
||||||
|
["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(
|
private static InstallerPlondsManifest CreateManifest(
|
||||||
@@ -205,4 +280,33 @@ public sealed class OnlineInstallerCoreTests : IDisposable
|
|||||||
return Task.FromResult(response);
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class FallbackPackageHandler(string zipPath) : HttpMessageHandler
|
||||||
|
{
|
||||||
|
protected override Task<HttpResponseMessage> 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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(manifestJson)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ internal static class PlondsClientServiceFactory
|
|||||||
{
|
{
|
||||||
private const string S3ManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_S3_MANIFEST_URL";
|
private const string S3ManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_S3_MANIFEST_URL";
|
||||||
private const string GitHubManifestUrlEnvironmentVariable = "LANMOUNTAIN_PLONDS_GITHUB_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 const string DefaultGitHubManifestUrl = "https://github.com/wwiinnddyy/LanMountainDesktop/releases/latest/download/PLONDS.json";
|
||||||
|
|
||||||
public static IPlondsService CreateDefault(HttpClient? httpClient = null)
|
public static IPlondsService CreateDefault(HttpClient? httpClient = null)
|
||||||
|
|||||||
Reference in New Issue
Block a user