diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 80505796..892e3aa9 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -14,6 +14,8 @@ using GeneralUpdate.Core.JsonContext; using GeneralUpdate.Core.Strategy; using GeneralUpdate.Core.Network; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Download.Reporting; namespace GeneralUpdate.Core; @@ -63,15 +65,31 @@ private async Task LaunchWithStrategy(IStrategy roleStra { ApplyRuntimeOptions(); + // Resolve hooks and reporter from extensions + var hooks = ResolveExtension() ?? new Hooks.NoOpUpdateHooks(); + var reporter = ResolveExtension() ?? new Download.Reporting.NoOpUpdateReporter(); + // Configure client-specific callbacks if (roleStrategy is ClientUpdateStrategy clientStrat) { + clientStrat.Hooks = hooks; + clientStrat.Reporter = reporter; if (_updatePrecheck != null) clientStrat.UseUpdatePrecheck(_updatePrecheck); foreach (var opt in _customOptions) clientStrat.UseCustomOption(opt); await CallSmallBowlHomeAsync(_configInfo.Bowl).ConfigureAwait(false); } + else if (roleStrategy is UpgradeUpdateStrategy upgradeStrat) + { + upgradeStrat.Hooks = hooks; + upgradeStrat.Reporter = reporter; + } + else if (roleStrategy is OSSUpdateStrategy ossStrat) + { + ossStrat.Hooks = hooks; + ossStrat.Reporter = reporter; + } roleStrategy.Create(_configInfo); await roleStrategy.ExecuteAsync(); diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs index deb66ecc..fdf76768 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs @@ -34,6 +34,11 @@ public class ClientUpdateStrategy : IStrategy private readonly Download.Abstractions.IDownloadOrchestrator? _orchestrator; private readonly DiffMode _diffMode = DiffMode.Serial; + /// 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(); + public ClientUpdateStrategy(Download.Abstractions.IDownloadOrchestrator? orchestrator = null) { _orchestrator = orchestrator; } public void Create(GlobalConfigInfo parameter) @@ -55,6 +60,9 @@ public async Task ExecuteAsync() } catch (Exception ex) { + var errCtx = BuildUpdateContext(); + await SafeOnUpdateErrorAsync(errCtx, ex).ConfigureAwait(false); + await SafeReportUpdateFailedAsync(errCtx, ex).ConfigureAwait(false); GeneralTracer.Error("ClientUpdateStrategy.ExecuteAsync failed.", ex); EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); } @@ -88,7 +96,7 @@ public ClientUpdateStrategy UseCustomOption(Func func) private async Task ExecuteWorkflowAsync() { - // Silent mode ! delegate to SilentUpdateMode + // Silent mode 鐃緒申 delegate to SilentUpdateMode // (encoding/format/timeout are read from _configInfo) var defaultEncoding = Encoding.UTF8; var defaultTimeout = 60; @@ -125,6 +133,17 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout) return; } + // Hooks: allow cancellation before download + var hooksCtx = BuildUpdateContext(); + if (!await SafeOnBeforeUpdateAsync(hooksCtx).ConfigureAwait(false)) + { + GeneralTracer.Info("ClientUpdateStrategy: update cancelled by OnBeforeUpdateAsync hook."); + return; + } + + // Report: update started + await SafeReportUpdateStartedAsync(hooksCtx).ConfigureAwait(false); + InitBlackList(); ApplyRuntimeOptions(encoding, timeout); @@ -167,18 +186,22 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout) switch (_configInfo.IsUpgradeUpdate) { case true when _configInfo.IsMainUpdate: - GeneralTracer.Info("ClientUpdateStrategy: both upgrade+main ! downloading and executing."); + GeneralTracer.Info("ClientUpdateStrategy: both upgrade+main -- downloading and executing."); await DownloadAsync(); + await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); await _osStrategy.ExecuteAsync(); + await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false); _osStrategy.StartApp(); break; case true when !_configInfo.IsMainUpdate: - GeneralTracer.Info("ClientUpdateStrategy: upgrade-only ! downloading and executing."); + GeneralTracer.Info("ClientUpdateStrategy: upgrade-only -- downloading and executing."); await DownloadAsync(); + await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false); await _osStrategy.ExecuteAsync(); break; case false when _configInfo.IsMainUpdate: - GeneralTracer.Info("ClientUpdateStrategy: main-only ! starting updater."); + GeneralTracer.Info("ClientUpdateStrategy: main-only -- starting updater."); + await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false); _osStrategy.StartApp(); break; } @@ -288,5 +311,75 @@ private void ExecuteCustomOptions() } } + // + // Hooks & Reporter safe wrappers + // + + private Hooks.UpdateContext BuildUpdateContext() + { + return new Hooks.UpdateContext( + _configInfo?.AppName ?? "unknown", + _configInfo?.InstallPath ?? AppDomain.CurrentDomain.BaseDirectory, + _configInfo?.ClientVersion ?? "0.0.0", + _configInfo?.LastVersion, + AppType.ClientApp + ); + } + + 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; } + } + + private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx) + { + 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}"); } + } + + private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx) + { + try + { + await Reporter.ReportAsync(new Download.Reporting.UpdateReport( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.UpdateStarted, ctx.AppType, DateTimeOffset.UtcNow + )).ConfigureAwait(false); + } + 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( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.DownloadCompleted, ctx.AppType, DateTimeOffset.UtcNow + )).ConfigureAwait(false); + } + 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( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow, + ErrorMessage: error.Message + )).ConfigureAwait(false); + } + catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); } + } + #endregion } diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs index 13b4f7fb..5b316fd5 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -28,6 +28,11 @@ public class OSSUpdateStrategy : IStrategy private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; private const int TimeOut = 60; + /// 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(); + public void Create(GlobalConfigInfo parameter) { _configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter)); @@ -38,6 +43,7 @@ public async Task ExecuteAsync() if (_configInfo == null) throw new InvalidOperationException("OSSUpdateStrategy not configured. Call Create() first."); + var ctx = BuildUpdateContext(); try { var versionFileName = $"{_configInfo.MainAppName ?? _configInfo.AppName}_versions.json"; @@ -55,17 +61,35 @@ public async Task ExecuteAsync() versions = versions.OrderBy(v => v.PubTime).ToList(); + // Hooks: allow cancellation before download + if (!await SafeOnBeforeUpdateAsync(ctx).ConfigureAwait(false)) + { + GeneralTracer.Info("OSSUpdateStrategy: update cancelled by OnBeforeUpdateAsync hook."); + return; + } + + // Report: update started + await SafeReportUpdateStartedAsync(ctx).ConfigureAwait(false); + GeneralTracer.Debug($"OSSUpdateStrategy: 3. Downloading {versions.Count} version(s)."); await DownloadVersionsAsync(versions); GeneralTracer.Debug("OSSUpdateStrategy: 4. Decompressing packages."); Decompress(versions); + // Report: update applied + await SafeReportUpdateAppliedAsync(ctx).ConfigureAwait(false); + + // Hooks: before starting main app + await SafeOnBeforeStartAppAsync(ctx).ConfigureAwait(false); + GeneralTracer.Debug("OSSUpdateStrategy: 5. Launching main application."); StartApp(); } catch (Exception ex) { + await SafeOnUpdateErrorAsync(ctx, ex).ConfigureAwait(false); + await SafeReportUpdateFailedAsync(ctx, ex).ConfigureAwait(false); GeneralTracer.Error("OSSUpdateStrategy.ExecuteAsync failed.", ex); throw; } @@ -89,6 +113,8 @@ public void StartApp() GeneralTracer.Debug("OSSUpdateStrategy: application started."); } + #region Helpers + private async Task DownloadVersionsAsync(List versions) { var manager = new DownloadManager(_appPath, Format.ZIP, TimeOut); @@ -121,4 +147,76 @@ private void Decompress(List versions) File.Delete(zipFilePath); } } + + // + // Hooks & Reporter safe wrappers + // + + private Hooks.UpdateContext BuildUpdateContext() + { + return new Hooks.UpdateContext( + _configInfo?.AppName ?? "unknown", + _configInfo?.InstallPath ?? _appPath, + _configInfo?.ClientVersion ?? "0.0.0", + _configInfo?.LastVersion, + AppType.OSSApp + ); + } + + 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; } + } + + private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx) + { + 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}"); } + } + + private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx) + { + try + { + await Reporter.ReportAsync(new Download.Reporting.UpdateReport( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.UpdateStarted, ctx.AppType, DateTimeOffset.UtcNow + )).ConfigureAwait(false); + } + catch (Exception ex) { GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); } + } + + private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx) + { + try + { + await Reporter.ReportAsync(new Download.Reporting.UpdateReport( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.UpdateApplied, ctx.AppType, DateTimeOffset.UtcNow + )).ConfigureAwait(false); + } + catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); } + } + + private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error) + { + try + { + await Reporter.ReportAsync(new Download.Reporting.UpdateReport( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow, + ErrorMessage: error.Message + )).ConfigureAwait(false); + } + catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); } + } + + #endregion } diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs index 3b9ae7ca..33240a75 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs @@ -25,6 +25,11 @@ public class UpgradeUpdateStrategy : IStrategy private GlobalConfigInfo? _configInfo; private IStrategy? _osStrategy; + /// 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(); + public void Create(GlobalConfigInfo parameter) { _configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter)); @@ -35,14 +40,22 @@ public async Task ExecuteAsync() { if (_configInfo == null) throw new InvalidOperationException("UpgradeUpdateStrategy not configured."); + var ctx = BuildUpdateContext(); try { GeneralTracer.Debug("UpgradeUpdateStrategy.ExecuteAsync start."); + // Hooks: allow cancellation before applying updates + if (!await SafeOnBeforeUpdateAsync(ctx).ConfigureAwait(false)) + { + GeneralTracer.Info("UpgradeUpdateStrategy: update cancelled by OnBeforeUpdateAsync hook."); + return; + } + ApplyRuntimeOptions(); _osStrategy!.Create(_configInfo); - // Apply updates via OS-specific pipeline (Hash Compress Patch) + // Apply updates via OS-specific pipeline (Hash -> Compress -> Patch) if (_configInfo.UpdateVersions?.Count > 0) { GeneralTracer.Info($"UpgradeUpdateStrategy: applying {_configInfo.UpdateVersions.Count} update(s)."); @@ -53,10 +66,21 @@ public async Task ExecuteAsync() GeneralTracer.Info("UpgradeUpdateStrategy: no updates to apply, starting application directly."); } + // Hooks: after all updates applied + await SafeOnAfterUpdateAsync(ctx).ConfigureAwait(false); + + // Report: update applied successfully + await SafeReportUpdateAppliedAsync(ctx).ConfigureAwait(false); + + // Hooks: before starting main app (e.g. chmod +x on Linux/macOS) + await SafeOnBeforeStartAppAsync(ctx).ConfigureAwait(false); + _osStrategy.StartApp(); } catch (Exception ex) { + await SafeOnUpdateErrorAsync(ctx, ex).ConfigureAwait(false); + await SafeReportUpdateFailedAsync(ctx, ex).ConfigureAwait(false); GeneralTracer.Error("UpgradeUpdateStrategy.ExecuteAsync failed.", ex); EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); } @@ -91,5 +115,69 @@ private void ApplyRuntimeOptions() _configInfo.Format = Format.ZIP; } + // + // Hooks & Reporter safe wrappers + // + + private Hooks.UpdateContext BuildUpdateContext() + { + return new Hooks.UpdateContext( + _configInfo?.AppName ?? "unknown", + _configInfo?.InstallPath ?? AppDomain.CurrentDomain.BaseDirectory, + _configInfo?.ClientVersion ?? "0.0.0", + _configInfo?.LastVersion, + AppType.UpgradeApp + ); + } + + 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; } + } + + private async Task SafeOnAfterUpdateAsync(Hooks.UpdateContext ctx) + { + try { await Hooks.OnAfterUpdateAsync(ctx).ConfigureAwait(false); } + catch (Exception ex) { GeneralTracer.Warn($"OnAfterUpdateAsync hook failed: {ex.Message}"); } + } + + private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx) + { + 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}"); } + } + + private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx) + { + try + { + await Reporter.ReportAsync(new Download.Reporting.UpdateReport( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.UpdateApplied, ctx.AppType, DateTimeOffset.UtcNow + )).ConfigureAwait(false); + } + catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); } + } + + private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error) + { + try + { + await Reporter.ReportAsync(new Download.Reporting.UpdateReport( + ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion, + Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow, + ErrorMessage: error.Message + )).ConfigureAwait(false); + } + catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); } + } + #endregion }