Skip to content

Commit 1c26de8

Browse files
authored
Batch 2: Wire IUpdateHooks and IUpdateReporter into strategy flows (#364)
- Resolve hooks and reporter in GeneralUpdateBootstrap.LaunchWithStrategy and inject into all three strategies (Client/Upgrade/OSS) - ClientUpdateStrategy: call hooks at key lifecycle points: OnBeforeUpdateAsync (can cancel), OnBeforeStartAppAsync, OnUpdateErrorAsync Report UpdateStarted, DownloadCompleted, UpdateFailed - UpgradeUpdateStrategy: call hooks at key lifecycle points: OnBeforeUpdateAsync, OnAfterUpdateAsync, OnBeforeStartAppAsync, OnUpdateErrorAsync Report UpdateApplied, UpdateFailed - OSSUpdateStrategy: call hooks at key lifecycle points: OnBeforeUpdateAsync, OnBeforeStartAppAsync, OnUpdateErrorAsync Report UpdateStarted, UpdateApplied, UpdateFailed - All hook/reporter calls use safe wrappers that log and swallow exceptions to prevent hooks from breaking the update flow Closes #363
1 parent 83ba93d commit 1c26de8

4 files changed

Lines changed: 302 additions & 5 deletions

File tree

src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
using GeneralUpdate.Core.JsonContext;
1515
using GeneralUpdate.Core.Strategy;
1616
using GeneralUpdate.Core.Network;
17+
using GeneralUpdate.Core.Hooks;
18+
using GeneralUpdate.Core.Download.Reporting;
1719

1820
namespace GeneralUpdate.Core;
1921

@@ -63,15 +65,31 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
6365
{
6466
ApplyRuntimeOptions();
6567

68+
// Resolve hooks and reporter from extensions
69+
var hooks = ResolveExtension<Hooks.IUpdateHooks>() ?? new Hooks.NoOpUpdateHooks();
70+
var reporter = ResolveExtension<Download.Reporting.IUpdateReporter>() ?? new Download.Reporting.NoOpUpdateReporter();
71+
6672
// Configure client-specific callbacks
6773
if (roleStrategy is ClientUpdateStrategy clientStrat)
6874
{
75+
clientStrat.Hooks = hooks;
76+
clientStrat.Reporter = reporter;
6977
if (_updatePrecheck != null)
7078
clientStrat.UseUpdatePrecheck(_updatePrecheck);
7179
foreach (var opt in _customOptions)
7280
clientStrat.UseCustomOption(opt);
7381
await CallSmallBowlHomeAsync(_configInfo.Bowl).ConfigureAwait(false);
7482
}
83+
else if (roleStrategy is UpgradeUpdateStrategy upgradeStrat)
84+
{
85+
upgradeStrat.Hooks = hooks;
86+
upgradeStrat.Reporter = reporter;
87+
}
88+
else if (roleStrategy is OSSUpdateStrategy ossStrat)
89+
{
90+
ossStrat.Hooks = hooks;
91+
ossStrat.Reporter = reporter;
92+
}
7593

7694
roleStrategy.Create(_configInfo);
7795
await roleStrategy.ExecuteAsync();

src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public class ClientUpdateStrategy : IStrategy
3434
private readonly Download.Abstractions.IDownloadOrchestrator? _orchestrator;
3535
private readonly DiffMode _diffMode = DiffMode.Serial;
3636

37+
/// <summary>Lifecycle hooks injected by the bootstrap.</summary>
38+
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
39+
/// <summary>Update status reporter injected by the bootstrap.</summary>
40+
public Download.Reporting.IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter();
41+
3742
public ClientUpdateStrategy(Download.Abstractions.IDownloadOrchestrator? orchestrator = null) { _orchestrator = orchestrator; }
3843

3944
public void Create(GlobalConfigInfo parameter)
@@ -55,6 +60,9 @@ public async Task ExecuteAsync()
5560
}
5661
catch (Exception ex)
5762
{
63+
var errCtx = BuildUpdateContext();
64+
await SafeOnUpdateErrorAsync(errCtx, ex).ConfigureAwait(false);
65+
await SafeReportUpdateFailedAsync(errCtx, ex).ConfigureAwait(false);
5866
GeneralTracer.Error("ClientUpdateStrategy.ExecuteAsync failed.", ex);
5967
EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message));
6068
}
@@ -88,7 +96,7 @@ public ClientUpdateStrategy UseCustomOption(Func<bool> func)
8896

8997
private async Task ExecuteWorkflowAsync()
9098
{
91-
// Silent mode ¡ª delegate to SilentUpdateMode
99+
// Silent mode �� delegate to SilentUpdateMode
92100
// (encoding/format/timeout are read from _configInfo)
93101
var defaultEncoding = Encoding.UTF8;
94102
var defaultTimeout = 60;
@@ -125,6 +133,17 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
125133
return;
126134
}
127135

