diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 4643d416..d6ee77d4 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -280,22 +280,36 @@ private void InitializeFromEnvironment() }; } + /// + /// 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() { - _configInfo.Encoding = GetOption(UpdateOptions.Encoding); - _configInfo.Format = GetOption(UpdateOptions.Format); - _configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60; - - // Download behaviour + // 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; + + // bool? options: use ??= so user-configured false is preserved + _configInfo.PatchEnabled ??= GetOption(UpdateOptions.PatchEnabled); + _configInfo.BackupEnabled ??= GetOption(UpdateOptions.BackupEnabled); + + // 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); - - // 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..41a5a6ac 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -89,16 +89,13 @@ 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(). + // Runtime options (Encoding, Format, DownloadTimeOut, etc.) are already + // populated on _configInfo by Bootstrap.ApplyRuntimeOptions(). + await ExecuteStandardWorkflowAsync(); } - private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout) + private async Task ExecuteStandardWorkflowAsync() { GeneralTracer.Info($"ClientUpdateStrategy: validating client={_configInfo!.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}"); @@ -145,7 +142,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 +168,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 +232,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..c7d6528a 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -29,7 +29,7 @@ public class OSSUpdateStrategy : IStrategy { private GlobalConfigInfo? _configInfo; private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; - private const int TimeOut = 60; + private const int DefaultTimeOut = 60; /// Lifecycle hooks injected by the bootstrap. public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks(); @@ -157,7 +157,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(_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 f0354a4d..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,12 +108,6 @@ private static IStrategy ResolveOsStrategy() throw new PlatformNotSupportedException("The current operating system is not supported!"); } - private void ApplyRuntimeOptions() - { - _configInfo!.Encoding = Encoding.UTF8; - _configInfo.Format = Format.ZIP; - } - // ════════════════════════════════════════════════════════════════ // Hooks & Reporter safe wrappers // ════════════════════════════════════════════════════════════════ 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() {