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 ab6ddcda..4a171417 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; @@ -130,6 +131,17 @@ private async Task LaunchWithStrategy(IStrategy roleStra 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) + { + if (roleStrategy is ClientUpdateStrategy cs2) + cs2.SetDiffer(differ); + else if (roleStrategy is UpgradeUpdateStrategy us2) + us2.SetDiffer(differ); + } + // Check custom skip condition before executing update if (_customSkipOption?.Invoke() == true) { @@ -161,8 +173,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..bafad077 100644 --- a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs @@ -67,7 +67,16 @@ public async Task ExecuteAsync( var tasks = plan.Assets.Select(async asset => { - await sem.WaitAsync(token).ConfigureAwait(false); + var acquired = await sem.WaitAsync(TimeSpan.FromMinutes(5), token).ConfigureAwait(false); + if (!acquired) + { + 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 { 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..bc45cf38 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..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,6 +75,18 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } + 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() { _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..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,6 +92,18 @@ public void Execute() ExecuteAsync().GetAwaiter().GetResult(); } + 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() { _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);