diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index f8c0c055..4c2f65e9 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -15,6 +15,7 @@ using GeneralUpdate.Core.JsonContext; using GeneralUpdate.Core.Strategy; using GeneralUpdate.Core.Network; +using GeneralUpdate.Core.Security; using GeneralUpdate.Core.Hooks; using GeneralUpdate.Core.Ipc; using GeneralUpdate.Core.Download.Reporting; @@ -98,12 +99,37 @@ private async Task LaunchWithStrategy(IStrategy roleStra var reporter = ResolveExtension() ?? new Download.Reporting.NoOpUpdateReporter(); + // ── Network-level extensions (global, applied before any HTTP call) ── + var sslPolicy = ResolveExtension(); + if (sslPolicy != null) Network.VersionService.SetSslValidationPolicy(sslPolicy); + var authProvider = ResolveExtension(); + if (authProvider != null) Network.VersionService.SetDefaultAuthProvider(authProvider); + // ── Phase 1: inject all dependencies before Create ── roleStrategy.Hooks = hooks; roleStrategy.Reporter = reporter; var binaryDiffer = ResolveExtension(); var dirtyStrategy = ResolveExtension(); + var downloadOrchestrator = ResolveExtension(); + var downloadPolicy = ResolveExtension(); + var downloadExecutor = ResolveExtension(); + + // Build download pipeline factory from registered extension type. + // Tries a string constructor first (for passing the expected hash, à la DefaultDownloadPipeline), + // falls back to parameterless constructor otherwise. + Func? downloadPipelineFactory = null; + var downloadPipeline = ResolveExtension(); + if (downloadPipeline != null) + { + var pipelineType = downloadPipeline.GetType(); + var stringCtor = pipelineType.GetConstructor([typeof(string)]); + if (stringCtor != null) + downloadPipelineFactory = hash => (Download.Abstractions.IDownloadPipeline)stringCtor.Invoke([hash]); + else + downloadPipelineFactory = _ => (Download.Abstractions.IDownloadPipeline)Activator.CreateInstance(pipelineType); + } + var diffPipeline = BuildDiffPipeline(); switch (roleStrategy) @@ -119,15 +145,36 @@ private async Task LaunchWithStrategy(IStrategy roleStra if (binaryDiffer != null) cs.SetBinaryDiffer(binaryDiffer); if (dirtyStrategy != null) cs.SetDirtyStrategy(dirtyStrategy); cs.SetDiffPipeline(diffPipeline); + if (downloadOrchestrator != null) cs.SetOrchestrator(downloadOrchestrator); + if (downloadPolicy != null) cs.SetDownloadPolicy(downloadPolicy); + if (downloadExecutor != null) cs.SetDownloadExecutor(downloadExecutor); + if (downloadPipelineFactory != null) cs.SetDownloadPipelineFactory(downloadPipelineFactory); break; case UpgradeUpdateStrategy us: - if (binaryDiffer != null) us.SetBinaryDiffer(binaryDiffer); - if (dirtyStrategy != null) us.SetDirtyStrategy(dirtyStrategy); + if (binaryDiffer != null) + us.SetBinaryDiffer(binaryDiffer); + if (dirtyStrategy != null) + us.SetDirtyStrategy(dirtyStrategy); us.SetDiffPipeline(diffPipeline); break; } + // Inject custom OS-level strategy if registered via Strategy() + var customOsStrategy = ResolveExtension(); + if (customOsStrategy != null) + { + switch (roleStrategy) + { + case ClientUpdateStrategy cs: + cs.SetOsStrategy(customOsStrategy); + break; + case UpgradeUpdateStrategy us: + us.SetOsStrategy(customOsStrategy); + break; + } + } + roleStrategy.Create(_configInfo); // Check custom skip condition before executing update @@ -316,9 +363,15 @@ private async Task LaunchSilentAsync() var reporter = ResolveExtension() ?? new Download.Reporting.NoOpUpdateReporter(); + var sslPolicy = ResolveExtension(); + if (sslPolicy != null) Network.VersionService.SetSslValidationPolicy(sslPolicy); + var authProvider = ResolveExtension(); + if (authProvider != null) Network.VersionService.SetDefaultAuthProvider(authProvider); + var orchestrator = new Silent.SilentPollOrchestrator(_configInfo, silentOptions) .WithHooks(hooks) - .WithReporter(reporter); + .WithReporter(reporter) + .WithOsStrategy(ResolveExtension()); await orchestrator.StartAsync().ConfigureAwait(false); GeneralTracer.Info("GeneralUpdateBootstrap: silent update mode started, returning to caller."); diff --git a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs index 2a364b0e..74819139 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs @@ -66,18 +66,22 @@ protected T GetOption(UpdateOption? option) return (TBootstrap)this; } - public TBootstrap PipelineFactory() where T : Pipeline.IUpdatePipelineFactory, new() - { - _extensions[typeof(Pipeline.IUpdatePipelineFactory)] = typeof(T); - return (TBootstrap)this; - } - + /// + /// Registers a custom retry/timeout policy for download operations. + /// Only effective when using the default download orchestrator. + /// If is also set, this is ignored. + /// public TBootstrap DownloadPolicy() where T : Download.Abstractions.IDownloadPolicy, new() { _extensions[typeof(Download.Abstractions.IDownloadPolicy)] = typeof(T); return (TBootstrap)this; } + /// + /// Registers a custom single-file download executor (e.g. for non-HTTP protocols). + /// Only effective when using the default download orchestrator. + /// If is also set, this is ignored. + /// public TBootstrap DownloadExecutor() where T : Download.Abstractions.IDownloadExecutor, new() { _extensions[typeof(Download.Abstractions.IDownloadExecutor)] = typeof(T); @@ -90,6 +94,11 @@ protected T GetOption(UpdateOption? option) return (TBootstrap)this; } + /// + /// Registers a custom post-download processing pipeline (hash verification, decryption, virus scan). + /// Only effective when using the default download orchestrator. + /// If is also set, this is ignored. + /// public TBootstrap DownloadPipeline() where T : Download.Abstractions.IDownloadPipeline, new() { _extensions[typeof(Download.Abstractions.IDownloadPipeline)] = typeof(T); @@ -108,18 +117,18 @@ protected T GetOption(UpdateOption? option) return (TBootstrap)this; } + /// + /// Registers a custom download orchestrator that handles batch downloads end-to-end. + /// This is the top-level download abstraction. When set, , + /// , and are ignored + /// — the custom orchestrator owns the entire download pipeline. + /// public TBootstrap DownloadOrchestrator() where T : Download.Abstractions.IDownloadOrchestrator, new() { _extensions[typeof(Download.Abstractions.IDownloadOrchestrator)] = typeof(T); return (TBootstrap)this; } - public TBootstrap CleanStrategy() where T : Differential.ICleanStrategy, new() - { - _extensions[typeof(Differential.ICleanStrategy)] = typeof(T); - return (TBootstrap)this; - } - public TBootstrap DirtyStrategy() where T : Differential.IDirtyStrategy, new() { _extensions[typeof(Differential.IDirtyStrategy)] = typeof(T); diff --git a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs index fa1cdac4..a2c27261 100644 --- a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs @@ -28,12 +28,21 @@ public class DefaultDownloadOrchestrator : IDownloadOrchestrator private readonly HttpClient _httpClient; private readonly IDownloadPolicy _policy; private readonly DownloadOrchestratorOptions _options; - - public DefaultDownloadOrchestrator(HttpClient httpClient, DownloadOrchestratorOptions? options = null, IDownloadPolicy? policy = null) + private readonly IDownloadExecutor? _customExecutor; + private readonly Func? _pipelineFactory; + + public DefaultDownloadOrchestrator( + HttpClient httpClient, + DownloadOrchestratorOptions? options = null, + IDownloadPolicy? policy = null, + IDownloadExecutor? executor = null, + Func? pipelineFactory = null) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _options = options ?? new DownloadOrchestratorOptions(); _policy = policy ?? new DefaultRetryPolicy(_options.RetryCount, _options.RetryInterval); + _customExecutor = executor; + _pipelineFactory = pipelineFactory; } /// Execute downloads for all assets in the plan. @@ -82,8 +91,8 @@ public async Task ExecuteAsync( var fileName = GetFileName(asset); var destPath = Path.Combine(destDir, fileName); - var executor = new HttpDownloadExecutor(_httpClient, _options.DownloadTimeout, _options.EnableResume); - var pipeline = new DefaultDownloadPipeline(asset.SHA256); + var executor = _customExecutor ?? new HttpDownloadExecutor(_httpClient, _options.DownloadTimeout, _options.EnableResume); + var pipeline = _pipelineFactory?.Invoke(asset.SHA256) ?? new DefaultDownloadPipeline(asset.SHA256); var result = await _policy.ExecuteAsync(async ct => { diff --git a/src/c#/GeneralUpdate.Core/Network/VersionService.cs b/src/c#/GeneralUpdate.Core/Network/VersionService.cs index 2825212e..3a1fb946 100644 --- a/src/c#/GeneralUpdate.Core/Network/VersionService.cs +++ b/src/c#/GeneralUpdate.Core/Network/VersionService.cs @@ -19,6 +19,7 @@ public class VersionService { private static readonly HttpClient _sharedClient; private static ISslValidationPolicy _globalSslPolicy = new StrictSslValidationPolicy(); + private static IHttpAuthProvider? _globalAuthProvider; private readonly IHttpAuthProvider _auth; private readonly TimeSpan _timeout; @@ -34,6 +35,9 @@ static VersionService() public static void SetSslValidationPolicy(ISslValidationPolicy policy) => _globalSslPolicy = policy ?? throw new ArgumentNullException(nameof(policy)); + public static void SetDefaultAuthProvider(IHttpAuthProvider? provider) + => _globalAuthProvider = provider; + private static bool SharedCertValidation(HttpRequestMessage m, X509Certificate2? c, X509Chain? ch, SslPolicyErrors e) => _globalSslPolicy.ValidateCertificate(c, ch, e); @@ -48,7 +52,7 @@ public VersionService(IHttpAuthProvider? auth = null, TimeSpan? timeout = null, public static Task Report(string url, int recordId, int status, int? type, string scheme = null, string token = null, CancellationToken ct = default) { - var a = HttpAuthProviderFactory.Create(scheme, token, null); + var a = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, null); return new VersionService(a).ReportAsync(url, recordId, status, type, ct); } @@ -57,7 +61,7 @@ public static Task Validate(string url, string version, AppType appType, string appKey, PlatformType platform, string productId, string scheme = null, string token = null, CancellationToken ct = default) { - var a = HttpAuthProviderFactory.Create(scheme, token, appKey); + var a = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, appKey); return new VersionService(a).ValidateAsync(url, version, (int)appType, appKey, (int)platform, productId, ct); } diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs index 72a04563..767047f9 100644 --- a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs +++ b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs @@ -39,6 +39,7 @@ public class SilentPollOrchestrator : IDisposable private int _updaterStarted; private IUpdateHooks? _hooks; private IUpdateReporter? _reporter; + private IStrategy? _customOsStrategy; private Configuration.ProcessInfo? _preparedProcessInfo; private List _clientVersions = new(); @@ -50,6 +51,7 @@ public SilentPollOrchestrator(GlobalConfigInfo configInfo, SilentOptions options public SilentPollOrchestrator WithHooks(IUpdateHooks? hooks) { _hooks = hooks; return this; } public SilentPollOrchestrator WithReporter(IUpdateReporter? reporter) { _reporter = reporter; return this; } + public SilentPollOrchestrator WithOsStrategy(IStrategy? strategy) { _customOsStrategy = strategy; return this; } public Task StartAsync() { @@ -335,8 +337,9 @@ private void InitBlackList() StorageManager.BlackListMatcher = new DefaultBlackListMatcher(effectiveConfig); } - private static IStrategy CreateStrategy() + private IStrategy CreateStrategy() { + if (_customOsStrategy != null) return _customOsStrategy; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return new WindowsStrategy(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return new LinuxStrategy(); if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return new MacStrategy(); diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index dbaa4828..6c69703b 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -34,8 +34,12 @@ public class ClientUpdateStrategy : IStrategy { private GlobalConfigInfo? _configInfo; private IStrategy? _osStrategy; + private IStrategy? _customOsStrategy; private Func? _updatePrecheck; - private readonly Download.Abstractions.IDownloadOrchestrator? _orchestrator; + private Download.Abstractions.IDownloadOrchestrator? _orchestrator; + private Download.Abstractions.IDownloadPolicy? _customDownloadPolicy; + private Download.Abstractions.IDownloadExecutor? _customDownloadExecutor; + private Func? _customDownloadPipelineFactory; private int _mainRecordId; /// Which side(s) need updating, determined by server validation. @@ -49,12 +53,30 @@ private enum UpdateScenario /// Lifecycle hooks injected by the bootstrap. public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks(); + /// Update status reporter injected by the bootstrap. public Download.Reporting.IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter(); + /// Download source (e.g., HTTP, SignalR Hub). Injected by bootstrap via HubConfig or extension registry (.DownloadSource<T>()). public Download.Abstractions.IDownloadSource? DownloadSource { get; set; } - public ClientUpdateStrategy(Download.Abstractions.IDownloadOrchestrator? orchestrator = null) { _orchestrator = orchestrator; } + public ClientUpdateStrategy() { } + + /// Sets a custom OS-level strategy (injected via .Strategy<T>()). + /// When set, this replaces the automatic platform detection in . + public void SetOsStrategy(IStrategy? strategy) => _customOsStrategy = strategy; + + /// Sets a custom download orchestrator (injected via .DownloadOrchestrator<T>()). + public void SetOrchestrator(Download.Abstractions.IDownloadOrchestrator? orchestrator) => _orchestrator = orchestrator; + + /// Sets a custom download retry policy (injected via .DownloadPolicy<T>()). + public void SetDownloadPolicy(Download.Abstractions.IDownloadPolicy? policy) => _customDownloadPolicy = policy; + + /// Sets a custom download executor (injected via .DownloadExecutor<T>()). + public void SetDownloadExecutor(Download.Abstractions.IDownloadExecutor? executor) => _customDownloadExecutor = executor; + + /// Sets a custom download pipeline factory (injected via .DownloadPipeline<T>()). + public void SetDownloadPipelineFactory(Func? factory) => _customDownloadPipelineFactory = factory; public void Create(GlobalConfigInfo parameter) { @@ -145,7 +167,8 @@ private async Task ExecuteWorkflowAsync() private async Task ExecuteStandardWorkflowAsync() { - GeneralTracer.Info($"ClientUpdateStrategy: validating client={_configInfo!.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}"); + GeneralTracer.Info( + $"ClientUpdateStrategy: validating client={_configInfo!.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}"); // Use injected DownloadSource (Hub/HTTP), or default to HttpDownloadSource var downloadSource = DownloadSource ?? new Download.Sources.HttpDownloadSource( @@ -172,9 +195,9 @@ private async Task ExecuteStandardWorkflowAsync() var scenario = (_configInfo.IsMainUpdate, _configInfo.IsUpgradeUpdate) switch { (false, false) => UpdateScenario.None, - (false, true) => UpdateScenario.UpgradeOnly, - (true, false) => UpdateScenario.MainOnly, - (true, true) => UpdateScenario.Both, + (false, true) => UpdateScenario.UpgradeOnly, + (true, false) => UpdateScenario.MainOnly, + (true, true) => UpdateScenario.Both, }; GeneralTracer.Info($"ClientUpdateStrategy: Scenario={scenario}, AssetCount={downloadPlan.Assets.Count}"); @@ -194,14 +217,14 @@ private async Task ExecuteStandardWorkflowAsync() IsCrossVersion = a.IsCrossVersion, FromVersion = a.FromVersion }).ToList(); - + var versionResp = new VersionRespDTO { Code = versionInfos.Count > 0 ? 200 : 404, Body = versionInfos, Message = versionInfos.Count > 0 ? $"Found {versionInfos.Count} update(s)." : "No updates available." }; - + var updateInfoArgs = new UpdateInfoEventArgs(versionResp); // Capture the first RecordId for status reporting to GeneralSpacestation @@ -241,7 +264,8 @@ private async Task ExecuteStandardWorkflowAsync() // Check failed version if (!string.IsNullOrEmpty(_configInfo.LastVersion) && CheckFail(_configInfo.LastVersion)) { - GeneralTracer.Warn($"ClientUpdateStrategy: version {_configInfo.LastVersion} matches known-failed upgrade."); + GeneralTracer.Warn( + $"ClientUpdateStrategy: version {_configInfo.LastVersion} matches known-failed upgrade."); return; } @@ -270,10 +294,14 @@ private async Task ExecuteStandardWorkflowAsync() var httpClient = GeneralUpdate.Core.Network.HttpClientProvider.Shared; try { - var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator(httpClient, orchOptions); + var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator( + httpClient, orchOptions, _customDownloadPolicy, + _customDownloadExecutor, _customDownloadPipelineFactory); await orchestrator.ExecuteAsync(downloadPlan, _configInfo.TempPath).ConfigureAwait(false); } - finally { } + finally + { + } } await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); @@ -292,7 +320,8 @@ private async Task ExecuteStandardWorkflowAsync() var upgradeVersions = downloadVersions.Where(v => v.AppType == (int)AppType.Upgrade).ToList(); var clientVersions = downloadVersions.Where(v => v.AppType == (int)AppType.Client).ToList(); - GeneralTracer.Info($"ClientUpdateStrategy: Upgrade packages={upgradeVersions.Count}, MainApp packages={clientVersions.Count}"); + GeneralTracer.Info( + $"ClientUpdateStrategy: Upgrade packages={upgradeVersions.Count}, MainApp packages={clientVersions.Count}"); // ── Dispatch by scenario — one switch, four states, zero nested if-else ── switch (scenario) @@ -356,7 +385,9 @@ private async Task LaunchUpgradeProcessAsync() abs.LaunchBowl = false; abs.UseUpdatePath = !string.IsNullOrWhiteSpace(_configInfo.UpdatePath); } - GeneralTracer.Info($"ClientUpdateStrategy: launching upgrade process {_configInfo!.UpdateAppName} via OS strategy."); + + GeneralTracer.Info( + $"ClientUpdateStrategy: launching upgrade process {_configInfo!.UpdateAppName} via OS strategy."); await _osStrategy!.StartAppAsync(); } @@ -364,8 +395,10 @@ private async Task LaunchUpgradeProcessAsync() #region Helpers - private static IStrategy ResolveOsStrategy() + private IStrategy ResolveOsStrategy() { + if (_customOsStrategy != null) + return _customOsStrategy; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return new WindowsStrategy(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -380,14 +413,17 @@ private void InitBlackList() var effectiveConfig = new BlackListConfig( _configInfo!.BlackFiles?.Count > 0 ? _configInfo.BlackFiles : BlackListDefaults.DefaultBlackFiles, _configInfo.BlackFormats?.Count > 0 ? _configInfo.BlackFormats : BlackListDefaults.DefaultBlackFormats, - _configInfo.SkipDirectorys?.Count > 0 ? _configInfo.SkipDirectorys : BlackListDefaults.DefaultSkipDirectories + _configInfo.SkipDirectorys?.Count > 0 + ? _configInfo.SkipDirectorys + : BlackListDefaults.DefaultSkipDirectories ); StorageManager.BlackListMatcher = new DefaultBlackListMatcher(effectiveConfig); } private void Backup() { - GeneralTracer.Info($"ClientUpdateStrategy: backing up {_configInfo!.InstallPath} -> {_configInfo.BackupDirectory}"); + GeneralTracer.Info( + $"ClientUpdateStrategy: backing up {_configInfo!.InstallPath} -> {_configInfo.BackupDirectory}"); StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory, _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories); } @@ -450,26 +486,51 @@ private Hooks.UpdateContext BuildUpdateContext() private async Task SafeOnBeforeUpdateAsync(Hooks.UpdateContext ctx) { - try { return await Hooks.OnBeforeUpdateAsync(ctx).ConfigureAwait(false); } - catch (Exception ex) { GeneralTracer.Warn($"OnBeforeUpdateAsync hook failed: {ex.Message}"); return true; } + try + { + return await Hooks.OnBeforeUpdateAsync(ctx).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"OnBeforeUpdateAsync hook failed: {ex.Message}"); + return true; + } } private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx) { - try { await Hooks.OnBeforeStartAppAsync(ctx).ConfigureAwait(false); } - catch (Exception ex) { GeneralTracer.Warn($"OnBeforeStartAppAsync hook failed: {ex.Message}"); } + try + { + await Hooks.OnBeforeStartAppAsync(ctx).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"OnBeforeStartAppAsync hook failed: {ex.Message}"); + } } private async Task SafeOnUpdateErrorAsync(Hooks.UpdateContext ctx, Exception error) { - try { await Hooks.OnUpdateErrorAsync(ctx, error).ConfigureAwait(false); } - catch (Exception ex) { GeneralTracer.Warn($"OnUpdateErrorAsync hook failed: {ex.Message}"); } + try + { + await Hooks.OnUpdateErrorAsync(ctx, error).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"OnUpdateErrorAsync hook failed: {ex.Message}"); + } } private async Task SafeOnAfterUpdateAsync(Hooks.UpdateContext ctx) { - try { await Hooks.OnAfterUpdateAsync(ctx).ConfigureAwait(false); } - catch (Exception ex) { GeneralTracer.Warn($"OnAfterUpdateAsync hook failed: {ex.Message}"); } + try + { + await Hooks.OnAfterUpdateAsync(ctx).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"OnAfterUpdateAsync hook failed: {ex.Message}"); + } } private async Task SafeOnDownloadCompletedAsync(Hooks.UpdateContext ctx) @@ -482,44 +543,67 @@ private async Task SafeOnDownloadCompletedAsync(Hooks.UpdateContext ctx) 0, TimeSpan.Zero, _configInfo?.TempPath, true); await Hooks.OnDownloadCompletedAsync(downloadCtx).ConfigureAwait(false); } - catch (Exception ex) { GeneralTracer.Warn($"OnDownloadCompletedAsync hook failed: {ex.Message}"); } + catch (Exception ex) + { + GeneralTracer.Warn($"OnDownloadCompletedAsync hook failed: {ex.Message}"); + } } private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx) { try { - await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Updating, 1)).ConfigureAwait(false); + await Reporter + .ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, + (int)Download.Reporting.UpdateStatus.Updating, 1)).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); } - catch (Exception ex) { GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); } } private async Task SafeReportDownloadCompletedAsync(Hooks.UpdateContext ctx) { try { - await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Updating, 1)).ConfigureAwait(false); + await Reporter + .ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, + (int)Download.Reporting.UpdateStatus.Updating, 1)).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"Report DownloadCompleted failed: {ex.Message}"); } - catch (Exception ex) { GeneralTracer.Warn($"Report DownloadCompleted failed: {ex.Message}"); } } private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error) { try { - await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Failure, 1)).ConfigureAwait(false); + await Reporter + .ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, + (int)Download.Reporting.UpdateStatus.Failure, 1)).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); } - catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); } } private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx) { try { - await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Success, 1)).ConfigureAwait(false); + await Reporter + .ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, + (int)Download.Reporting.UpdateStatus.Success, 1)).ConfigureAwait(false); + } + catch (Exception ex) + { + GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); } - catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); } } #endregion -} +} \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index f413288b..7ee248fc 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -27,6 +27,7 @@ public class UpgradeUpdateStrategy : IStrategy { private GlobalConfigInfo? _configInfo; private IStrategy? _osStrategy; + private IStrategy? _customOsStrategy; /// Lifecycle hooks injected by the bootstrap. public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks(); @@ -34,6 +35,10 @@ public class UpgradeUpdateStrategy : IStrategy /// Update status reporter injected by the bootstrap. public Download.Reporting.IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter(); + /// Sets a custom OS-level strategy (injected via .Strategy<T>()). + /// When set, this replaces the automatic platform detection in . + public void SetOsStrategy(IStrategy? strategy) => _customOsStrategy = strategy; + public void Create(GlobalConfigInfo parameter) { _configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter)); @@ -151,8 +156,10 @@ public async Task StartAppAsync() #region Helpers - private static IStrategy ResolveOsStrategy() + private IStrategy ResolveOsStrategy() { + if (_customOsStrategy != null) + return _customOsStrategy; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return new WindowsStrategy(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs index 0132fcd8..3229fc0e 100644 --- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs @@ -110,11 +110,6 @@ public bool ValidateCertificate(System.Security.Cryptography.X509Certificates.X5 } - 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); @@ -159,11 +154,6 @@ private sealed class StubDownloadOrchestrator : GeneralUpdate.Core.Download.Abst => 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; @@ -173,14 +163,12 @@ private sealed class StubDirtyStrategy : GeneralUpdate.Core.Differential.IDirtyS [Fact] public void Inject_Strategy() => Assert.NotNull(B().Strategy()); [Fact] public void Inject_SslPolicy() => Assert.NotNull(B().SslPolicy()); [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_DownloadOrchestrator() => Assert.NotNull(B().DownloadOrchestrator()); - [Fact] public void Inject_CleanStrategy() => Assert.NotNull(B().CleanStrategy()); [Fact] public void Chain_AllExtensionsInjected() @@ -194,11 +182,8 @@ public void Chain_AllExtensionsInjected() .DownloadPipeline() .DownloadOrchestrator() .DirtyStrategy() - .CleanStrategy() - .DirtyStrategy() .SslPolicy() .UpdateAuth() - .PipelineFactory() .Strategy(); Assert.NotNull(b); }