From 45859ff47fd44210234d5a4013068b77dfc81f38 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 13:21:19 +0800 Subject: [PATCH 1/2] fix: resolve P0 pipeline bugs and improve robustness in GeneralUpdate.Core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **P0 - MacStrategy Pipeline order mismatched Windows/Linux** MacStrategy.BuildPipeline registered middleware in wrong LIFO order (Hash→Compress→Patch), causing actual execution Patch→Compress→Hash. Fixed to match Windows/Linux: Patch→Compress→Hash (LIFO execution: Hash→Compress→Patch). Also added missing try/catch/finally with GracefulExit to StartApp. **P0 - PatchMiddleware unable to receive injected IBinaryDiffer** PipelineBuilder uses new() to create middleware, so the parametrized constructor was never invoked by the pipeline system. Differ was always null, making differential patching non-functional regardless of Bootstrap.BinaryDiffer() configuration. Fix: differ now flows through PipelineContext ("BinaryDiffer" key), set by AbstractStrategy.CreatePipelineContext from its new Differ property. Bootstrap injects differ into strategies after resolving extensions. **P1 - OSSUpgrade exception path lacked process exit** ExecuteUpgradeAsync re-threw exceptions without terminating the process. Added finally block with GracefulExit.CurrentProcessAsync(), and replaced throw with EventManager dispatch for consistent error handling. **P1 - Download semaphore had no timeout** SemaphoreSlim.WaitAsync in DefaultDownloadOrchestrator could block indefinitely if a hung download held the slot. Added 5-minute timeout. **P2 - Configinfo.Validate() was never called** SetConfig now calls configInfo.Validate() before mapping to catch invalid configuration early with clear error messages. **P2 - Rollback/restore not wired into update failure path** Added TryRollback() to AbstractStrategy, called when pipeline execution fails for a version. Restores from BackupDirectory if it exists. **P2 - GetTempDirectory timestamp only day-level precision** Added millisecond precision and ProcessId to prevent collisions between concurrent update processes. **Also fixed:** - OSSUpdateStrategy zipName construction ("MyAppzip.zip" double-zip) - GlobalConfigInfo orphaned DriverDirectory XML docs - Updated PatchMiddlewareTests for new PipelineContext approach Co-Authored-By: Claude Opus 4.7 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 16 ++++++- .../Configuration/GlobalConfigInfo.cs | 4 -- .../DefaultDownloadOrchestrator.cs | 5 +- .../FileSystem/StorageManager.cs | 2 +- .../Pipeline/PatchMiddleware.cs | 23 ++++----- .../Strategy/AbstractStrategy.cs | 31 +++++++++++- .../Strategy/ClientUpdateStrategy.cs | 7 +++ .../Strategy/MacStrategy.cs | 31 ++++++++---- .../Strategy/OSSUpdateStrategy.cs | 6 ++- .../Strategy/UpgradeUpdateStrategy.cs | 7 +++ .../CoreTest/Pipeline/PatchMiddlewareTests.cs | 48 +++++++------------ 11 files changed, 116 insertions(+), 64 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index ab6ddcda..cdcb8eb4 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -18,6 +18,7 @@ using GeneralUpdate.Core.Hooks; using GeneralUpdate.Core.Ipc; using GeneralUpdate.Core.Download.Reporting; +using GeneralUpdate.Core.Differential; namespace GeneralUpdate.Core; @@ -128,6 +129,16 @@ private async Task LaunchWithStrategy(IStrategy roleStra ossStrat.Reporter = reporter; } + // Inject binary differ into OS-level strategy for differential patching + var differ = ResolveExtension(); + if (differ != null) + { + if (roleStrategy is ClientUpdateStrategy cs2) + cs2.SetDiffer(differ); + else if (roleStrategy is UpgradeUpdateStrategy us2) + us2.SetDiffer(differ); + } + roleStrategy.Create(_configInfo); // Check custom skip condition before executing update @@ -161,8 +172,9 @@ private async Task LaunchWithStrategy(IStrategy roleStra // ════════════════════════════════════════════════════════════════ public GeneralUpdateBootstrap SetConfig(Configinfo configInfo) - { - _configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo); + { + configInfo.Validate(); + _configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo); var appType = GetOption(UpdateOptions.AppType); if (appType != AppType.Upgrade) diff --git a/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs b/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs index c17de889..03a478fe 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs @@ -95,10 +95,6 @@ public class GlobalConfigInfo : BaseConfigInfo /// public string ProcessInfo { get; set; } - /// - /// Directory path containing driver files for update. - /// Used when DriveEnabled is true to locate driver files for installation. - /// /// /// Indicates whether differential patch update is enabled. /// Computed from UpdateOption.Patch or defaults to true. diff --git a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs index 48b647a3..50e35998 100644 --- a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs @@ -67,7 +67,10 @@ public async Task ExecuteAsync( var tasks = plan.Assets.Select(async asset => { - await sem.WaitAsync(token).ConfigureAwait(false); + if (!await sem.WaitAsync(TimeSpan.FromMinutes(5), token).ConfigureAwait(false)) + { + GeneralTracer.Warn("DefaultDownloadOrchestrator: semaphore wait timed out, proceeding anyway."); + } try { var fileName = GetFileName(asset); diff --git a/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs b/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs index 7afe6771..aac160bd 100644 --- a/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs +++ b/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs @@ -82,7 +82,7 @@ public static void CreateJson(string targetPath, T obj, JsonTypeInfo? type public static string GetTempDirectory(string name) { - var path = $"generalupdate_{DateTime.Now:yyyy-MM-dd}_{name}"; + var path = $"generalupdate_{DateTime.Now:yyyy-MM-dd-HHmmss-fff}_{System.Diagnostics.Process.GetCurrentProcess().Id}_{name}"; var tempDir = Path.Combine(Path.GetTempPath(), path); if (!Directory.Exists(tempDir)) { diff --git a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs index 75b7ceaa..1fa9007c 100644 --- a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs +++ b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs @@ -10,29 +10,22 @@ namespace GeneralUpdate.Core.Pipeline; /// Differential patch middleware. Applies binary patches (BSDIFF, HDiffPatch, etc.) /// to bring files from an old version to a new version. /// -/// The implementation is injected via +/// The implementation is resolved from +/// (key "BinaryDiffer"), set by +/// when the differ is injected via /// Bootstrap.BinaryDiffer<T>(). Without injection, patches are skipped. /// public class PatchMiddleware : IMiddleware { - private readonly IBinaryDiffer? _differ; - - /// Parameterless constructor (required by PipelineBuilder). Uses no differ. - public PatchMiddleware() { } - - /// Creates a PatchMiddleware with an optional differ. - /// Binary differ implementation. If null, patches are skipped. - public PatchMiddleware(IBinaryDiffer? differ) - { - _differ = differ; - } - public async Task InvokeAsync(PipelineContext context) { var sourcePath = context.Get("SourcePath"); var targetPath = context.Get("PatchPath"); - if (_differ == null) + // Resolve differ from pipeline context (injected via AbstractStrategy) + var differ = context.Get("BinaryDiffer"); + + if (differ == null) { GeneralTracer.Info("PatchMiddleware.InvokeAsync: no IBinaryDiffer injected — patch skipped. " + "Use Bootstrap.BinaryDiffer() to enable differential patching."); @@ -42,7 +35,7 @@ public async Task InvokeAsync(PipelineContext context) GeneralTracer.Info($"PatchMiddleware.InvokeAsync: applying differential patch. SourcePath={sourcePath}, PatchPath={targetPath}"); try { - await _differ.DirtyAsync(sourcePath, targetPath, targetPath); + await differ.DirtyAsync(sourcePath, targetPath, targetPath); GeneralTracer.Info("PatchMiddleware.InvokeAsync: differential patch applied successfully."); } catch (Exception ex) diff --git a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs index 6cdeb140..be7e18c9 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using GeneralUpdate.Core.Differential; using GeneralUpdate.Core.FileSystem; using GeneralUpdate.Core.Event; using GeneralUpdate.Core.Pipeline; @@ -22,6 +23,9 @@ public abstract class AbstractStrategy : IStrategy /// Optional reporter for update status reporting. protected IUpdateReporter? Reporter { get; set; } + + /// Optional binary differ for differential patch updates. + public IBinaryDiffer? Differ { get; set; } public virtual void Execute() => throw new NotImplementedException(); @@ -46,6 +50,7 @@ public virtual async Task ExecuteAsync() { status = ReportType.Failure; HandleExecuteException(e); + TryRollback(); } finally { @@ -89,6 +94,8 @@ protected virtual PipelineContext CreatePipelineContext(VersionInfo version, str context.Add("SourcePath", _configinfo.InstallPath); context.Add("PatchPath", patchPath); context.Add("PatchEnabled", _configinfo.PatchEnabled); + // Binary differ for differential patching + context.Add("BinaryDiffer", Differ); return context; } @@ -135,10 +142,32 @@ protected static string CheckPath(string path, string name) // The Hooks and Reporter properties are declared here so subclasses inherit them // without redeclaring. + /// + /// Attempts to restore from backup when a pipeline execution fails. + /// Only restores if a backup directory exists for the current version. + /// + private void TryRollback() + { + try + { + var backupDir = _configinfo.BackupDirectory; + if (!string.IsNullOrWhiteSpace(backupDir) && Directory.Exists(backupDir)) + { + GeneralTracer.Warn($"AbstractStrategy.TryRollback: restoring from backup {backupDir} -> {_configinfo.InstallPath}"); + StorageManager.Restore(backupDir, _configinfo.InstallPath); + GeneralTracer.Info("AbstractStrategy.TryRollback: restore completed."); + } + } + catch (Exception ex) + { + GeneralTracer.Error("AbstractStrategy.TryRollback: rollback failed.", ex); + } + } + private static void Clear(string path) { if (Directory.Exists(path)) StorageManager.DeleteDirectory(path); } } -} \ No newline at end of file +} diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index ff9f0023..0c343483 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -73,6 +73,13 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } + /// Sets the binary differ on the underlying OS-level strategy for differential patch updates. + public void SetDiffer(Differential.IBinaryDiffer? differ) + { + if (_osStrategy is AbstractStrategy abs) + abs.Differ = differ; + } + public void StartApp() { _osStrategy?.StartApp(); diff --git a/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs index 11b998bc..d7d96dda 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs @@ -21,14 +21,28 @@ public override async Task ExecuteAsync() public override void StartApp() { - var mainApp = Path.Combine( - _configinfo.InstallPath ?? string.Empty, - _configinfo.MainAppName ?? string.Empty); + try + { + var mainApp = Path.Combine( + _configinfo.InstallPath ?? string.Empty, + _configinfo.MainAppName ?? string.Empty); - if (!string.IsNullOrEmpty(_configinfo.MainAppName) && File.Exists(mainApp)) + if (!string.IsNullOrEmpty(_configinfo.MainAppName) && File.Exists(mainApp)) + { + GeneralTracer.Info($"MacStrategy: starting {mainApp}"); + System.Diagnostics.Process.Start(mainApp); + } + } + catch (Exception e) + { + GeneralTracer.Error("The StartApp method in MacStrategy threw an exception.", e); + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(e, e.Message)); + } + finally { - GeneralTracer.Info($"MacStrategy: starting {mainApp}"); - System.Diagnostics.Process.Start(mainApp); + GeneralTracer.Info("MacStrategy.StartApp: releasing tracer and terminating updater process."); + GeneralTracer.Dispose(); + GracefulExit.CurrentProcessAsync().GetAwaiter().GetResult(); } } @@ -36,10 +50,11 @@ public override void StartApp() protected override PipelineBuilder BuildPipeline(PipelineContext context) { + GeneralTracer.Info($"MacStrategy.BuildPipeline: assembling middleware pipeline. PatchEnabled={_configinfo.PatchEnabled}"); var builder = new PipelineBuilder(context) - .UseMiddleware() + .UseMiddlewareIf(_configinfo.PatchEnabled) .UseMiddleware() - .UseMiddleware(); + .UseMiddleware(); return builder; } } diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs index eb393935..345c41ba 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -189,7 +189,11 @@ private async Task ExecuteUpgradeAsync() await SafeOnUpdateErrorAsync(ctx, ex).ConfigureAwait(false); await SafeReportUpdateFailedAsync(ctx, ex).ConfigureAwait(false); GeneralTracer.Error("OSSUpdateStrategy.ExecuteUpgradeAsync failed.", ex); - throw; + GeneralUpdate.Core.Event.EventManager.Instance.Dispatch(this, new GeneralUpdate.Core.Event.ExceptionEventArgs(ex, ex.Message)); + } + finally + { + await GracefulExit.CurrentProcessAsync().ConfigureAwait(false); } } diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index 90ecf53a..396d8cc9 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -90,6 +90,13 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } + /// Sets the binary differ on the underlying OS-level strategy for differential patch updates. + public void SetDiffer(Differential.IBinaryDiffer? differ) + { + if (_osStrategy is AbstractStrategy abs) + abs.Differ = differ; + } + public void StartApp() { _osStrategy?.StartApp(); diff --git a/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs b/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs index bf5630e2..5a2afe94 100644 --- a/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs +++ b/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs @@ -5,7 +5,7 @@ namespace CoreTest.Pipeline; /// /// AAAT unit tests for . -/// Covers: null differ (skip), non-null differ (invoke), success path, exception propagation, both constructors. +/// Covers: null differ (skip), non-null differ (invoke), success path, exception propagation. /// public class PatchMiddlewareTests { @@ -35,12 +35,12 @@ public Task DirtyAsync( } } - #region Parameterless constructor — null differ (skip) + #region No differ in context — skip [Fact] - public async Task InvokeAsync_NullDiffer_SkipsWithoutThrow() + public async Task InvokeAsync_NoDifferInContext_SkipsWithoutThrow() { - var middleware = new PatchMiddleware(); // paramless ctor = no differ + var middleware = new PatchMiddleware(); var context = new PipelineContext(); context.Add("SourcePath", "/src/path"); context.Add("PatchPath", "/patch/path"); @@ -63,33 +63,17 @@ public async Task InvokeAsync_NullContextProperties_SkipsWithoutThrow() #endregion - #region Explicit null differ — also skip + #region Differ in context — invokes [Fact] - public async Task InvokeAsync_ExplicitNullDiffer_SkipsWithoutThrow() - { - var middleware = new PatchMiddleware(differ: null!); - var context = new PipelineContext(); - context.Add("SourcePath", "/src"); - context.Add("PatchPath", "/patch"); - - var ex = await Record.ExceptionAsync(() => middleware.InvokeAsync(context)); - - Assert.Null(ex); - } - - #endregion - - #region Non-null differ — invokes - - [Fact] - public async Task InvokeAsync_ValidDiffer_InvokesDirtyAsync() + public async Task InvokeAsync_DifferInContext_InvokesDirtyAsync() { var differ = new StubDiffer(); - var middleware = new PatchMiddleware(differ); + var middleware = new PatchMiddleware(); var context = new PipelineContext(); context.Add("SourcePath", "/src/a.txt"); context.Add("PatchPath", "/patch/a.txt"); + context.Add("BinaryDiffer", differ); await middleware.InvokeAsync(context); @@ -97,13 +81,14 @@ public async Task InvokeAsync_ValidDiffer_InvokesDirtyAsync() } [Fact] - public async Task InvokeAsync_DifferThrows_ExceptionPropagates() + public async Task InvokeAsync_DifferInContext_ThrowsExceptionPropagates() { var differ = new StubDiffer { ShouldThrow = true }; - var middleware = new PatchMiddleware(differ); + var middleware = new PatchMiddleware(); var context = new PipelineContext(); context.Add("SourcePath", "/src"); context.Add("PatchPath", "/patch"); + context.Add("BinaryDiffer", differ); await Assert.ThrowsAsync(() => middleware.InvokeAsync(context)); } @@ -113,26 +98,27 @@ public async Task InvokeAsync_DifferThrows_ExceptionPropagates() #region PipelineContext values edge cases [Fact] - public async Task InvokeAsync_ValidDiffer_WithNullPaths_InvokesStill() + public async Task InvokeAsync_DifferInContext_NullPaths_InvokesStill() { var differ = new StubDiffer(); - var middleware = new PatchMiddleware(differ); + var middleware = new PatchMiddleware(); var context = new PipelineContext(); + context.Add("BinaryDiffer", differ); - // SourcePath/PatchPath are null in context — differ is still called with null args await middleware.InvokeAsync(context); Assert.True(differ.Invoked); } [Fact] - public async Task InvokeAsync_ValidDiffer_EmptyStringPaths_InvokesStill() + public async Task InvokeAsync_DifferInContext_EmptyStringPaths_InvokesStill() { var differ = new StubDiffer(); - var middleware = new PatchMiddleware(differ); + var middleware = new PatchMiddleware(); var context = new PipelineContext(); context.Add("SourcePath", string.Empty); context.Add("PatchPath", string.Empty); + context.Add("BinaryDiffer", differ); await middleware.InvokeAsync(context); From 15012007910ef3135740c7e43f91b20e2cdf0aa2 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 13:45:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20semaphore=20over-release,=20SetDiffer=20ordering,?= =?UTF-8?q?=20XML=20doc=20cref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Semaphore: only Release() when WaitAsync succeeded; skip asset on timeout instead of bypassing concurrency limit (prevent SemaphoreFullException). - SetDiffer: move injection after Create() so _osStrategy exists; add _pendingDiffer caching in ClientUpdateStrategy/UpgradeUpdateStrategy so SetDiffer is safe to call at any point. - XML doc cref: use fully-qualified type name in PatchMiddleware. Co-Authored-By: Claude Opus 4.7 --- ipc/BOWL_TEST_ENV_VAR.enc | 1 + .../Bootstrap/GeneralUpdateBootstrap.cs | 5 +++-- .../Orchestrators/DefaultDownloadOrchestrator.cs | 10 ++++++++-- src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs | 2 +- .../Strategy/ClientUpdateStrategy.cs | 9 ++++++++- .../Strategy/UpgradeUpdateStrategy.cs | 9 ++++++++- 6 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 ipc/BOWL_TEST_ENV_VAR.enc diff --git a/ipc/BOWL_TEST_ENV_VAR.enc b/ipc/BOWL_TEST_ENV_VAR.enc new file mode 100644 index 00000000..658d7bb5 --- /dev/null +++ b/ipc/BOWL_TEST_ENV_VAR.enc @@ -0,0 +1 @@ +j{N;qm8 \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index cdcb8eb4..4a171417 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -129,7 +129,10 @@ private async Task LaunchWithStrategy(IStrategy roleStra ossStrat.Reporter = reporter; } + roleStrategy.Create(_configInfo); + // Inject binary differ into OS-level strategy for differential patching + // Must be called after Create() since _osStrategy is initialized there. var differ = ResolveExtension(); if (differ != null) { @@ -139,8 +142,6 @@ private async Task LaunchWithStrategy(IStrategy roleStra us2.SetDiffer(differ); } - roleStrategy.Create(_configInfo); - // Check custom skip condition before executing update if (_customSkipOption?.Invoke() == true) { diff --git a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs index 50e35998..bafad077 100644 --- a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs @@ -67,9 +67,15 @@ public async Task ExecuteAsync( var tasks = plan.Assets.Select(async asset => { - if (!await sem.WaitAsync(TimeSpan.FromMinutes(5), token).ConfigureAwait(false)) + var acquired = await sem.WaitAsync(TimeSpan.FromMinutes(5), token).ConfigureAwait(false); + if (!acquired) { - GeneralTracer.Warn("DefaultDownloadOrchestrator: semaphore wait timed out, proceeding anyway."); + GeneralTracer.Warn("DefaultDownloadOrchestrator: semaphore wait timed out for " + asset.Name + ", skipping."); + lock (results) + { + results.Add(new DownloadResult(asset, null, 0, TimeSpan.Zero, 0, false, "Semaphore wait timed out")); + } + return; } try { diff --git a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs index 1fa9007c..bc45cf38 100644 --- a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs +++ b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs @@ -12,7 +12,7 @@ namespace GeneralUpdate.Core.Pipeline; /// /// The implementation is resolved from /// (key "BinaryDiffer"), set by -/// when the differ is injected via +/// when the differ is injected via /// Bootstrap.BinaryDiffer<T>(). Without injection, patches are skipped. /// public class PatchMiddleware : IMiddleware diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 0c343483..cac7cd2e 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -46,6 +46,8 @@ public void Create(GlobalConfigInfo parameter) { _configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter)); _osStrategy = ResolveOsStrategy(); + if (_pendingDiffer != null && _osStrategy is AbstractStrategy abs) + abs.Differ = _pendingDiffer; } public async Task ExecuteAsync() @@ -73,11 +75,16 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } - /// Sets the binary differ on the underlying OS-level strategy for differential patch updates. + private Differential.IBinaryDiffer? _pendingDiffer; + + /// Sets the binary differ on the underlying OS-level strategy for differential patch updates. + /// Safe to call before or after Create(). If called before, the differ is cached and applied when Create() resolves _osStrategy. public void SetDiffer(Differential.IBinaryDiffer? differ) { if (_osStrategy is AbstractStrategy abs) abs.Differ = differ; + else + _pendingDiffer = differ; } public void StartApp() diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index 396d8cc9..09b5a64d 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -34,6 +34,8 @@ public void Create(GlobalConfigInfo parameter) { _configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter)); _osStrategy = ResolveOsStrategy(); + if (_pendingDiffer != null && _osStrategy is AbstractStrategy abs) + abs.Differ = _pendingDiffer; } public async Task ExecuteAsync() @@ -90,11 +92,16 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } - /// Sets the binary differ on the underlying OS-level strategy for differential patch updates. + private Differential.IBinaryDiffer? _pendingDiffer; + + /// Sets the binary differ on the underlying OS-level strategy for differential patch updates. + /// Safe to call before or after Create(). If called before, the differ is cached and applied when Create() resolves _osStrategy. public void SetDiffer(Differential.IBinaryDiffer? differ) { if (_osStrategy is AbstractStrategy abs) abs.Differ = differ; + else + _pendingDiffer = differ; } public void StartApp()