diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index a071a6db..0c486a6f 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -53,18 +53,33 @@ namespace GeneralUpdate.Core; /// components, network policies, and OS strategy. /// /// +/// Standard flow (Client): /// /// var result = await new GeneralUpdateBootstrap() -/// .SetConfig(new UpdateRequest { -/// UpdateUrl = "https://api.example.com", -/// ClientVersion = "1.0.0", -/// InstallPath = @"C:\MyApp", -/// AppSecretKey = "my-key" -/// }) +/// .SetSource("https://api.example.com", "my-key") /// .SetOption(Option.AppType, AppType.Client) /// .Hooks<MyCustomHooks>() /// .LaunchAsync(); /// +/// OSS flow (Object Storage Service): +/// +/// // OssClient — only secrets in code; identity from generalupdate.manifest.json +/// var result = await new GeneralUpdateBootstrap() +/// .SetSource("https://oss.example.com/versions.json", "my-key") +/// .SetOption(Option.AppType, AppType.OssClient) +/// .Hooks<MyCustomHooks>() +/// .LaunchAsync(); +/// +/// // OssUpgrade — running from a subdirectory; pass installPath to locate +/// // generalupdate.manifest.json in the parent directory +/// var installPath = Path.GetFullPath(Path.Combine(baseDir, "..")); +/// var result = await new GeneralUpdateBootstrap() +/// .SetSource("https://oss.example.com/versions.json", "my-key", +/// installPath: installPath) +/// .SetOption(Option.AppType, AppType.OssUpgrade) +/// .Hooks<MyCustomHooks>() +/// .LaunchAsync(); +/// /// public class GeneralUpdateBootstrap : AbstractBootstrap { @@ -232,15 +247,29 @@ public GeneralUpdateBootstrap SetConfig(string filePath) /// /// Zero-config entry-point: supply only secrets. Provides sensible identity defaults /// that pass ; the real identity metadata is - /// discovered later by from generalupdate.manifest.json. + /// discovered later by or + /// from generalupdate.manifest.json. Works with all + /// modes (Client, Upgrade, OssClient, OssUpgrade). /// + /// The remote endpoint for update validation or version config retrieval. + /// The application secret key used for authentication. + /// Optional URL for reporting update status back to the server. + /// Optional URL scheme override (e.g. "https"). + /// Optional authentication token for API requests. + /// + /// Optional override for the installation root directory. When null, + /// defaults to . Set this when + /// the current process runs from a subdirectory (e.g. the OSS upgrade process in + /// update/) and generalupdate.manifest.json lives in the parent. + /// /// This bootstrap instance for chaining. public GeneralUpdateBootstrap SetSource( string updateUrl, string appSecretKey, string? reportUrl = null, string? scheme = null, - string? token = null) + string? token = null, + string? installPath = null) { var config = new UpdateRequest { @@ -250,6 +279,8 @@ public GeneralUpdateBootstrap SetSource( Scheme = scheme, ReportUrl = reportUrl }; + if (installPath != null) + config.InstallPath = installPath; return SetConfig(config); } diff --git a/src/c#/GeneralUpdate.Core/Configuration/UpdateRequest.cs b/src/c#/GeneralUpdate.Core/Configuration/UpdateRequest.cs index d3a675ac..b3f9ea30 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/UpdateRequest.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/UpdateRequest.cs @@ -62,17 +62,8 @@ public void Validate() if (!string.IsNullOrWhiteSpace(UpdateLogUrl) && !Uri.IsWellFormedUriString(UpdateLogUrl, UriKind.Absolute)) throw new ArgumentException("Invalid UpdateLogUrl"); - if (string.IsNullOrWhiteSpace(UpdateAppName)) - throw new ArgumentException("UpdateAppName cannot be empty"); - - if (string.IsNullOrWhiteSpace(MainAppName)) - throw new ArgumentException("MainAppName cannot be empty"); - if (string.IsNullOrWhiteSpace(AppSecretKey)) throw new ArgumentException("AppSecretKey cannot be empty"); - - if (string.IsNullOrWhiteSpace(ClientVersion)) - throw new ArgumentException("ClientVersion cannot be empty"); } } } diff --git a/src/c#/GeneralUpdate.Core/Strategy/OssStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OssStrategy.cs index 0d6d31db..65270a2b 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OssStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OssStrategy.cs @@ -150,7 +150,12 @@ public async Task ExecuteAsync() if (_configInfo == null) throw new InvalidOperationException("OssStrategy not configured. Call Create() first."); - // Dispatch by role �?no env-var detection needed. + // Fill missing identity fields from generalupdate.manifest.json, + // making the manifest the single source of configuration across + // all update flows (standard ClientStrategy and OssStrategy). + Configuration.AppMetadataDiscoverer.Discover(_configInfo); + + // Dispatch by role — no env-var detection needed. if (_role == AppType.OssUpgrade) { await ExecuteUpgradeAsync(); @@ -189,35 +194,63 @@ private async Task ExecuteClientAsync() var versionFileName = $"{_configInfo.MainAppName ?? _configInfo.UpdateAppName}_versions.json"; var versionsFilePath = Path.Combine(installPath, versionFileName); + GeneralTracer.Info($"[OssClient] InstallPath={installPath}"); + GeneralTracer.Info($"[OssClient] VersionFileName={versionFileName}"); + GeneralTracer.Info($"[OssClient] ClientVersion={_configInfo.ClientVersion}"); + GeneralTracer.Info($"[OssClient] MainAppName={_configInfo.MainAppName}"); + GeneralTracer.Info($"[OssClient] UpdateAppName={_configInfo.UpdateAppName}"); + if (!string.IsNullOrEmpty(_configInfo.UpdateUrl)) { + GeneralTracer.Info($"[OssClient] Downloading from {_configInfo.UpdateUrl} ..."); await DownloadVersionConfig(_configInfo.UpdateUrl, versionsFilePath).ConfigureAwait(false); + GeneralTracer.Info($"[OssClient] Downloaded -> {versionsFilePath}"); } if (!File.Exists(versionsFilePath)) { - GeneralTracer.Info("OssStrategy: version config download failed, aborting."); + GeneralTracer.Info("[OssClient] FAIL: version config file not found after download!"); return; } - var versions = JsonSerializer.Deserialize( - File.ReadAllText(versionsFilePath), - JsonContext.OssVersionRecordJsonContext.Default.ListOssVersionRecord); + var jsonText = File.ReadAllText(versionsFilePath); + GeneralTracer.Info($"[OssClient] JSON downloaded ({jsonText.Length} chars)"); + + List versions; + try + { + versions = JsonSerializer.Deserialize( + jsonText, + JsonContext.OssVersionRecordJsonContext.Default.ListOssVersionRecord); + } + catch (Exception ex) + { + GeneralTracer.Error($"[OssClient] JSON deserialize error: {ex.GetType().Name}: {ex.Message}", ex); + throw; + } + if (versions == null || versions.Count == 0) { - GeneralTracer.Info("OssStrategy: no versions found, aborting."); + GeneralTracer.Info($"[OssClient] FAIL: no versions found (count={versions?.Count.ToString() ?? "null"})"); return; } + GeneralTracer.Info($"[OssClient] Deserialized {versions.Count} version(s)"); + foreach (var v in versions) + GeneralTracer.Info($" - {v.PacketName} v{v.Version} PubTime={v.PubTime:O}"); + versions = versions.OrderByDescending(x => x.PubTime).ToList(); var latest = versions.First(); + GeneralTracer.Info($"[OssClient] Latest: {latest.Version} (PubTime={latest.PubTime:O})"); if (!IsOssUpgrade(_configInfo.ClientVersion, latest.Version)) { - GeneralTracer.Info("OssStrategy: no upgrade needed."); + GeneralTracer.Info($"[OssClient] No upgrade needed: {_configInfo.ClientVersion} >= {latest.Version}"); return; } + GeneralTracer.Info($"[OssClient] Upgrade needed: {_configInfo.ClientVersion} -> {latest.Version}"); + // Resolve upgrade exe: prefer UpdatePath, fall back to InstallPath var upgradeDir = !string.IsNullOrWhiteSpace(_configInfo.UpdatePath) ? (Path.IsPathRooted(_configInfo.UpdatePath) @@ -228,10 +261,28 @@ private async Task ExecuteClientAsync() ? _configInfo.UpdateAppName : "GeneralUpdate.Upgrade.exe"; var appPath = Path.Combine(upgradeDir, upgradeAppName); + GeneralTracer.Info($"[OssClient] Resolved upgrade path: {appPath}"); + + // List exe files in the directory to help diagnose missing file issues + try + { + var dirFiles = Directory.GetFiles(upgradeDir, "*.exe").Select(f => Path.GetFileName(f)); + GeneralTracer.Info($"[OssClient] *.exe files in {upgradeDir}: [{string.Join(", ", dirFiles)}]"); + } + catch (Exception ex) + { + GeneralTracer.Warn($"[OssClient] Could not list directory {upgradeDir}: {ex.Message}"); + } + if (!File.Exists(appPath)) + { + GeneralTracer.Error($"[OssClient] FAIL: Upgrade app NOT FOUND at {appPath}"); throw new FileNotFoundException($"Upgrade application not found: {appPath}"); + } + GeneralTracer.Info($"[OssClient] Launching upgrade: {appPath}"); Process.Start(appPath); + GeneralTracer.Info("[OssClient] Upgrade launched, exiting."); await GracefulExit.CurrentProcessAsync().ConfigureAwait(false); } @@ -273,15 +324,6 @@ private async Task ExecuteUpgradeAsync() if (!File.Exists(jsonPath) && DownloadSource == null) throw new FileNotFoundException($"Version config not found: {jsonPath}"); - // Hooks: allow cancellation before download - if (!await SafeOnBeforeUpdateAsync(ctx).ConfigureAwait(false)) - { - GeneralTracer.Info("OssStrategy (upgrade): cancelled by hook."); - return; - } - - await SafeReportUpdateStartedAsync(ctx).ConfigureAwait(false); - // Build download assets from version config or injected source List assets; if (DownloadSource != null) @@ -314,6 +356,25 @@ private async Task ExecuteUpgradeAsync() if (assets.Count == 0) throw new InvalidOperationException("No assets to download."); + // Compute LastVersion deterministically via Version comparison + // so hooks see the correct TargetVersion regardless of source ordering. + _configInfo.LastVersion = assets + .Select(a => new Version(a.Version)) + .Max()! + .ToString(); + ctx = BuildUpdateContext(); + + // Hooks: allow cancellation before download. Called after assets are + // built and LastVersion is set so OnBeforeUpdateAsync sees the real + // TargetVersion for decision-making. + if (!await SafeOnBeforeUpdateAsync(ctx).ConfigureAwait(false)) + { + GeneralTracer.Info("OssStrategy (upgrade): cancelled by hook."); + return; + } + + await SafeReportUpdateStartedAsync(ctx).ConfigureAwait(false); + GeneralTracer.Debug($"OssStrategy (upgrade): downloading {assets.Count} asset(s)."); await DownloadAssetsAsync(assets, installPath).ConfigureAwait(false); @@ -321,6 +382,12 @@ private async Task ExecuteUpgradeAsync() var encoding = Encoding.GetEncoding(_configInfo?.Encoding?.CodePage ?? Encoding.UTF8.CodePage); DecompressAssets(assets, installPath, encoding); + // Update generalupdate.manifest.json ClientVersion so the client + // reads the correct version on next startup, preventing infinite loops. + Configuration.ManifestInfo.TryUpdateVersion( + installPath, + clientVersion: _configInfo.LastVersion); + await SafeOnDownloadCompletedAsync(ctx).ConfigureAwait(false); await SafeOnAfterUpdateAsync(ctx).ConfigureAwait(false); await SafeReportUpdateAppliedAsync(ctx).ConfigureAwait(false); diff --git a/tests/ClientTest/Program.cs b/tests/ClientTest/Program.cs index a68ec96f..e0e9668c 100644 --- a/tests/ClientTest/Program.cs +++ b/tests/ClientTest/Program.cs @@ -6,6 +6,68 @@ using GeneralUpdate.Core.Hooks; try +{ + await RunOssClientAsync(); + /*var isOssMode = args.Length > 0 && args[0] == "--oss"; + + if (isOssMode) + { + await RunOssClientAsync(); + } + else + { + await RunStandardClientAsync(); + }*/ +} +catch (Exception ex) +{ + Console.WriteLine($"FATAL: {ex}"); + Console.WriteLine("Press Enter to exit..."); + Console.ReadLine(); + Environment.Exit(1); +} + +Console.WriteLine("Press Enter to exit..."); +Console.ReadLine(); + +// ═══════════════════════════════════════════════════════════════════ +// OSS Client mode — version JSON download → version compare → launch upgrade +// ═══════════════════════════════════════════════════════════════════ +static async Task RunOssClientAsync() +{ + Console.WriteLine("=== GeneralUpdate OSS Client Test ==="); + Console.WriteLine($"Started at {DateTime.Now}"); + Console.WriteLine($"Running from: {AppDomain.CurrentDomain.BaseDirectory}"); + + // Only secrets are supplied in code. Identity fields (MainAppName, + // ClientVersion, UpdateAppName, UpdatePath) are read from + // generalupdate.manifest.json by OssStrategy via + // AppMetadataDiscoverer.Discover() — same as the standard flow. + var updateUrl = "http://localhost:5000/packages/versions.json"; + var appSecretKey = "dfeb5833-975e-4afb-88f1-6278ee9aeff6"; + + Console.WriteLine($"UpdateUrl: {updateUrl}"); + Console.WriteLine(); + + await new GeneralUpdateBootstrap() + .SetSource(updateUrl, appSecretKey) + .SetOption(Option.AppType, AppType.OssClient) + .Hooks() + .AddListenerMultiDownloadStatistics(OnDownloadStatistics) + .AddListenerMultiDownloadCompleted(OnDownloadCompleted) + .AddListenerMultiAllDownloadCompleted(OnAllDownloadCompleted) + .AddListenerMultiDownloadError(OnDownloadError) + .AddListenerException(OnException) + .AddListenerUpdateInfo(OnUpdateInfo) + .LaunchAsync(); + + Console.WriteLine("OSS Client test completed."); +} + +// ═══════════════════════════════════════════════════════════════════ +// Standard Client mode — silent poll with IPC handoff to Upgrade +// ═══════════════════════════════════════════════════════════════════ +static async Task RunStandardClientAsync() { Console.WriteLine("=== GeneralUpdate Client Test (Silent Mode) ==="); Console.WriteLine($"Started at {DateTime.Now}"); @@ -47,14 +109,12 @@ Console.WriteLine(); // Keep the process alive so the background poll loop can work. - // When the user presses Ctrl+C or Enter, the process exits and - // ProcessExit fires, which triggers the upgrade launch. var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { Console.WriteLine(); Console.WriteLine("[Shutdown] Ctrl+C pressed. Exiting..."); - e.Cancel = true; // Prevent immediate kill — let ProcessExit fire + e.Cancel = true; cts.Cancel(); }; @@ -67,10 +127,6 @@ // Expected on Ctrl+C — graceful shutdown } - // Explicitly launch the upgrade process before exiting. - // ProcessExit may not fire reliably in all scenarios (e.g. console Ctrl+C), - // so we call TryLaunchUpgrade() directly as the primary launch path. - // If ProcessExit also fires later, the _updaterStarted guard prevents a double-launch. Console.WriteLine("[Shutdown] Launching upgrade process..."); if (orchestrator != null && orchestrator.HasPreparedUpdate) { @@ -86,11 +142,10 @@ Console.WriteLine("[Shutdown] Client test exiting gracefully."); } -catch (Exception ex) -{ - Console.WriteLine($"FATAL: {ex}"); - Environment.Exit(1); -} + +// ═══════════════════════════════════════════════════════════════════ +// Event handlers (shared across both modes) +// ═══════════════════════════════════════════════════════════════════ static void OnDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs e) { @@ -136,6 +191,10 @@ static void OnUpdateInfo(object sender, UpdateInfoEventArgs e) } } +// ═══════════════════════════════════════════════════════════════════ +// Hooks (shared across both modes) +// ═══════════════════════════════════════════════════════════════════ + sealed class ClientTestHooks : IUpdateHooks { public async Task OnBeforeUpdateAsync(HookContext ctx) diff --git a/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs b/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs index cad3684a..2626e4c5 100644 --- a/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs +++ b/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs @@ -190,8 +190,9 @@ public void Configinfo_Validate_MissingUpdateUrl_Throws() } [Fact] - public void Configinfo_Validate_MissingMainAppName_Throws() + public void Configinfo_Validate_MissingMainAppName_DoesNotThrow() { + // MainAppName is optional — filled from manifest by strategies. var config = new UpdateRequest { UpdateUrl = "https://api.example.com", @@ -200,12 +201,14 @@ public void Configinfo_Validate_MissingMainAppName_Throws() MainAppName = null! }; - Assert.Throws(() => config.Validate()); + var ex = Record.Exception(() => config.Validate()); + Assert.Null(ex); } [Fact] - public void Configinfo_Validate_MissingClientVersion_Throws() + public void Configinfo_Validate_MissingClientVersion_DoesNotThrow() { + // ClientVersion is optional — filled from manifest by strategies. var config = new UpdateRequest { UpdateUrl = "https://api.example.com", @@ -214,7 +217,8 @@ public void Configinfo_Validate_MissingClientVersion_Throws() ClientVersion = null! }; - Assert.Throws(() => config.Validate()); + var ex = Record.Exception(() => config.Validate()); + Assert.Null(ex); } [Theory] diff --git a/tests/CoreTest/Configuration/UpdateRequestTests.cs b/tests/CoreTest/Configuration/UpdateRequestTests.cs index bc0c14c2..c11a784c 100644 --- a/tests/CoreTest/Configuration/UpdateRequestTests.cs +++ b/tests/CoreTest/Configuration/UpdateRequestTests.cs @@ -95,8 +95,9 @@ public void Validate_UpdateLogUrlInvalid_ThrowsArgumentException() [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void Validate_UpgradeAppNameNullOrWhitespace_ThrowsArgumentException(string appName) + public void Validate_UpgradeAppNameNullOrWhitespace_NoException(string appName) { + // UpdateAppName is optional — filled from manifest by strategies. var config = new UpdateRequest { UpdateUrl = "https://api.example.com", @@ -106,15 +107,17 @@ public void Validate_UpgradeAppNameNullOrWhitespace_ThrowsArgumentException(stri ClientVersion = "1.0.0", InstallPath = "C:\\app" }; - Assert.Throws(() => config.Validate()); + var ex = Record.Exception(() => config.Validate()); + Assert.Null(ex); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void Validate_MainAppNameNullOrWhitespace_ThrowsArgumentException(string mainUpgradeAppName) + public void Validate_MainAppNameNullOrWhitespace_NoException(string mainUpgradeAppName) { + // MainAppName is optional — filled from manifest by strategies. var config = new UpdateRequest { UpdateUrl = "https://api.example.com", @@ -124,7 +127,8 @@ public void Validate_MainAppNameNullOrWhitespace_ThrowsArgumentException(string ClientVersion = "1.0.0", InstallPath = "C:\\app" }; - Assert.Throws(() => config.Validate()); + var ex = Record.Exception(() => config.Validate()); + Assert.Null(ex); } [Theory] @@ -149,8 +153,9 @@ public void Validate_AppSecretKeyNullOrWhitespace_ThrowsArgumentException(string [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void Validate_ClientVersionNullOrWhitespace_ThrowsArgumentException(string clientVersion) + public void Validate_ClientVersionNullOrWhitespace_NoException(string clientVersion) { + // ClientVersion is optional — filled from manifest by strategies. var config = new UpdateRequest { UpdateUrl = "https://api.example.com", @@ -160,7 +165,8 @@ public void Validate_ClientVersionNullOrWhitespace_ThrowsArgumentException(strin ClientVersion = clientVersion, InstallPath = "C:\\app" }; - Assert.Throws(() => config.Validate()); + var ex = Record.Exception(() => config.Validate()); + Assert.Null(ex); } [Theory] diff --git a/tests/UpgradeTest/Program.cs b/tests/UpgradeTest/Program.cs index 2f510234..ef728de2 100644 --- a/tests/UpgradeTest/Program.cs +++ b/tests/UpgradeTest/Program.cs @@ -5,6 +5,70 @@ using GeneralUpdate.Core.Hooks; try +{ + await RunOssUpgradeAsync(); + /*var isOssMode = args.Length > 0 && args[0] == "--oss"; + + if (isOssMode) + { + await RunOssUpgradeAsync(); + } + else + { + await RunStandardUpgradeAsync(); + }*/ +} +catch (Exception ex) +{ + Console.WriteLine($"FATAL: {ex}"); + Environment.Exit(1); +} + +// ═══════════════════════════════════════════════════════════════════ +// OSS Upgrade mode — read versions.json → download packages → decompress → launch main app +// ═══════════════════════════════════════════════════════════════════ +static async Task RunOssUpgradeAsync() +{ + Console.WriteLine("=== GeneralUpdate OSS Upgrade Test ==="); + Console.WriteLine($"Started at {DateTime.Now}"); + Console.WriteLine($"Running from: {AppDomain.CurrentDomain.BaseDirectory}"); + + // OssUpgrade runs from a subdirectory (e.g. update/), but the manifest + // and versions.json are in the parent directory (same as OssClient). + // InstallPath resolves to the parent so OssStrategy reads the correct + // manifest and versions.json. + var baseDir = AppDomain.CurrentDomain.BaseDirectory; + var installPath = Path.GetFullPath(Path.Combine(baseDir, "..")); + + Console.WriteLine($"BaseDir={baseDir}"); + Console.WriteLine($"InstallPath={installPath} (parent, where manifest + versions.json live)"); + Console.WriteLine(); + + // Only secrets and InstallPath are supplied in code. Identity fields + // (MainAppName, ClientVersion, UpdateAppName) are read from + // generalupdate.manifest.json by OssStrategy via + // AppMetadataDiscoverer.Discover() — same as the standard OSS client flow. + await new GeneralUpdateBootstrap() + .SetSource( + "http://localhost:5000/packages/versions.json", + "dfeb5833-975e-4afb-88f1-6278ee9aeff6", + installPath: installPath) + .SetOption(Option.AppType, AppType.OssUpgrade) + .Hooks() + .AddListenerMultiDownloadStatistics(OnDownloadStatistics) + .AddListenerMultiDownloadCompleted(OnDownloadCompleted) + .AddListenerMultiAllDownloadCompleted(OnAllDownloadCompleted) + .AddListenerMultiDownloadError(OnDownloadError) + .AddListenerException(OnException) + .LaunchAsync(); + + Console.WriteLine("OSS Upgrade test completed."); +} + +// ═══════════════════════════════════════════════════════════════════ +// Standard Upgrade mode — read IPC config → apply patches → launch main app +// ═══════════════════════════════════════════════════════════════════ +static async Task RunStandardUpgradeAsync() { Console.WriteLine("=== GeneralUpdate Upgrade Test ==="); Console.WriteLine($"Started at {DateTime.Now}"); @@ -26,11 +90,10 @@ Console.WriteLine("Upgrade test completed."); } -catch (Exception ex) -{ - Console.WriteLine($"FATAL: {ex}"); - Environment.Exit(1); -} + +// ═══════════════════════════════════════════════════════════════════ +// Event handlers (shared across both modes) +// ═══════════════════════════════════════════════════════════════════ static void OnDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs e) { @@ -62,6 +125,10 @@ static void OnException(object sender, ExceptionEventArgs e) Console.WriteLine($"[Error] {e.Exception}"); } +// ═══════════════════════════════════════════════════════════════════ +// Hooks (shared across both modes) +// ═══════════════════════════════════════════════════════════════════ + sealed class UpgradeTestHooks : IUpdateHooks { public async Task OnBeforeUpdateAsync(HookContext ctx) @@ -79,6 +146,8 @@ public async Task OnDownloadCompletedAsync(DownloadContext ctx) public async Task OnAfterUpdateAsync(HookContext ctx) { Console.WriteLine($"[Hook] OnAfterUpdate: {ctx.CurrentVersion} -> {ctx.TargetVersion}"); + // Manifest ClientVersion is updated by OssStrategy itself via + // ManifestInfo.TryUpdateVersion() after decompression. await Task.CompletedTask; }