mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Add update contracts, IPC progress & providers
Introduce a new update subsystem: shared contracts for manifests, messages, paths and state (LanMountainDesktop.Shared.Contracts.Update). Add IPC and reporting infrastructure for installer progress (IUpdateProgressReporter, LauncherUpdateProgressIpcServer, NullUpdateProgressReporter) and integrate progress/complete reporting into UpdateEngineService. Add multiple update service components and providers (CompositeManifestProvider, GithubReleaseManifestProvider, PlondsApiManifestProvider, UpdateDownloadEngine, UpdateOrchestrator, UpdateInstallGateway, CLI launcher bridge, launcher bridge interfaces, observable helper, state store, progress subject, JSON context). Update settings and models to support UseGhProxyMirror and PLONDS/GitHub fallback logic, plus localization strings and UI/viewmodel files for update settings and progress. Misc: installer script tweak and a small change in Plonds generator. This adds end-to-end support for checking, downloading and reporting update progress and results.
This commit is contained in:
86
LanMountainDesktop.Shared.Contracts/Update/UpdateManifest.cs
Normal file
86
LanMountainDesktop.Shared.Contracts/Update/UpdateManifest.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
namespace LanMountainDesktop.Shared.Contracts.Update;
|
||||
|
||||
public sealed record UpdateManifest(
|
||||
string DistributionId,
|
||||
string FromVersion,
|
||||
string ToVersion,
|
||||
string Platform,
|
||||
string Channel,
|
||||
DateTimeOffset PublishedAt,
|
||||
UpdatePayloadKind Kind,
|
||||
string? FileMapUrl,
|
||||
string? FileMapSignatureUrl,
|
||||
string? FileMapSha256,
|
||||
IReadOnlyList<UpdateFileEntry> Files,
|
||||
IReadOnlyList<UpdateMirrorAsset>? InstallerMirrors,
|
||||
IReadOnlyDictionary<string, string> Metadata)
|
||||
{
|
||||
public bool IsDelta => Kind is UpdatePayloadKind.DeltaPlonds or UpdatePayloadKind.DeltaLegacy;
|
||||
|
||||
public long EstimatedDeltaBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = 0;
|
||||
foreach (var f in Files)
|
||||
{
|
||||
if (f.Action is not ("reuse" or "delete"))
|
||||
{
|
||||
total += f.Size;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record UpdateFileEntry(
|
||||
string Path,
|
||||
string Action,
|
||||
string Sha256,
|
||||
long Size,
|
||||
string Mode,
|
||||
string? ObjectKey,
|
||||
string? ObjectUrl,
|
||||
string? ArchiveSha256,
|
||||
IReadOnlyDictionary<string, string>? Metadata);
|
||||
|
||||
public sealed record UpdateMirrorAsset(
|
||||
string Platform,
|
||||
string? Url,
|
||||
string? Name,
|
||||
string? Sha256,
|
||||
long Size);
|
||||
|
||||
public sealed record UpdateSettingsState(
|
||||
string UpdateChannel,
|
||||
string UpdateMode,
|
||||
string UpdateDownloadSource,
|
||||
int UpdateDownloadThreads,
|
||||
string? PreferredDistributionId,
|
||||
string? LastAppliedVersion,
|
||||
DateTimeOffset? LastAppliedAt,
|
||||
int ConsecutiveFailCount,
|
||||
DateTimeOffset? LastFailureAt,
|
||||
string? PendingUpdateInstallerPath,
|
||||
string? PendingUpdateVersion,
|
||||
long? PendingUpdatePublishedAtUtcMs,
|
||||
long? LastUpdateCheckUtcMs,
|
||||
string? PendingUpdateSha256)
|
||||
{
|
||||
public static UpdateSettingsState Default => new(
|
||||
UpdateChannel: "stable",
|
||||
UpdateMode: "download_then_confirm",
|
||||
UpdateDownloadSource: "plonds-api",
|
||||
UpdateDownloadThreads: 4,
|
||||
PreferredDistributionId: null,
|
||||
LastAppliedVersion: null,
|
||||
LastAppliedAt: null,
|
||||
ConsecutiveFailCount: 0,
|
||||
LastFailureAt: null,
|
||||
PendingUpdateInstallerPath: null,
|
||||
PendingUpdateVersion: null,
|
||||
PendingUpdatePublishedAtUtcMs: null,
|
||||
LastUpdateCheckUtcMs: null,
|
||||
PendingUpdateSha256: null);
|
||||
}
|
||||
66
LanMountainDesktop.Shared.Contracts/Update/UpdateMessages.cs
Normal file
66
LanMountainDesktop.Shared.Contracts/Update/UpdateMessages.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace LanMountainDesktop.Shared.Contracts.Update;
|
||||
|
||||
public sealed record InstallProgressReport(
|
||||
InstallStage Stage,
|
||||
string Message,
|
||||
int ProgressPercent,
|
||||
string? CurrentFile,
|
||||
int FilesCompleted,
|
||||
int FilesTotal)
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed record InstallCompleteReport(
|
||||
bool Success,
|
||||
string? FromVersion,
|
||||
string? ToVersion,
|
||||
string? ErrorMessage,
|
||||
bool WasRolledBack)
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed record DownloadProgressReport(
|
||||
string CurrentFile,
|
||||
long BytesDownloaded,
|
||||
long BytesTotal,
|
||||
double BytesPerSecond,
|
||||
int FilesCompleted,
|
||||
int FilesTotal,
|
||||
double OverallFraction)
|
||||
{
|
||||
public int OverallPercent => (int)Math.Clamp(OverallFraction * 100, 0, 100);
|
||||
}
|
||||
|
||||
public sealed record UpdateProgressReport(
|
||||
UpdatePhase Phase,
|
||||
string Message,
|
||||
double ProgressFraction,
|
||||
DownloadProgressReport? DownloadDetail,
|
||||
InstallProgressReport? InstallDetail)
|
||||
{
|
||||
public int ProgressPercent => (int)Math.Clamp(ProgressFraction * 100, 0, 100);
|
||||
}
|
||||
|
||||
public sealed record UpdateCheckReport(
|
||||
bool IsUpdateAvailable,
|
||||
string? LatestVersion,
|
||||
string? CurrentVersion,
|
||||
UpdatePayloadKind? PayloadKind,
|
||||
string? DistributionId,
|
||||
string? Channel,
|
||||
DateTimeOffset? PublishedAt,
|
||||
long? TotalDownloadBytes,
|
||||
long? FullInstallerBytes,
|
||||
string? ErrorMessage);
|
||||
|
||||
public sealed record InstallRequest(
|
||||
UpdatePayloadKind PayloadKind,
|
||||
string LauncherRoot,
|
||||
string? LaunchSource = null);
|
||||
|
||||
public sealed record LaunchResult(
|
||||
bool Success,
|
||||
string? ErrorMessage,
|
||||
int? ProcessId);
|
||||
71
LanMountainDesktop.Shared.Contracts/Update/UpdatePaths.cs
Normal file
71
LanMountainDesktop.Shared.Contracts/Update/UpdatePaths.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
namespace LanMountainDesktop.Shared.Contracts.Update;
|
||||
|
||||
public static class UpdatePaths
|
||||
{
|
||||
private const string LauncherDirectoryName = ".launcher";
|
||||
private const string UpdateDirectoryName = "update";
|
||||
private const string IncomingDirectoryName = "incoming";
|
||||
private const string ObjectsDirectoryName = "objects";
|
||||
private const string SnapshotsDirectoryName = "snapshots";
|
||||
|
||||
public static string ResolveLauncherRoot(string appBaseDirectory)
|
||||
{
|
||||
var trimmed = appBaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
var parent = Path.GetDirectoryName(trimmed);
|
||||
return string.IsNullOrWhiteSpace(parent) ? appBaseDirectory : parent;
|
||||
}
|
||||
|
||||
public static string GetLauncherDataRoot(string launcherRoot)
|
||||
{
|
||||
return Path.Combine(launcherRoot, LauncherDirectoryName);
|
||||
}
|
||||
|
||||
public static string GetIncomingDirectory(string launcherRoot)
|
||||
{
|
||||
return Path.Combine(launcherRoot, LauncherDirectoryName, UpdateDirectoryName, IncomingDirectoryName);
|
||||
}
|
||||
|
||||
public static string GetObjectsDirectory(string launcherRoot)
|
||||
{
|
||||
return Path.Combine(GetIncomingDirectory(launcherRoot), ObjectsDirectoryName);
|
||||
}
|
||||
|
||||
public static string GetSnapshotsDirectory(string launcherRoot)
|
||||
{
|
||||
return Path.Combine(launcherRoot, LauncherDirectoryName, SnapshotsDirectoryName);
|
||||
}
|
||||
|
||||
public static string GetDownloadMarkerPath(string launcherRoot)
|
||||
{
|
||||
return Path.Combine(GetIncomingDirectory(launcherRoot), ".download-complete");
|
||||
}
|
||||
|
||||
public static string GetPlondsFileMapName() => "plonds-filemap.json";
|
||||
public static string GetPlondsSignatureName() => "plonds-filemap.sig";
|
||||
public static string GetPlondsUpdateMetadataName() => "plonds-update.json";
|
||||
public static string GetLegacyFileMapName() => "files.json";
|
||||
public static string GetLegacySignatureName() => "files.json.sig";
|
||||
public static string GetLegacyArchiveName() => "update.zip";
|
||||
public static string GetPublicKeyFileName() => "public-key.pem";
|
||||
|
||||
public static string GetPlondsFileMapPath(string launcherRoot)
|
||||
=> Path.Combine(GetIncomingDirectory(launcherRoot), GetPlondsFileMapName());
|
||||
|
||||
public static string GetPlondsSignaturePath(string launcherRoot)
|
||||
=> Path.Combine(GetIncomingDirectory(launcherRoot), GetPlondsSignatureName());
|
||||
|
||||
public static string GetPlondsUpdateMetadataPath(string launcherRoot)
|
||||
=> Path.Combine(GetIncomingDirectory(launcherRoot), GetPlondsUpdateMetadataName());
|
||||
|
||||
public static string GetDownloadMarkerContent(string manifestSha256, string targetVersion, int objectCount)
|
||||
{
|
||||
return $$"""
|
||||
{
|
||||
"manifestSha256": "{{manifestSha256}}",
|
||||
"targetVersion": "{{targetVersion}}",
|
||||
"objectCount": {{objectCount}},
|
||||
"completedAt": "{{DateTimeOffset.UtcNow:O}}"
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
83
LanMountainDesktop.Shared.Contracts/Update/UpdateState.cs
Normal file
83
LanMountainDesktop.Shared.Contracts/Update/UpdateState.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
namespace LanMountainDesktop.Shared.Contracts.Update;
|
||||
|
||||
public enum UpdatePhase
|
||||
{
|
||||
Idle,
|
||||
Checking,
|
||||
Checked,
|
||||
Downloading,
|
||||
Downloaded,
|
||||
Installing,
|
||||
Installed,
|
||||
Verifying,
|
||||
Completed,
|
||||
Failed,
|
||||
Recovering,
|
||||
RollingBack,
|
||||
RolledBack
|
||||
}
|
||||
|
||||
public enum UpdatePayloadKind
|
||||
{
|
||||
DeltaPlonds,
|
||||
DeltaLegacy,
|
||||
FullInstaller
|
||||
}
|
||||
|
||||
public enum InstallStage
|
||||
{
|
||||
None,
|
||||
VerifySignature,
|
||||
CreateTarget,
|
||||
ApplyFiles,
|
||||
VerifyHashes,
|
||||
ActivateDeployment,
|
||||
Cleanup,
|
||||
Completed,
|
||||
Failed,
|
||||
RollingBack
|
||||
}
|
||||
|
||||
public enum UpdateChannel
|
||||
{
|
||||
Stable,
|
||||
Preview
|
||||
}
|
||||
|
||||
public enum UpdateMode
|
||||
{
|
||||
Manual,
|
||||
DownloadThenConfirm,
|
||||
SilentOnExit
|
||||
}
|
||||
|
||||
public enum UpdateDownloadSource
|
||||
{
|
||||
PlondsApi,
|
||||
GitHub,
|
||||
GhProxy
|
||||
}
|
||||
|
||||
public static class UpdatePhaseExtensions
|
||||
{
|
||||
public static bool IsTerminal(this UpdatePhase phase) =>
|
||||
phase is UpdatePhase.Completed or UpdatePhase.Failed or UpdatePhase.RolledBack;
|
||||
|
||||
public static bool IsBusy(this UpdatePhase phase) =>
|
||||
phase is not (UpdatePhase.Idle or UpdatePhase.Checked or UpdatePhase.Downloaded
|
||||
or UpdatePhase.Installed or UpdatePhase.Completed or UpdatePhase.Failed
|
||||
or UpdatePhase.RolledBack);
|
||||
|
||||
public static bool CanCheck(this UpdatePhase phase) =>
|
||||
phase is UpdatePhase.Idle or UpdatePhase.Checked or UpdatePhase.Downloaded
|
||||
or UpdatePhase.Completed or UpdatePhase.Failed or UpdatePhase.RolledBack;
|
||||
|
||||
public static bool CanDownload(this UpdatePhase phase) =>
|
||||
phase is UpdatePhase.Checked;
|
||||
|
||||
public static bool CanInstall(this UpdatePhase phase) =>
|
||||
phase is UpdatePhase.Downloaded;
|
||||
|
||||
public static bool CanRollback(this UpdatePhase phase) =>
|
||||
phase is UpdatePhase.Failed;
|
||||
}
|
||||
Reference in New Issue
Block a user