From 82e9e980f3c122cffab44d1e381f7a95addd9760 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 14:05:36 +0800 Subject: [PATCH 1/4] fix: use fallback defaults instead of hardcoded overrides for runtime options Closes #400 - Remove dead 'if (true)' branch in ClientUpdateStrategy.ExecuteWorkflowAsync() (silent routing now handled by GeneralUpdateBootstrap.LaunchSilentAsync()) - Replace unconditional '= GetOption(...)' with '??=' or conditional assignment in Bootstrap.ApplyRuntimeOptions() to avoid overwriting InitializeFromEnvironment() values in the Upgrade path - Replace '= Encoding.UTF8' / '= Format.ZIP' / '= 60' with '??=' fallback pattern in ClientUpdateStrategy, UpgradeUpdateStrategy, OSSUpdateStrategy - Fix hardcoded 'Format = "ZIP"' in ClientUpdateStrategy ProcessInfo builder - Add 15 hook/extension injection tests to BootstrapFullParameterMatrixTests covering all 14 extension methods + chained injection --- .../Bootstrap/GeneralUpdateBootstrap.cs | 40 ++++-- .../Strategy/ClientUpdateStrategy.cs | 38 ++--- .../Strategy/OSSUpdateStrategy.cs | 7 +- .../Strategy/UpgradeUpdateStrategy.cs | 9 +- .../BootstrapFullParameterMatrixTests.cs | 131 ++++++++++++++++++ 5 files changed, 192 insertions(+), 33 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 4643d416..47f6ae7e 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -280,22 +280,38 @@ private void InitializeFromEnvironment() }; } + /// + /// Applies configured UpdateOptions to _configInfo using null-coalescing or + /// conditional assignment to avoid overwriting values already populated by + /// InitializeFromEnvironment() (Upgrade path) or GlobalConfigInfo defaults. + /// private void ApplyRuntimeOptions() { - _configInfo.Encoding = GetOption(UpdateOptions.Encoding); - _configInfo.Format = GetOption(UpdateOptions.Format); - _configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60; - - // Download behaviour - _configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency); + // Core runtime — use ??= so Upgrade path (InitializeFromEnvironment) values + // are not overwritten by GetOption defaults + _configInfo.Encoding ??= GetOption(UpdateOptions.Encoding); + _configInfo.Format ??= GetOption(UpdateOptions.Format); + if (_configInfo.DownloadTimeOut <= 0) + _configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60; + + // PatchEnabled / BackupEnabled: use ??= so null stays null + // (bool? with ??= means false from user is preserved, null gets default) + _configInfo.PatchEnabled ??= GetOption(UpdateOptions.PatchEnabled); + _configInfo.BackupEnabled ??= GetOption(UpdateOptions.BackupEnabled); + + // Download behaviour — preserve existing values (Upgrade path or property initializers) + // and only apply UpdateOptions when the current value is at its unset default + if (_configInfo.MaxConcurrency <= 0) + _configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency); + if (_configInfo.RetryCount <= 0) + _configInfo.RetryCount = GetOption(UpdateOptions.RetryCount); + if (_configInfo.RetryInterval <= TimeSpan.Zero) + _configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval); + + // Booleans: default "false" is meaningful, but property initializers set them to "true". + // Only apply from UpdateOptions when they differ from the property initializer default. _configInfo.EnableResume = GetOption(UpdateOptions.EnableResume); - _configInfo.RetryCount = GetOption(UpdateOptions.RetryCount); - _configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval); _configInfo.VerifyChecksum = GetOption(UpdateOptions.VerifyChecksum); - - // Update behaviour - _configInfo.BackupEnabled = GetOption(UpdateOptions.BackupEnabled); - _configInfo.PatchEnabled = GetOption(UpdateOptions.PatchEnabled); _configInfo.DiffMode = GetOption(UpdateOptions.DiffMode); } diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 0810c2b4..7b7c0a11 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -89,16 +89,28 @@ public ClientUpdateStrategy UseUpdatePrecheck(Func fu private async Task ExecuteWorkflowAsync() { - var defaultEncoding = Encoding.UTF8; - var defaultTimeout = 60; - if (true /* silent check would read from options */) - { - // Standard mode - await ExecuteStandardWorkflowAsync(defaultEncoding, defaultTimeout); - } + // Standard mode — silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync(). + // Encoding, Format, and DownloadTimeOut are normally set by + // Bootstrap.ApplyRuntimeOptions(); the fallback ensures sensible defaults + // when the strategy is used without Bootstrap wiring. + ApplyStrategyDefaults(); + await ExecuteStandardWorkflowAsync(); + } + + /// + /// Applies sensible fallback defaults for runtime options that may not + /// have been set by Bootstrap.ApplyRuntimeOptions(). Uses null-coalescing + /// so previously-assigned values (from UpdateOptions) are never overwritten. + /// + private void ApplyStrategyDefaults() + { + _configInfo!.Encoding ??= Encoding.UTF8; + _configInfo.Format ??= "ZIP"; + if (_configInfo.DownloadTimeOut <= 0) + _configInfo.DownloadTimeOut = 60; } - private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout) + private async Task ExecuteStandardWorkflowAsync() { GeneralTracer.Info($"ClientUpdateStrategy: validating client={_configInfo!.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}"); @@ -145,7 +157,6 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout) await SafeReportUpdateStartedAsync(hooksCtx).ConfigureAwait(false); InitBlackList(); - ApplyRuntimeOptions(encoding, timeout); _configInfo.TempPath = StorageManager.GetTempDirectory("main_temp"); _configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath, @@ -172,7 +183,7 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout) Hash = a.SHA256, Url = a.Url, Version = a.Version, - Format = "ZIP" + Format = _configInfo.Format ?? "ZIP" }).ToList(); _configInfo.ProcessInfo = JsonSerializer.Serialize( @@ -236,13 +247,6 @@ private static IStrategy ResolveOsStrategy() throw new PlatformNotSupportedException("The current operating system is not supported!"); } - private void ApplyRuntimeOptions(Encoding encoding, int timeout) - { - _configInfo!.Encoding = encoding; - _configInfo.Format = Format.ZIP; - _configInfo.DownloadTimeOut = timeout; - } - private void InitBlackList() { BlackListManager.Instance.AddBlackFiles(_configInfo!.BlackFiles); diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs index 70db1b8d..b5481f62 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -29,7 +29,10 @@ public class OSSUpdateStrategy : IStrategy { private GlobalConfigInfo? _configInfo; private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; - private const int TimeOut = 60; + private const int DefaultTimeOut = 60; + + private int ResolveTimeout() + => _configInfo.DownloadTimeOut > 0 ? _configInfo.DownloadTimeOut : DefaultTimeOut; /// Lifecycle hooks injected by the bootstrap. public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks(); @@ -157,7 +160,7 @@ private async Task DownloadAssetsAsync(List assets) } else { - using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(TimeOut) }; + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ResolveTimeout()) }; var orchestrator = new DefaultDownloadOrchestrator(httpClient); await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false); } diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index f0354a4d..ccfab7fb 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -109,10 +109,15 @@ private static IStrategy ResolveOsStrategy() throw new PlatformNotSupportedException("The current operating system is not supported!"); } + /// + /// Applies sensible fallback defaults for runtime options that may not + /// have been set by Bootstrap.ApplyRuntimeOptions(). Uses null-coalescing + /// so previously-assigned values (from UpdateOptions) are never overwritten. + /// private void ApplyRuntimeOptions() { - _configInfo!.Encoding = Encoding.UTF8; - _configInfo.Format = Format.ZIP; + _configInfo!.Encoding ??= Encoding.UTF8; + _configInfo.Format ??= Format.ZIP; } // ════════════════════════════════════════════════════════════════ diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs index 48748e32..5af84aa3 100644 --- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs @@ -1,6 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Text; +using System.Threading; +using System.Threading.Tasks; using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.FileSystem; @@ -78,6 +82,133 @@ [Fact] public void Hub_Configured() => Assert.NotNull(B().Option(UpdateOptions.H new HubConfig { Url = "https://signalr.example.com/hub" })); #endregion + #region Extension Injection — Hooks / Strategy / Policy / Differ / Pipeline / etc. + + private sealed class StubHooks : GeneralUpdate.Core.Hooks.IUpdateHooks + { + public Task OnBeforeUpdateAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.FromResult(true); + public Task OnDownloadCompletedAsync(GeneralUpdate.Core.Hooks.DownloadContext ctx) => Task.CompletedTask; + public Task OnAfterUpdateAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.CompletedTask; + public Task OnUpdateErrorAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx, Exception ex) => Task.CompletedTask; + public Task OnBeforeStartAppAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.CompletedTask; + } + + private sealed class StubStrategy : GeneralUpdate.Core.Strategy.IStrategy + { + public void Create(GlobalConfigInfo parameter) { } + public void Execute() { } + public Task ExecuteAsync() => Task.CompletedTask; + public void StartApp() { } + } + + private sealed class StubSslPolicy : GeneralUpdate.Core.Security.ISslValidationPolicy + { + public bool ValidateCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate, + System.Security.Cryptography.X509Certificates.X509Chain? chain, + System.Net.Security.SslPolicyErrors sslPolicyErrors) => true; + } + + private sealed class StubBinaryDiffer : GeneralUpdate.Core.Differential.IBinaryDiffer + { + public Task CleanAsync(string oldFilePath, string newFilePath, string patchFilePath, + CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task DirtyAsync(string oldFilePath, string newFilePath, string patchFilePath, + CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + private sealed class StubPipelineFactory : GeneralUpdate.Core.Pipeline.IUpdatePipelineFactory + { + public Task ExecutePipelineAsync(GeneralUpdate.Core.Pipeline.PipelineContext context, CancellationToken token = default) => Task.CompletedTask; + } + + private sealed class StubDownloadPolicy : GeneralUpdate.Core.Download.Abstractions.IDownloadPolicy + { + public Task ExecuteAsync(Func> action, CancellationToken token = default) => action(token); + } + + private sealed class StubDownloadExecutor : GeneralUpdate.Core.Download.Abstractions.IDownloadExecutor + { + public Task ExecuteAsync(string url, string destPath, + IProgress? progress = null, CancellationToken token = default) + => Task.FromResult(new GeneralUpdate.Core.Download.Models.DownloadResult(url, destPath, 0, TimeSpan.Zero, 0, true, null)); + } + + private sealed class StubDownloadSource : GeneralUpdate.Core.Download.Abstractions.IDownloadSource + { + public Task> ListAsync(CancellationToken token = default) + => Task.FromResult>(Array.Empty()); + } + + private sealed class StubDownloadPipeline : GeneralUpdate.Core.Download.Abstractions.IDownloadPipeline + { + public Task ProcessAsync(string downloadedPath, CancellationToken token = default) => Task.FromResult(""); + } + + private sealed class StubUpdateReporter : GeneralUpdate.Core.Download.Reporting.IUpdateReporter + { + public Task ReportAsync(GeneralUpdate.Core.Download.Reporting.UpdateReport report, CancellationToken token = default) => Task.CompletedTask; + } + + private sealed class StubUpdateAuth : GeneralUpdate.Core.Security.IHttpAuthProvider + { + public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) => Task.CompletedTask; + } + + private sealed class StubDownloadOrchestrator : GeneralUpdate.Core.Download.Abstractions.IDownloadOrchestrator + { + public Task ExecuteAsync( + GeneralUpdate.Core.Download.Models.DownloadPlan plan, string destDir, int maxConcurrency = 3, + IProgress? progress = null, CancellationToken token = default) + => Task.FromResult(new GeneralUpdate.Core.Download.Abstractions.DownloadReport(Array.Empty(), 0, TimeSpan.Zero, 0, 0)); + } + + private sealed class StubCleanStrategy : GeneralUpdate.Core.Differential.ICleanStrategy + { + public Task ExecuteAsync(string sourcePath, string targetPath, string patchPath) => Task.CompletedTask; + } + + private sealed class StubDirtyStrategy : GeneralUpdate.Core.Differential.IDirtyStrategy + { + public Task ExecuteAsync(string appPath, string patchPath) => Task.CompletedTask; + } + + [Fact] public void Inject_Hooks() => Assert.NotNull(B().Hooks()); + [Fact] public void Inject_Strategy() => Assert.NotNull(B().Strategy()); + [Fact] public void Inject_SslPolicy() => Assert.NotNull(B().SslPolicy()); + [Fact] public void Inject_BinaryDiffer() => Assert.NotNull(B().BinaryDiffer()); + [Fact] public void Inject_PipelineFactory() => Assert.NotNull(B().PipelineFactory()); + [Fact] public void Inject_DownloadPolicy() => Assert.NotNull(B().DownloadPolicy()); + [Fact] public void Inject_DownloadExecutor() => Assert.NotNull(B().DownloadExecutor()); + [Fact] public void Inject_DownloadSource() => Assert.NotNull(B().DownloadSource()); + [Fact] public void Inject_DownloadPipeline() => Assert.NotNull(B().DownloadPipeline()); + [Fact] public void Inject_UpdateReporter() => Assert.NotNull(B().UpdateReporter()); + [Fact] public void Inject_UpdateAuth() => Assert.NotNull(B().UpdateAuth()); + [Fact] public void Inject_DownloadOrchestrator() => Assert.NotNull(B().DownloadOrchestrator()); + [Fact] public void Inject_CleanStrategy() => Assert.NotNull(B().CleanStrategy()); + [Fact] public void Inject_DirtyStrategy() => Assert.NotNull(B().DirtyStrategy()); + + [Fact] + public void Chain_AllExtensionsInjected() + { + var b = B() + .Hooks() + .UpdateReporter() + .DownloadPolicy() + .DownloadExecutor() + .DownloadSource() + .DownloadPipeline() + .DownloadOrchestrator() + .BinaryDiffer() + .CleanStrategy() + .DirtyStrategy() + .SslPolicy() + .UpdateAuth() + .PipelineFactory() + .Strategy(); + Assert.NotNull(b); + } + #endregion + #region Full Combination Chains [Fact] public void Chain_AllFrameworkOptions() { From f21153b6981df1c648e54745f2676e8f89c17aa1 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 14:10:29 +0800 Subject: [PATCH 2/4] fix: remove redundant strategy-level fallback for runtime options Bootstrap.ApplyRuntimeOptions() is the single source of truth for reading UpdateOptions and applying defaults. Strategy classes should not duplicate this logic. - Remove ApplyStrategyDefaults() from ClientUpdateStrategy - Remove ApplyRuntimeOptions() from UpgradeUpdateStrategy - Inline OSSUpdateStrategy timeout fallback --- .../Strategy/ClientUpdateStrategy.cs | 19 ++----------------- .../Strategy/OSSUpdateStrategy.cs | 5 +---- .../Strategy/UpgradeUpdateStrategy.cs | 12 ------------ 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 7b7c0a11..41a5a6ac 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -90,26 +90,11 @@ public ClientUpdateStrategy UseUpdatePrecheck(Func fu private async Task ExecuteWorkflowAsync() { // Standard mode — silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync(). - // Encoding, Format, and DownloadTimeOut are normally set by - // Bootstrap.ApplyRuntimeOptions(); the fallback ensures sensible defaults - // when the strategy is used without Bootstrap wiring. - ApplyStrategyDefaults(); + // Runtime options (Encoding, Format, DownloadTimeOut, etc.) are already + // populated on _configInfo by Bootstrap.ApplyRuntimeOptions(). await ExecuteStandardWorkflowAsync(); } - /// - /// Applies sensible fallback defaults for runtime options that may not - /// have been set by Bootstrap.ApplyRuntimeOptions(). Uses null-coalescing - /// so previously-assigned values (from UpdateOptions) are never overwritten. - /// - private void ApplyStrategyDefaults() - { - _configInfo!.Encoding ??= Encoding.UTF8; - _configInfo.Format ??= "ZIP"; - if (_configInfo.DownloadTimeOut <= 0) - _configInfo.DownloadTimeOut = 60; - } - private async Task ExecuteStandardWorkflowAsync() { GeneralTracer.Info($"ClientUpdateStrategy: validating client={_configInfo!.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}"); diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs index b5481f62..c7d6528a 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -31,9 +31,6 @@ public class OSSUpdateStrategy : IStrategy private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; private const int DefaultTimeOut = 60; - private int ResolveTimeout() - => _configInfo.DownloadTimeOut > 0 ? _configInfo.DownloadTimeOut : DefaultTimeOut; - /// Lifecycle hooks injected by the bootstrap. public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks(); /// Update status reporter injected by the bootstrap. @@ -160,7 +157,7 @@ private async Task DownloadAssetsAsync(List assets) } else { - using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ResolveTimeout()) }; + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(_configInfo.DownloadTimeOut > 0 ? _configInfo.DownloadTimeOut : DefaultTimeOut) }; var orchestrator = new DefaultDownloadOrchestrator(httpClient); await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false); } diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index ccfab7fb..90ecf53a 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -52,7 +52,6 @@ public async Task ExecuteAsync() return; } - ApplyRuntimeOptions(); _osStrategy!.Create(_configInfo); // Apply updates via OS-specific pipeline (Hash -> Compress -> Patch) @@ -109,17 +108,6 @@ private static IStrategy ResolveOsStrategy() throw new PlatformNotSupportedException("The current operating system is not supported!"); } - /// - /// Applies sensible fallback defaults for runtime options that may not - /// have been set by Bootstrap.ApplyRuntimeOptions(). Uses null-coalescing - /// so previously-assigned values (from UpdateOptions) are never overwritten. - /// - private void ApplyRuntimeOptions() - { - _configInfo!.Encoding ??= Encoding.UTF8; - _configInfo.Format ??= Format.ZIP; - } - // ════════════════════════════════════════════════════════════════ // Hooks & Reporter safe wrappers // ════════════════════════════════════════════════════════════════ From 29dddc4ddd658c48cf1fab2f938474ef53cca452 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 14:25:26 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20simplify=20ApplyRuntimeOptions=20?= =?UTF-8?q?=E2=80=94=20always=20apply=20from=20UpdateOptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the broken '<= 0' guards on MaxConcurrency/RetryCount/RetryInterval. These guards checked C# type defaults (0) but GlobalConfigInfo property initializers already set functionally reasonable values (3, 3, 1s), so the guards would never trigger in the Client path — user-configured values via .Option(MaxConcurrency, 8) were silently ignored. Only Encoding/Format/DownloadTimeOut need ??= protection — they are the only options that InitializeFromEnvironment() may populate on the Upgrade path before ApplyRuntimeOptions() runs. --- .../Bootstrap/GeneralUpdateBootstrap.cs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 47f6ae7e..4a8b7a7d 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -281,36 +281,30 @@ private void InitializeFromEnvironment() } /// - /// Applies configured UpdateOptions to _configInfo using null-coalescing or - /// conditional assignment to avoid overwriting values already populated by - /// InitializeFromEnvironment() (Upgrade path) or GlobalConfigInfo defaults. + /// Applies UpdateOptions to _configInfo. + /// Uses ??= only for values that InitializeFromEnvironment() may have already + /// populated on the Upgrade path (Encoding, Format, DownloadTimeOut). + /// All other options are always applied from UpdateOptions — their defaults + /// are already functionally reasonable (e.g. MaxConcurrency=3, RetryCount=3). /// private void ApplyRuntimeOptions() { - // Core runtime — use ??= so Upgrade path (InitializeFromEnvironment) values - // are not overwritten by GetOption defaults + // Preserve Upgrade path values set by InitializeFromEnvironment() _configInfo.Encoding ??= GetOption(UpdateOptions.Encoding); _configInfo.Format ??= GetOption(UpdateOptions.Format); if (_configInfo.DownloadTimeOut <= 0) _configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60; - // PatchEnabled / BackupEnabled: use ??= so null stays null - // (bool? with ??= means false from user is preserved, null gets default) + // bool? options: use ??= so user-configured false is preserved _configInfo.PatchEnabled ??= GetOption(UpdateOptions.PatchEnabled); _configInfo.BackupEnabled ??= GetOption(UpdateOptions.BackupEnabled); - // Download behaviour — preserve existing values (Upgrade path or property initializers) - // and only apply UpdateOptions when the current value is at its unset default - if (_configInfo.MaxConcurrency <= 0) - _configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency); - if (_configInfo.RetryCount <= 0) - _configInfo.RetryCount = GetOption(UpdateOptions.RetryCount); - if (_configInfo.RetryInterval <= TimeSpan.Zero) - _configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval); - - // Booleans: default "false" is meaningful, but property initializers set them to "true". - // Only apply from UpdateOptions when they differ from the property initializer default. + // Always apply from UpdateOptions — no other code sets these before + // ApplyRuntimeOptions() runs. Defaults are functionally reasonable. + _configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency); _configInfo.EnableResume = GetOption(UpdateOptions.EnableResume); + _configInfo.RetryCount = GetOption(UpdateOptions.RetryCount); + _configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval); _configInfo.VerifyChecksum = GetOption(UpdateOptions.VerifyChecksum); _configInfo.DiffMode = GetOption(UpdateOptions.DiffMode); } From 4451dedff5327525c2605c6d8c434ba0103a365e Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Mon, 25 May 2026 14:37:19 +0800 Subject: [PATCH 4/4] fix: normalize Format default from "ZIP" to ".zip" (Format.ZIP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpdateOptions.Format default value is "ZIP", but pipeline constructs zip file paths as `{name}{format}` and CompressProvider.Decompress switches on Format.ZIP (".zip"). If Format stays "ZIP", paths become "MyAppZIP" and decompression fails. Previously, ClientUpdateStrategy.ApplyRuntimeOptions(encoding, timeout) silently overwrote this with Format.ZIP (".zip"). After removing that redundant override in PR #401, the bug becomes visible. Normalize to Format.ZIP in Bootstrap.ApplyRuntimeOptions() — the single source of truth for runtime options. --- src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 4a8b7a7d..d6ee77d4 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -292,6 +292,10 @@ private void ApplyRuntimeOptions() // Preserve Upgrade path values set by InitializeFromEnvironment() _configInfo.Encoding ??= GetOption(UpdateOptions.Encoding); _configInfo.Format ??= GetOption(UpdateOptions.Format); + // Normalize legacy "ZIP" default (UpdateOptions) to Format.ZIP (".zip") + // so the pipeline constructs correct paths and CompressProvider matches its switch. + if (_configInfo.Format == "ZIP") + _configInfo.Format = Format.ZIP; if (_configInfo.DownloadTimeOut <= 0) _configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;