136+
// Hooks: allow cancellation before download
137+
var hooksCtx = BuildUpdateContext();
138+
if (!await SafeOnBeforeUpdateAsync(hooksCtx).ConfigureAwait(false))
139+
{
140+
GeneralTracer.Info("ClientUpdateStrategy: update cancelled by OnBeforeUpdateAsync hook.");
141+
return;
142+
}
143+
144+
// Report: update started
145+
await SafeReportUpdateStartedAsync(hooksCtx).ConfigureAwait(false);
146+
128147
InitBlackList();
129148
ApplyRuntimeOptions(encoding, timeout);
130149

@@ -167,18 +186,22 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
167186
switch (_configInfo.IsUpgradeUpdate)
168187
{
169188
case true when _configInfo.IsMainUpdate:
170-
GeneralTracer.Info("ClientUpdateStrategy: both upgrade+main ¡ª downloading and executing.");
189+
GeneralTracer.Info("ClientUpdateStrategy: both upgrade+main -- downloading and executing.");
171190
await DownloadAsync();
191+
await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false);
172192
await _osStrategy.ExecuteAsync();
193+
await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false);
173194
_osStrategy.StartApp();
174195
break;
175196
case true when !_configInfo.IsMainUpdate:
176-
GeneralTracer.Info("ClientUpdateStrategy: upgrade-only ¡ª downloading and executing.");
197+
GeneralTracer.Info("ClientUpdateStrategy: upgrade-only -- downloading and executing.");
177198
await DownloadAsync();
199+
await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false);
178200
await _osStrategy.ExecuteAsync();
179201
break;
180202
case false when _configInfo.IsMainUpdate:
181-
GeneralTracer.Info("ClientUpdateStrategy: main-only ¡ª starting updater.");
203+
GeneralTracer.Info("ClientUpdateStrategy: main-only -- starting updater.");
204+
await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false);
182205
_osStrategy.StartApp();
183206
break;
184207
}
@@ -288,5 +311,75 @@ private void ExecuteCustomOptions()
288311
}
289312
}
290313

314+
// ════════════════════════════════════════════════════════════════
315+
// Hooks & Reporter safe wrappers
316+
// ════════════════════════════════════════════════════════════════
317+
318+
private Hooks.UpdateContext BuildUpdateContext()
319+
{
320+
return new Hooks.UpdateContext(
321+
_configInfo?.AppName ?? "unknown",
322+
_configInfo?.InstallPath ?? AppDomain.CurrentDomain.BaseDirectory,
323+
_configInfo?.ClientVersion ?? "0.0.0",
324+
_configInfo?.LastVersion,
325+
AppType.ClientApp
326+
);
327+
}
328+
329+
private async Task<bool> SafeOnBeforeUpdateAsync(Hooks.UpdateContext ctx)
330+
{
331+
try { return await Hooks.OnBeforeUpdateAsync(ctx).ConfigureAwait(false); }
332+
catch (Exception ex) { GeneralTracer.Warn($"OnBeforeUpdateAsync hook failed: {ex.Message}"); return true; }
333+
}
334+
335+
private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx)
336+
{
337+
try { await Hooks.OnBeforeStartAppAsync(ctx).ConfigureAwait(false); }
338+
catch (Exception ex) { GeneralTracer.Warn($"OnBeforeStartAppAsync hook failed: {ex.Message}"); }
339+
}
340+
341+
private async Task SafeOnUpdateErrorAsync(Hooks.UpdateContext ctx, Exception error)
342+
{
343+
try { await Hooks.OnUpdateErrorAsync(ctx, error).ConfigureAwait(false); }
344+
catch (Exception ex) { GeneralTracer.Warn($"OnUpdateErrorAsync hook failed: {ex.Message}"); }
345+
}
346+
347+
private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx)
348+
{
349+
try
350+
{
351+
await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
352+
ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
353+
Download.Reporting.UpdateEvent.UpdateStarted, ctx.AppType, DateTimeOffset.UtcNow
354+
)).ConfigureAwait(false);
355+
}
356+
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); }
357+
}
358+
359+
private async Task SafeReportDownloadCompletedAsync(Hooks.UpdateContext ctx)
360+
{
361+
try
362+
{
363+
await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
364+
ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
365+
Download.Reporting.UpdateEvent.DownloadCompleted, ctx.AppType, DateTimeOffset.UtcNow
366+
)).ConfigureAwait(false);
367+
}
368+
catch (Exception ex) { GeneralTracer.Warn($"Report DownloadCompleted failed: {ex.Message}"); }
369+
}
370+
371+
private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error)
372+
{
373+
try
374+
{
375+
await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
376+
ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
377+
Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow,
378+
ErrorMessage: error.Message
379+
)).ConfigureAwait(false);
380+
}
381+
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); }
382+
}
383+
291384
#endregion
292385
}

src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public class OSSUpdateStrategy : IStrategy
2828
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
2929
private const int TimeOut = 60;
3030

31+
/// <summary>Lifecycle hooks injected by the bootstrap.</summary>
32+
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
33+
/// <summary>Update status reporter injected by the bootstrap.</summary>
34+
public Download.Reporting.IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter();
35+
3136
public void Create(GlobalConfigInfo parameter)
3237
{
3338
_configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter));
@@ -38,6 +43,7 @@ public async Task ExecuteAsync()
3843
if (_configInfo == null)
3944
throw new InvalidOperationException("OSSUpdateStrategy not configured. Call Create() first.");
4045

