From 8517871feb083b3d3c327248595e5c225b1c2aeb Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 14:52:36 +0800 Subject: [PATCH 1/6] fix: restore two-process architecture in standard Client mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClientUpdateStrategy previously ran the update pipeline and launched the main application in-process. This broke the "mutual upgrade" capability (client cannot update the upgrade executable itself while running). Now the Client validates versions, downloads packages, sends IPC, and launches the upgrade process (AppName, default "Update.exe") — then exits. The upgrade process reads ProcessInfo via AES-encrypted file IPC, runs the pipeline, and launches the main application. This matches the documented two-process architecture and the behavior already used by SilentPollOrchestrator and OSSUpdateStrategy. Co-Authored-By: Claude Opus 4.7 --- .../Strategy/ClientUpdateStrategy.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index cac7cd2e..f24d9df8 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -232,12 +232,17 @@ private async Task ExecuteStandardWorkflowAsync() await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); await SafeOnDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); - // Apply updates and start app - await _osStrategy.ExecuteAsync(); - await SafeOnAfterUpdateAsync(hooksCtx).ConfigureAwait(false); - await SafeReportUpdateAppliedAsync(hooksCtx).ConfigureAwait(false); - await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false); - _osStrategy.StartApp(); + // Launch the upgrade process to apply updates and restart the main application. + // The upgrade process (AppName, default "Update.exe") reads ProcessInfo via IPC + // and runs the pipeline (Hash -> Compress -> Patch) before launching the main app. + var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); + if (!File.Exists(updaterPath)) + throw new FileNotFoundException($"Upgrade application not found: {updaterPath}"); + + GeneralTracer.Info($"ClientUpdateStrategy: launching upgrade process {updaterPath}"); + Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath }); + GeneralTracer.Info("ClientUpdateStrategy: upgrade process launched, exiting."); + await GracefulExit.CurrentProcessAsync().ConfigureAwait(false); } #endregion From 037decf21c4c12d7427ce2d198d8fa821b23abb5 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 15:10:30 +0800 Subject: [PATCH 2/6] feat: two-phase pipeline in UpgradeUpdateStrategy for mutual upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits UpdateVersions by AppType and runs the pipeline in two phases: 1. Upgrade itself (AppType.Upgrade) — updates Upgrade.exe first 2. MainApp (AppType.Client) — updates the main application This enables the "mutual upgrade" pattern: Client downloads all packages and launches Upgrade, which updates itself before updating MainApp. Co-Authored-By: Claude Opus 4.7 --- .../Strategy/UpgradeUpdateStrategy.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index 09b5a64d..3f6f22ba 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -54,14 +55,36 @@ public async Task ExecuteAsync() return; } - _osStrategy!.Create(_configInfo); + var allVersions = _configInfo.UpdateVersions ?? Enumerable.Empty().ToList(); + var upgradeVersions = allVersions.Where(v => v.AppType == (int)AppType.Upgrade).ToList(); + var clientVersions = allVersions.Where(v => v.AppType != (int)AppType.Upgrade).ToList(); - // Apply updates via OS-specific pipeline (Hash -> Compress -> Patch) - if (_configInfo.UpdateVersions?.Count > 0) + // Phase 1: update Upgrade.exe itself (if upgrade packages exist) + if (upgradeVersions.Count > 0) { - GeneralTracer.Info($"UpgradeUpdateStrategy: applying {_configInfo.UpdateVersions.Count} update(s)."); + GeneralTracer.Info($"UpgradeUpdateStrategy: phase 1 — updating Upgrade itself ({upgradeVersions.Count} version(s))."); + var prevUpdateVersions = _configInfo.UpdateVersions; + _configInfo.UpdateVersions = upgradeVersions; + _osStrategy!.Create(_configInfo); await _osStrategy.ExecuteAsync(); + _configInfo.UpdateVersions = prevUpdateVersions; + GeneralTracer.Info("UpgradeUpdateStrategy: phase 1 complete."); } + + // Phase 2: update MainApp + if (clientVersions.Count > 0) + { + GeneralTracer.Info($"UpgradeUpdateStrategy: phase 2 — updating MainApp ({clientVersions.Count} version(s))."); + _configInfo.UpdateVersions = clientVersions; + _osStrategy!.Create(_configInfo); + await _osStrategy.ExecuteAsync(); + GeneralTracer.Info("UpgradeUpdateStrategy: phase 2 complete."); + } + else if (upgradeVersions.Count == 0) + { + GeneralTracer.Info("UpgradeUpdateStrategy: no updates to apply, starting application directly."); + } + else { GeneralTracer.Info("UpgradeUpdateStrategy: no updates to apply, starting application directly."); From 51947f7d3e7c42547d5ac345474e9b1c25bc55c3 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 15:15:47 +0800 Subject: [PATCH 3/6] fix: Client applies Upgrade pipeline, Upgrade applies MainApp pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each process updates the OTHER executable — no file lock conflict: - MainApp runs pipeline for Upgrade packages → updates Upgrade.exe - Upgrade runs pipeline for MainApp packages → updates MainApp.exe - IPC now only carries MainApp versions (Upgrade already applied) Co-Authored-By: Claude Opus 4.7 --- .../Strategy/ClientUpdateStrategy.cs | 84 ++++++++++++------- .../Strategy/UpgradeUpdateStrategy.cs | 31 +------ 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index f24d9df8..c8c0ac34 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -174,31 +174,6 @@ private async Task ExecuteStandardWorkflowAsync() return; } - // Build process info for the upgrade process - // Convert DownloadAsset list to VersionInfo for ProcessInfo compatibility - var downloadVersions = downloadPlan.Assets.Select(a => new VersionInfo - { - Name = a.Name, - Hash = a.SHA256, - Url = a.Url, - Version = a.Version, - Format = _configInfo.Format ?? "ZIP" - }).ToList(); - - var processInfo = ConfigurationMapper.MapToProcessInfo( - _configInfo, downloadVersions, - _configInfo.BlackFormats ?? BlackListDefaults.DefaultBlackFormats, - _configInfo.BlackFiles ?? BlackListDefaults.DefaultBlackFiles, - _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories); - - // Keep JSON string for backward compatibility (GlobalConfigInfo.ProcessInfo) - _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, - ProcessInfoJsonContext.Default.ProcessInfo); - - // Wire ProcessInfo via AES-encrypted file IPC. - new EncryptedFileProcessInfoProvider().Send(processInfo); - GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent via encrypted file IPC."); - // Backup — conditionally skipped when BackupEnabled is false if (_configInfo.BackupEnabled != false) { @@ -232,9 +207,62 @@ private async Task ExecuteStandardWorkflowAsync() await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); await SafeOnDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); - // Launch the upgrade process to apply updates and restart the main application. - // The upgrade process (AppName, default "Update.exe") reads ProcessInfo via IPC - // and runs the pipeline (Hash -> Compress -> Patch) before launching the main app. + // Phase: apply Upgrade packages — update Upgrade.exe itself before launching it. + // Safe because MainApp and Upgrade.exe are different files (no lock conflict). + var allVersions = downloadPlan.Assets.Select(a => new VersionInfo + { + Name = a.Name, + Hash = a.SHA256, + Url = a.Url, + Version = a.Version, + Format = _configInfo.Format ?? "ZIP", + AppType = a.IsForcibly ? null : null // preserve original AppType + }).ToList(); + + // Rebuild the full VersionInfo list with AppType preserved from download source + var downloadVersions = downloadPlan.Assets.Select(a => new VersionInfo + { + Name = a.Name, + Hash = a.SHA256, + Url = a.Url, + Version = a.Version, + Format = _configInfo.Format ?? "ZIP", + AppType = _configInfo.IsUpgradeUpdate == true && a.Version != _configInfo.ClientVersion + ? (int)AppType.Upgrade : (int)AppType.Client + }).ToList(); + + // Split: Upgrade versions vs MainApp versions + var upgradeVersions = downloadVersions.Where(v => v.AppType == (int)AppType.Upgrade).ToList(); + var clientVersions = downloadVersions.Where(v => v.AppType != (int)AppType.Upgrade).ToList(); + + GeneralTracer.Info($"ClientUpdateStrategy: Upgrade packages={upgradeVersions.Count}, MainApp packages={clientVersions.Count}"); + + // Apply Upgrade packages now (update Upgrade.exe before launching it) + if (upgradeVersions.Count > 0) + { + GeneralTracer.Info("ClientUpdateStrategy: applying Upgrade packages."); + _configInfo.UpdateVersions = upgradeVersions; + _osStrategy!.Create(_configInfo); + await _osStrategy.ExecuteAsync(); + } + + // Send IPC with remaining MainApp versions for the upgrade process + var processInfo = ConfigurationMapper.MapToProcessInfo( + _configInfo, clientVersions, + _configInfo.BlackFormats ?? BlackListDefaults.DefaultBlackFormats, + _configInfo.BlackFiles ?? BlackListDefaults.DefaultBlackFiles, + _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories); + + _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, + ProcessInfoJsonContext.Default.ProcessInfo); + new EncryptedFileProcessInfoProvider().Send(processInfo); + GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent with MainApp versions only."); + + await SafeOnAfterUpdateAsync(hooksCtx).ConfigureAwait(false); + await SafeReportUpdateAppliedAsync(hooksCtx).ConfigureAwait(false); + await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false); + + // Launch the upgrade process to apply MainApp updates var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); if (!File.Exists(updaterPath)) throw new FileNotFoundException($"Upgrade application not found: {updaterPath}"); diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index 3f6f22ba..73629f26 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -55,36 +54,14 @@ public async Task ExecuteAsync() return; } - var allVersions = _configInfo.UpdateVersions ?? Enumerable.Empty().ToList(); - var upgradeVersions = allVersions.Where(v => v.AppType == (int)AppType.Upgrade).ToList(); - var clientVersions = allVersions.Where(v => v.AppType != (int)AppType.Upgrade).ToList(); + _osStrategy!.Create(_configInfo); - // Phase 1: update Upgrade.exe itself (if upgrade packages exist) - if (upgradeVersions.Count > 0) + // Apply MainApp updates — Client already applied Upgrade packages, IPC only has MainApp versions + if (_configInfo.UpdateVersions?.Count > 0) { - GeneralTracer.Info($"UpgradeUpdateStrategy: phase 1 — updating Upgrade itself ({upgradeVersions.Count} version(s))."); - var prevUpdateVersions = _configInfo.UpdateVersions; - _configInfo.UpdateVersions = upgradeVersions; - _osStrategy!.Create(_configInfo); + GeneralTracer.Info("UpgradeUpdateStrategy: applying " + _configInfo.UpdateVersions.Count + " MainApp update(s)."); await _osStrategy.ExecuteAsync(); - _configInfo.UpdateVersions = prevUpdateVersions; - GeneralTracer.Info("UpgradeUpdateStrategy: phase 1 complete."); } - - // Phase 2: update MainApp - if (clientVersions.Count > 0) - { - GeneralTracer.Info($"UpgradeUpdateStrategy: phase 2 — updating MainApp ({clientVersions.Count} version(s))."); - _configInfo.UpdateVersions = clientVersions; - _osStrategy!.Create(_configInfo); - await _osStrategy.ExecuteAsync(); - GeneralTracer.Info("UpgradeUpdateStrategy: phase 2 complete."); - } - else if (upgradeVersions.Count == 0) - { - GeneralTracer.Info("UpgradeUpdateStrategy: no updates to apply, starting application directly."); - } - else { GeneralTracer.Info("UpgradeUpdateStrategy: no updates to apply, starting application directly."); From 55c4c0265adff061dca3f5a5e1284346f89a3db2 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 15:39:25 +0800 Subject: [PATCH 4/6] refactor: replace IBinaryDiffer with IDirtyStrategy in Core layer Core should only depend on IDirtyStrategy (apply patches), not IBinaryDiffer (which has both CleanAsync for patch generation and DirtyAsync for application). Patch generation belongs in GeneralUpdate.Differential. - Removed IBinaryDiffer.cs from Core (moved responsibility to Differential) - PatchMiddleware now calls IDirtyStrategy.ExecuteAsync(sourcePath, patchPath) - AbstractStrategy.DirtyStrategy property replaces Differ - Bootstrap.DirtyStrategy() replaces Bootstrap.BinaryDiffer() - Updated all tests accordingly Co-Authored-By: Claude Opus 4.7 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 8 +-- .../Configuration/AbstractBootstrap.cs | 2 - .../Differential/IBinaryDiffer.cs | 40 ------------- .../Pipeline/PatchMiddleware.cs | 10 ++-- .../Strategy/AbstractStrategy.cs | 4 +- .../Strategy/ClientUpdateStrategy.cs | 13 +++-- .../Strategy/UpgradeUpdateStrategy.cs | 13 +++-- .../BootstrapFullParameterMatrixTests.cs | 13 +---- .../CoreTest/Pipeline/PatchMiddlewareTests.cs | 56 ++++++++----------- 9 files changed, 49 insertions(+), 110 deletions(-) delete mode 100644 src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 4a171417..b6c7871a 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -133,13 +133,13 @@ private async Task LaunchWithStrategy(IStrategy roleStra // 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) + var dirtyStrategy = ResolveExtension(); + if (dirtyStrategy != null) { if (roleStrategy is ClientUpdateStrategy cs2) - cs2.SetDiffer(differ); + cs2.SetDirtyStrategy(dirtyStrategy); else if (roleStrategy is UpgradeUpdateStrategy us2) - us2.SetDiffer(differ); + us2.SetDirtyStrategy(dirtyStrategy); } // Check custom skip condition before executing update diff --git a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs index d8c8b7f8..5a05f149 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs @@ -55,8 +55,6 @@ protected T GetOption(UpdateOption? option) public TBootstrap SslPolicy() where T : Security.ISslValidationPolicy, new() { _extensions[typeof(Security.ISslValidationPolicy)] = typeof(T); return (TBootstrap)this; } - public TBootstrap BinaryDiffer() where T : Differential.IBinaryDiffer, new() - { _extensions[typeof(Differential.IBinaryDiffer)] = typeof(T); return (TBootstrap)this; } public TBootstrap PipelineFactory() where T : Pipeline.IUpdatePipelineFactory, new() { _extensions[typeof(Pipeline.IUpdatePipelineFactory)] = typeof(T); return (TBootstrap)this; } diff --git a/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs b/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs deleted file mode 100644 index c0f4aae6..00000000 --- a/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace GeneralUpdate.Core.Differential -{ - /// - /// Defines a pluggable binary differential algorithm (diff generation and patch application). - /// Implementations may use different strategies: BSDIFF, HDiffPatch-style, VCDIFF, etc. - /// - /// - /// This interface lives in Core so that Pipeline middleware can depend on it - /// without creating a circular dependency on the GeneralUpdate.Differential assembly. - /// - /// Concrete implementations (StreamingHdiffDiffer, BSDIFF, etc.) live in - /// GeneralUpdate.Differential and are injected via Bootstrap.BinaryDiffer<T>(). - /// - public interface IBinaryDiffer - { - /// - /// Generates a binary patch from to , - /// writing the result to . - /// - Task CleanAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default); - - /// - /// Applies a binary patch to , producing - /// using the patch at . - /// - Task DirtyAsync( - string oldFilePath, - string newFilePath, - string patchFilePath, - CancellationToken cancellationToken = default); - } -} diff --git a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs index bc45cf38..13615104 100644 --- a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs +++ b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs @@ -23,19 +23,19 @@ public async Task InvokeAsync(PipelineContext context) var targetPath = context.Get("PatchPath"); // Resolve differ from pipeline context (injected via AbstractStrategy) - var differ = context.Get("BinaryDiffer"); + var dirtyStrategy = context.Get("DirtyStrategy"); - if (differ == null) + if (dirtyStrategy == null) { - GeneralTracer.Info("PatchMiddleware.InvokeAsync: no IBinaryDiffer injected — patch skipped. " + - "Use Bootstrap.BinaryDiffer() to enable differential patching."); + GeneralTracer.Info("PatchMiddleware.InvokeAsync: no IDirtyStrategy injected — patch skipped. " + + "Use Bootstrap.DirtyStrategy() to enable differential patching."); return; } GeneralTracer.Info($"PatchMiddleware.InvokeAsync: applying differential patch. SourcePath={sourcePath}, PatchPath={targetPath}"); try { - await differ.DirtyAsync(sourcePath, targetPath, targetPath); + await dirtyStrategy.ExecuteAsync(sourcePath, 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 be7e18c9..2701bff9 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs @@ -25,7 +25,7 @@ public abstract class AbstractStrategy : IStrategy protected IUpdateReporter? Reporter { get; set; } /// Optional binary differ for differential patch updates. - public IBinaryDiffer? Differ { get; set; } + public IDirtyStrategy? DirtyStrategy { get; set; } public virtual void Execute() => throw new NotImplementedException(); @@ -95,7 +95,7 @@ protected virtual PipelineContext CreatePipelineContext(VersionInfo version, str context.Add("PatchPath", patchPath); context.Add("PatchEnabled", _configinfo.PatchEnabled); // Binary differ for differential patching - context.Add("BinaryDiffer", Differ); + context.Add("DirtyStrategy", DirtyStrategy); return context; } diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index c8c0ac34..2e8f2dc1 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -1,3 +1,4 @@ +using GeneralUpdate.Core.Differential; using System; using System.Collections.Generic; using System.Diagnostics; @@ -46,8 +47,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; + if (_pendingDirtyStrategy != null && _osStrategy is AbstractStrategy abs) + abs.DirtyStrategy = _pendingDirtyStrategy; } public async Task ExecuteAsync() @@ -75,16 +76,16 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } - private Differential.IBinaryDiffer? _pendingDiffer; + private IDirtyStrategy? _pendingDirtyStrategy; /// 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) + public void SetDirtyStrategy(IDirtyStrategy? dirtyStrategy) { if (_osStrategy is AbstractStrategy abs) - abs.Differ = differ; + abs.DirtyStrategy = dirtyStrategy; else - _pendingDiffer = differ; + _pendingDirtyStrategy = dirtyStrategy; } public void StartApp() diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index 73629f26..dac0f67c 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -1,3 +1,4 @@ +using GeneralUpdate.Core.Differential; using System; using System.Runtime.InteropServices; using System.Text; @@ -34,8 +35,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; + if (_pendingDirtyStrategy != null && _osStrategy is AbstractStrategy abs) + abs.DirtyStrategy = _pendingDirtyStrategy; } public async Task ExecuteAsync() @@ -92,16 +93,16 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } - private Differential.IBinaryDiffer? _pendingDiffer; + private IDirtyStrategy? _pendingDirtyStrategy; /// 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) + public void SetDirtyStrategy(IDirtyStrategy? dirtyStrategy) { if (_osStrategy is AbstractStrategy abs) - abs.Differ = differ; + abs.DirtyStrategy = dirtyStrategy; else - _pendingDiffer = differ; + _pendingDirtyStrategy = dirtyStrategy; } public void StartApp() diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs index 0f6dd7b5..6cff8e62 100644 --- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs @@ -109,13 +109,6 @@ public bool ValidateCertificate(System.Security.Cryptography.X509Certificates.X5 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 { @@ -176,17 +169,15 @@ private sealed class StubDirtyStrategy : GeneralUpdate.Core.Differential.IDirtyS [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_DirtyStrategy() => Assert.NotNull(B().DirtyStrategy()); [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() @@ -199,7 +190,7 @@ public void Chain_AllExtensionsInjected() .DownloadSource() .DownloadPipeline() .DownloadOrchestrator() - .BinaryDiffer() + .DirtyStrategy() .CleanStrategy() .DirtyStrategy() .SslPolicy() diff --git a/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs b/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs index 5a2afe94..a4661d4b 100644 --- a/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs +++ b/tests/CoreTest/Pipeline/PatchMiddlewareTests.cs @@ -5,40 +5,28 @@ namespace CoreTest.Pipeline; /// /// AAAT unit tests for . -/// Covers: null differ (skip), non-null differ (invoke), success path, exception propagation. +/// Covers: null strategy (skip), non-null strategy (invoke), success path, exception propagation. /// public class PatchMiddlewareTests { - private sealed class StubDiffer : IBinaryDiffer + private sealed class StubDirtyStrategy : IDirtyStrategy { public bool Invoked { get; private set; } public bool ShouldThrow { get; set; } - public Task CleanAsync( - string oldFilePath, string newFilePath, string patchFilePath, - CancellationToken cancellationToken = default) + public Task ExecuteAsync(string appPath, string patchPath) { Invoked = true; if (ShouldThrow) - throw new InvalidOperationException("test differ failure"); - return Task.CompletedTask; - } - - public Task DirtyAsync( - string oldFilePath, string newFilePath, string patchFilePath, - CancellationToken cancellationToken = default) - { - Invoked = true; - if (ShouldThrow) - throw new InvalidOperationException("test differ failure"); + throw new InvalidOperationException("test dirty strategy failure"); return Task.CompletedTask; } } - #region No differ in context — skip + #region No strategy in context — skip [Fact] - public async Task InvokeAsync_NoDifferInContext_SkipsWithoutThrow() + public async Task InvokeAsync_NoDirtyStrategyInContext_SkipsWithoutThrow() { var middleware = new PatchMiddleware(); var context = new PipelineContext(); @@ -63,32 +51,32 @@ public async Task InvokeAsync_NullContextProperties_SkipsWithoutThrow() #endregion - #region Differ in context — invokes + #region Strategy in context — invokes [Fact] - public async Task InvokeAsync_DifferInContext_InvokesDirtyAsync() + public async Task InvokeAsync_DirtyStrategyInContext_InvokesExecuteAsync() { - var differ = new StubDiffer(); + var strategy = new StubDirtyStrategy(); var middleware = new PatchMiddleware(); var context = new PipelineContext(); context.Add("SourcePath", "/src/a.txt"); context.Add("PatchPath", "/patch/a.txt"); - context.Add("BinaryDiffer", differ); + context.Add("DirtyStrategy", strategy); await middleware.InvokeAsync(context); - Assert.True(differ.Invoked); + Assert.True(strategy.Invoked); } [Fact] - public async Task InvokeAsync_DifferInContext_ThrowsExceptionPropagates() + public async Task InvokeAsync_DirtyStrategyThrows_ExceptionPropagates() { - var differ = new StubDiffer { ShouldThrow = true }; + var strategy = new StubDirtyStrategy { ShouldThrow = true }; var middleware = new PatchMiddleware(); var context = new PipelineContext(); context.Add("SourcePath", "/src"); context.Add("PatchPath", "/patch"); - context.Add("BinaryDiffer", differ); + context.Add("DirtyStrategy", strategy); await Assert.ThrowsAsync(() => middleware.InvokeAsync(context)); } @@ -98,31 +86,31 @@ public async Task InvokeAsync_DifferInContext_ThrowsExceptionPropagates() #region PipelineContext values edge cases [Fact] - public async Task InvokeAsync_DifferInContext_NullPaths_InvokesStill() + public async Task InvokeAsync_DirtyStrategyInContext_NullPaths_InvokesStill() { - var differ = new StubDiffer(); + var strategy = new StubDirtyStrategy(); var middleware = new PatchMiddleware(); var context = new PipelineContext(); - context.Add("BinaryDiffer", differ); + context.Add("DirtyStrategy", strategy); await middleware.InvokeAsync(context); - Assert.True(differ.Invoked); + Assert.True(strategy.Invoked); } [Fact] - public async Task InvokeAsync_DifferInContext_EmptyStringPaths_InvokesStill() + public async Task InvokeAsync_DirtyStrategyInContext_EmptyStringPaths_InvokesStill() { - var differ = new StubDiffer(); + var strategy = new StubDirtyStrategy(); var middleware = new PatchMiddleware(); var context = new PipelineContext(); context.Add("SourcePath", string.Empty); context.Add("PatchPath", string.Empty); - context.Add("BinaryDiffer", differ); + context.Add("DirtyStrategy", strategy); await middleware.InvokeAsync(context); - Assert.True(differ.Invoked); + Assert.True(strategy.Invoked); } #endregion From 4bba626e6e5bd010a3b6bbf41a98f1d7d823504b Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 16:03:25 +0800 Subject: [PATCH 5/6] feat: expose IBinaryDiffer injection alongside IDirtyStrategy - Restored IBinaryDiffer to Core with DirtyAsync only (file-level algorithm) - Added BinaryDiffer() injection to AbstractBootstrap - Added BinaryDiffer property to AbstractStrategy, passed via PipelineContext - Bootstrap injects both IDirtyStrategy and IBinaryDiffer into strategies - ClientUpdateStrategy/UpgradeUpdateStrategy: SetBinaryDiffer() - Fixed Differential's IBinaryDiffer: extends Core's + adds CleanAsync Co-Authored-By: Claude Opus 4.7 --- .../Configuration/AbstractBootstrap.cs | 3 ++ .../Differential/IBinaryDiffer.cs | 17 +++++++++++ .../Strategy/AbstractStrategy.cs | 4 +++ .../Strategy/ClientUpdateStrategy.cs | 11 ++++++-- .../Strategy/UpgradeUpdateStrategy.cs | 11 ++++++-- .../Abstractions/IBinaryDiffer.cs | 28 ++++++++----------- 6 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs diff --git a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs index 5a05f149..36c396c6 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs @@ -86,6 +86,9 @@ protected T GetOption(UpdateOption? option) public TBootstrap DirtyStrategy() where T : Differential.IDirtyStrategy, new() { _extensions[typeof(Differential.IDirtyStrategy)] = typeof(T); return (TBootstrap)this; } + public TBootstrap BinaryDiffer() where T : Differential.IBinaryDiffer, new() + { _extensions[typeof(Differential.IBinaryDiffer)] = typeof(T); return (TBootstrap)this; } + public TBootstrap ConfigureBlackList(BlackListConfig config) { _instances[typeof(BlackListConfig)] = config ?? BlackListConfig.Empty; diff --git a/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs b/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs new file mode 100644 index 00000000..1cb56e23 --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Differential/IBinaryDiffer.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace GeneralUpdate.Core.Differential; + +/// +/// Pluggable file-level binary patch-application algorithm. +/// Implement this to customize how individual files are patched (BSDIFF, HDiffPatch, etc.). +/// +/// For full directory-level control, inject instead. +/// +public interface IBinaryDiffer +{ + /// Applies a binary patch: oldFile + patchFile → newFile. + Task DirtyAsync(string oldFilePath, string newFilePath, string patchFilePath, + CancellationToken cancellationToken = default); +} diff --git a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs index 2701bff9..08a01cf1 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs @@ -26,6 +26,9 @@ public abstract class AbstractStrategy : IStrategy /// Optional binary differ for differential patch updates. public IDirtyStrategy? DirtyStrategy { get; set; } + + /// Optional file-level binary differ for patch application. + public IBinaryDiffer? BinaryDiffer { get; set; } public virtual void Execute() => throw new NotImplementedException(); @@ -96,6 +99,7 @@ protected virtual PipelineContext CreatePipelineContext(VersionInfo version, str context.Add("PatchEnabled", _configinfo.PatchEnabled); // Binary differ for differential patching context.Add("DirtyStrategy", DirtyStrategy); + context.Add("BinaryDiffer", BinaryDiffer); return context; } diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index 2e8f2dc1..465f720e 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -78,8 +78,8 @@ public void Execute() private IDirtyStrategy? _pendingDirtyStrategy; - /// 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. + /// Sets the directory-level dirty strategy on the underlying OS-level strategy for differential patch updates. + /// Safe to call before or after Create(). If called before, the strategy is cached and applied when Create() resolves _osStrategy. public void SetDirtyStrategy(IDirtyStrategy? dirtyStrategy) { if (_osStrategy is AbstractStrategy abs) @@ -88,6 +88,13 @@ public void SetDirtyStrategy(IDirtyStrategy? dirtyStrategy) _pendingDirtyStrategy = dirtyStrategy; } + /// Sets the file-level binary differ on the underlying OS-level strategy. + public void SetBinaryDiffer(IBinaryDiffer? binaryDiffer) + { + if (_osStrategy is AbstractStrategy abs) + abs.BinaryDiffer = binaryDiffer; + } + public void StartApp() { _osStrategy?.StartApp(); diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index dac0f67c..7f52fc05 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -95,8 +95,8 @@ public void Execute() private IDirtyStrategy? _pendingDirtyStrategy; - /// 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. + /// Sets the directory-level dirty strategy on the underlying OS-level strategy for differential patch updates. + /// Safe to call before or after Create(). If called before, the strategy is cached and applied when Create() resolves _osStrategy. public void SetDirtyStrategy(IDirtyStrategy? dirtyStrategy) { if (_osStrategy is AbstractStrategy abs) @@ -105,6 +105,13 @@ public void SetDirtyStrategy(IDirtyStrategy? dirtyStrategy) _pendingDirtyStrategy = dirtyStrategy; } + /// Sets the file-level binary differ on the underlying OS-level strategy. + public void SetBinaryDiffer(IBinaryDiffer? binaryDiffer) + { + if (_osStrategy is AbstractStrategy abs) + abs.BinaryDiffer = binaryDiffer; + } + public void StartApp() { _osStrategy?.StartApp(); diff --git a/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs b/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs index 3d885f3a..637053ca 100644 --- a/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs +++ b/src/c#/GeneralUpdate.Differential/Abstractions/IBinaryDiffer.cs @@ -1,20 +1,16 @@ -// IBinaryDiffer has been moved to GeneralUpdate.Core.Differential. -// This file provides a backward-compatible type alias. -// New code should reference GeneralUpdate.Core.Differential.IBinaryDiffer directly. - +using System.Threading; +using System.Threading.Tasks; using CoreBinaryDiffer = GeneralUpdate.Core.Differential.IBinaryDiffer; -namespace GeneralUpdate.Differential.Abstractions +namespace GeneralUpdate.Differential.Abstractions; + +/// +/// Binary differential algorithm with both patch generation and application. +/// Extends (DirtyAsync) with CleanAsync. +/// +public interface IBinaryDiffer : CoreBinaryDiffer { - /// - /// Binary differential algorithm abstraction. - /// - /// - /// Migration note: This interface is an alias for - /// . Use - /// using GeneralUpdate.Core.Differential; directly in new code. - /// - public interface IBinaryDiffer : CoreBinaryDiffer - { - } + /// Generates a patch: oldFile vs newFile → patchFile. + Task CleanAsync(string oldFilePath, string newFilePath, string patchFilePath, + CancellationToken cancellationToken = default); } From aa982832b5a94f732e8a7fd5a595eba4053d9ac9 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Tue, 26 May 2026 16:14:41 +0800 Subject: [PATCH 6/6] docs: fix PatchMiddleware XML comment for IDirtyStrategy --- src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs index 13615104..873d742c 100644 --- a/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs +++ b/src/c#/GeneralUpdate.Core/Pipeline/PatchMiddleware.cs @@ -10,9 +10,9 @@ 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 resolved from -/// (key "BinaryDiffer"), set by -/// when the differ is injected via +/// The implementation is resolved from +/// (key "DirtyStrategy"), set by +/// when the differ is injected via /// Bootstrap.BinaryDiffer<T>(). Without injection, patches are skipped. /// public class PatchMiddleware : IMiddleware