Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -63,15 +65,31 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
{
ApplyRuntimeOptions();

// Resolve hooks and reporter from extensions
var hooks = ResolveExtension<Hooks.IUpdateHooks>() ?? new Hooks.NoOpUpdateHooks();
var reporter = ResolveExtension<Download.Reporting.IUpdateReporter>() ?? 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();
Expand Down
101 changes: 97 additions & 4 deletions src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class ClientUpdateStrategy : IStrategy
private readonly Download.Abstractions.IDownloadOrchestrator? _orchestrator;
private readonly DiffMode _diffMode = DiffMode.Serial;

/// <summary>Lifecycle hooks injected by the bootstrap.</summary>
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
/// <summary>Update status reporter injected by the bootstrap.</summary>
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)
Expand All @@ -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));
}
Expand Down Expand Up @@ -88,7 +96,7 @@ public ClientUpdateStrategy UseCustomOption(Func<bool> 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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<bool> 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
}
98 changes: 98 additions & 0 deletions src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public class OSSUpdateStrategy : IStrategy
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
private const int TimeOut = 60;

/// <summary>Lifecycle hooks injected by the bootstrap.</summary>
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
/// <summary>Update status reporter injected by the bootstrap.</summary>
public Download.Reporting.IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter();

public void Create(GlobalConfigInfo parameter)
{
_configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter));
Expand All @@ -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";
Expand All @@ -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;
}
Expand All @@ -89,6 +113,8 @@ public void StartApp()
GeneralTracer.Debug("OSSUpdateStrategy: application started.");
}

#region Helpers

private async Task DownloadVersionsAsync(List<VersionOSS> versions)
{
var manager = new DownloadManager(_appPath, Format.ZIP, TimeOut);
Expand Down Expand Up @@ -121,4 +147,76 @@ private void Decompress(List<VersionOSS> 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<bool> 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
}
Loading
Loading