46+
var ctx = BuildUpdateContext();
4147
try
4248
{
4349
var versionFileName = $"{_configInfo.MainAppName ?? _configInfo.AppName}_versions.json";
@@ -55,17 +61,35 @@ public async Task ExecuteAsync()
5561

5662
versions = versions.OrderBy(v => v.PubTime).ToList();
5763

64+
// Hooks: allow cancellation before download
65+
if (!await SafeOnBeforeUpdateAsync(ctx).ConfigureAwait(false))
66+
{
67+
GeneralTracer.Info("OSSUpdateStrategy: update cancelled by OnBeforeUpdateAsync hook.");
68+
return;
69+
}
70+
71+
// Report: update started
72+
await SafeReportUpdateStartedAsync(ctx).ConfigureAwait(false);
73+
5874
GeneralTracer.Debug($"OSSUpdateStrategy: 3. Downloading {versions.Count} version(s).");
5975
await DownloadVersionsAsync(versions);
6076

6177
GeneralTracer.Debug("OSSUpdateStrategy: 4. Decompressing packages.");
6278
Decompress(versions);
6379

80+
// Report: update applied
81+
await SafeReportUpdateAppliedAsync(ctx).ConfigureAwait(false);
82+
83+
// Hooks: before starting main app
84+
await SafeOnBeforeStartAppAsync(ctx).ConfigureAwait(false);
85+
6486
GeneralTracer.Debug("OSSUpdateStrategy: 5. Launching main application.");
6587
StartApp();
6688
}
6789
catch (Exception ex)
6890
{
91+
await SafeOnUpdateErrorAsync(ctx, ex).ConfigureAwait(false);
92+
await SafeReportUpdateFailedAsync(ctx, ex).ConfigureAwait(false);
6993
GeneralTracer.Error("OSSUpdateStrategy.ExecuteAsync failed.", ex);
7094
throw;
7195
}
@@ -89,6 +113,8 @@ public void StartApp()
89113
GeneralTracer.Debug("OSSUpdateStrategy: application started.");
90114
}
91115

116+
#region Helpers
117+
92118
private async Task DownloadVersionsAsync(List<VersionOSS> versions)
93119
{
94120
var manager = new DownloadManager(_appPath, Format.ZIP, TimeOut);
@@ -121,4 +147,76 @@ private void Decompress(List<VersionOSS> versions)
121147
File.Delete(zipFilePath);
122148
}
123149
}
150+
151+
// ════════════════════════════════════════════════════════════════
152+
// Hooks & Reporter safe wrappers
153+
// ════════════════════════════════════════════════════════════════
154+
155+
private Hooks.UpdateContext BuildUpdateContext()
156+
{
157+
return new Hooks.UpdateContext(
158+
_configInfo?.AppName ?? "unknown",
159+
_configInfo?.InstallPath ?? _appPath,
160+
_configInfo?.ClientVersion ?? "0.0.0",
161+
_configInfo?.LastVersion,
162+
AppType.OSSApp
163+
);
164+
}
165+
166+
private async Task<bool> SafeOnBeforeUpdateAsync(Hooks.UpdateContext ctx)
167+
{
168+
try { return await Hooks.OnBeforeUpdateAsync(ctx).ConfigureAwait(false); }
169+
catch (Exception ex) { GeneralTracer.Warn($"OnBeforeUpdateAsync hook failed: {ex.Message}"); return true; }
170+
}
171+
172+
private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx)
173+
{
174+
try { await Hooks.OnBeforeStartAppAsync(ctx).ConfigureAwait(false); }
175+
catch (Exception ex) { GeneralTracer.Warn($"OnBeforeStartAppAsync hook failed: {ex.Message}"); }
176+
}
177+
178+
private async Task SafeOnUpdateErrorAsync(Hooks.UpdateContext ctx, Exception error)
179+
{
180+
try { await Hooks.OnUpdateErrorAsync(ctx, error).ConfigureAwait(false); }
181+
catch (Exception ex) { GeneralTracer.Warn($"OnUpdateErrorAsync hook failed: {ex.Message}"); }
182+
}
183+
184+
private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx)
185+
{
186+
try
187+
{
188+
await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
189+
ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
190+
Download.Reporting.UpdateEvent.UpdateStarted, ctx.AppType, DateTimeOffset.UtcNow
191+
)).ConfigureAwait(false);
192+
}
193+
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); }
194+
}
195+
196+
private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx)
197+
{
198+
try
199+
{
200+
await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
201+
ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
202+
Download.Reporting.UpdateEvent.UpdateApplied, ctx.AppType, DateTimeOffset.UtcNow
203+
)).ConfigureAwait(false);
204+
}
205+
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); }
206+
}
207+
208+
private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error)
209+
{
210+
try
211+
{
212+
await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
213+
ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
214+
Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow,
215+
ErrorMessage: error.Message
216+
)).ConfigureAwait(false);
217+
}
218+
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); }
219+
}
220+
221+
#endregion
124222
}

0 commit comments

Comments
 (0)