Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 14 additions & 0 deletions src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
var hooks = ResolveExtension<Hooks.IUpdateHooks>() ?? new Hooks.NoOpUpdateHooks();
roleStrategy.Hooks = hooks;

// Resolve reporter from extensions. If ReportUrl is not configured,
// force NoOpUpdateReporter regardless of what the user registered.
Download.Reporting.IUpdateReporter reporter;
if (string.IsNullOrWhiteSpace(_configInfo.ReportUrl))
{
reporter = new Download.Reporting.NoOpUpdateReporter();
}
else
{
reporter = ResolveExtension<Download.Reporting.IUpdateReporter>() ??
new Download.Reporting.NoOpUpdateReporter();
}
roleStrategy.Reporter = reporter;

// ── Download components ──
var downloadOrchestrator = ResolveExtension<Download.Abstractions.IDownloadOrchestrator>();
var downloadPolicy = ResolveExtension<Download.Abstractions.IDownloadPolicy>();
Expand Down
60 changes: 54 additions & 6 deletions src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ public enum UpdateStatus { Updating = 1, Success = 2, Failure = 3 }
/// </remarks>
public record UpdateReport(int RecordId, int Status = 1, int Type = 1);

/// <summary>
/// A no-op (null object) update status reporter that performs no actual work.
/// Used when no ReportUrl is configured.
/// </summary>
/// <remarks>
/// <para>
/// This class implements the Null Object Pattern, eliminating the need for null checks
/// in consumer code. Every report operation returns a completed task immediately without
/// performing any actual data transmission.
/// </para>
/// <para>
/// Use this implementation when no remote status reporting is needed,
/// such as during local testing or when the report endpoint is not configured.
/// </para>
/// </remarks>
/// <summary>
/// An HTTP POST-based update status reporter that serializes <see cref="UpdateReport"/> to JSON
/// and sends it to a configured remote endpoint. Compatible with the GeneralSpacestation ReportDTO format.
Expand All @@ -86,26 +101,59 @@ public record UpdateReport(int RecordId, int Status = 1, int Type = 1);
/// </remarks>
public class HttpUpdateReporter : IUpdateReporter
{
private readonly HttpClient _client;
private readonly string _reportUrl;
private HttpClient _client;
private string _reportUrl;

/// <summary>
/// Initializes a new instance of the <see cref="HttpUpdateReporter"/> class.
/// Gets or sets the report URL for update status reporting.
/// When null or empty, <see cref="ReportAsync"/> is a no-op.
/// </summary>
public string ReportUrl
{
get => _reportUrl;
set => _reportUrl = value ?? string.Empty;
}

/// <summary>
/// Gets or sets the <see cref="HttpClient"/> used for HTTP requests.
/// If not set, a default instance is created in the parameterless constructor.
/// </summary>
public HttpClient Client
{
get => _client;
set => _client = value ?? throw new ArgumentNullException(nameof(value));
}

/// <summary>
/// Parameterless constructor required by the extension resolution mechanism.
/// Uses a default HttpClient and empty ReportUrl (no-op until configured).
/// </summary>
public HttpUpdateReporter()
{
_client = new HttpClient();
_reportUrl = string.Empty;
}

/// <summary>
/// Initializes a new instance of the <see cref="HttpUpdateReporter"/> class
/// with a specific client and report URL.
/// </summary>
/// <param name="client">The <see cref="HttpClient"/> instance used to send HTTP requests.</param>
/// <param name="reportUrl">The remote URL that receives the update status reports.</param>
public HttpUpdateReporter(HttpClient client, string reportUrl)
{
_client = client;
_reportUrl = reportUrl;
_client = client ?? throw new ArgumentNullException(nameof(client));
_reportUrl = reportUrl ?? string.Empty;
}

public async Task ReportAsync(UpdateReport report, CancellationToken token = default)
{
try
{
if(string.IsNullOrWhiteSpace(_reportUrl))
return;

var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

using var request = new HttpRequestMessage(HttpMethod.Post, _reportUrl);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");

Expand Down
68 changes: 3 additions & 65 deletions src/c#/GeneralUpdate.Core/Network/VersionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ namespace GeneralUpdate.Core.Network
/// <list type="bullet">
/// <item><description>At startup, call <see cref="Validate(string, string, AppType, string, PlatformType, string, string, string, CancellationToken)"/>
/// to check whether the server has a new version.</description></item>
/// <item><description>After download completes, call <see cref="Report(string, int, int, int?, string, string, CancellationToken)"/>
/// <item><description>After download completes, use <see cref="Download.Reporting.IUpdateReporter.ReportAsync"/>
/// to report the update status.</description></item>
/// </list>
/// </para>
Expand Down Expand Up @@ -146,43 +146,6 @@ public VersionService(IHttpAuthProvider? auth = null, TimeSpan? timeout = null,
_maxRetries = maxRetries;
}

/// <summary>
/// Reports the update status to the server for a specified record.
/// </summary>
/// <remarks>
/// <para>
/// This is a backward-compatible static convenience method that internally creates
/// a <see cref="VersionService"/> instance and calls <see cref="ReportAsync"/>.
/// </para>
/// <para>
/// Execution flow:
/// <list type="number">
/// <item><description>Resolves the authentication provider: uses the global provider
/// (<see cref="SetDefaultAuthProvider"/>) first; otherwise creates one via
/// <see cref="HttpAuthProviderFactory.Create"/>.</description></item>
/// <item><description>Creates a temporary <see cref="VersionService"/> instance.</description></item>
/// <item><description>Calls <see cref="ReportAsync"/> to perform the report.</description></item>
/// </list>
/// </para>
/// </remarks>
/// <param name="url">The server API URL.</param>
/// <param name="recordId">The update record identifier.</param>
/// <param name="status">The current status code.</param>
/// <param name="type">The update type (may be null).</param>
/// <param name="scheme">The authentication scheme (e.g., "bearer", "apikey", "hmac"), used to create the auth provider. Ignored when a global auth provider is set.</param>
/// <param name="token">The authentication token or key, used together with <paramref name="scheme"/>.</param>
/// <param name="ct">A <see cref="CancellationToken"/> for cancelling the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task Report(string url, int recordId, int status, int? type,
string scheme = null, string token = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(url))
return Task.CompletedTask;

var a = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, null);
return new VersionService(a).ReportAsync(url, recordId, status, type, ct);
}

/// <summary>
/// Validates the current version against the server to check for available updates.
/// </summary>
Expand Down Expand Up @@ -219,8 +182,8 @@ public static Task<VersionRespDTO> Validate(string url, string version,
AppType appType, string appKey, PlatformType platform, string productId,
string scheme = null, string token = null, CancellationToken ct = default)
{
var a = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, appKey);
return new VersionService(a).ValidateAsync(url, version, (int)appType, appKey, (int)platform, productId, ct);
var auth = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, appKey);
return new VersionService(auth).ValidateAsync(url, version, (int)appType, appKey, (int)platform, productId, ct);
}

/// <summary>
Expand Down Expand Up @@ -249,31 +212,6 @@ public static Task<VersionRespDTO> Validate(string url, string version,
string scheme = null, string token = null, CancellationToken ct = default)
=> Validate(url, version, (AppType)appType, appKey, (PlatformType)platform, productId, scheme, token, ct);

/// <summary>
/// Asynchronously reports the update record status to the server.
/// </summary>
/// <remarks>
/// <para>
/// Execution flow:
/// <list type="number">
/// <item><description>Constructs a parameter dictionary with the record ID, status, and type.</description></item>
/// <item><description>Sends the parameters via a POST request using <see cref="PostAsync{T}"/>.</description></item>
/// <item><description>Deserializes the response into a <see cref="BaseResponseDTO{T}"/> of type <see cref="bool"/>.</description></item>
/// </list>
/// </para>
/// </remarks>
/// <param name="url">The server API URL.</param>
/// <param name="recordId">The update record identifier.</param>
/// <param name="status">The current status code.</param>
/// <param name="type">The update type (may be null).</param>
/// <param name="t">A <see cref="CancellationToken"/> for cancelling the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
private async Task ReportAsync(string url, int recordId, int status, int? type, CancellationToken t = default)
{
var p = new Dictionary<string, object> { ["recordId"] = recordId, ["status"] = status, ["type"] = type };
await PostAsync<BaseResponseDTO<bool>>(url, p, ReportRespJsonContext.Default.BaseResponseDTOBoolean, t);
}

/// <summary>
/// Asynchronously validates the version by querying the server for available updates.
/// </summary>
Expand Down
14 changes: 5 additions & 9 deletions src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
using GeneralUpdate.Core.Event;
using GeneralUpdate.Core.Pipeline;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Network;

using GeneralUpdate.Core.Hooks;
using IUpdateReporter = GeneralUpdate.Core.Download.Reporting.IUpdateReporter;
using UpdateReport = GeneralUpdate.Core.Download.Reporting.UpdateReport;

namespace GeneralUpdate.Core.Strategy
{
Expand All @@ -30,7 +31,7 @@ namespace GeneralUpdate.Core.Strategy
/// <item><description>Calls <see cref="BuildPipeline"/> (abstract method, implemented by subclasses) to obtain the middleware builder.</description></item>
/// <item><description>Executes <c>PipelineBuilder.Build()</c> to run the registered middleware in FIFO order:
/// <c>Hash</c> (integrity verification) → <c>Decompress</c> (extract update package) → <c>Patch</c> (apply incremental patches).</description></item>
/// <item><description>Reports the update result for the current version to the server via <see cref="VersionService.Report"/>.</description></item>
/// <item><description>Reports the update result for the current version via <see cref="Reporter"/>.</description></item>
/// <item><description>Deletes the processed archive file.</description></item>
/// </list>
/// </para>
Expand Down Expand Up @@ -58,7 +59,7 @@ public abstract class AbstractStrategy : IStrategy
/// <summary>
/// Gets or sets the update status reporter. Responsible for reporting the processing progress and final result of each version to the server.
/// </summary>
public IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter();
public IUpdateReporter Reporter { get; set; } = new Download.Reporting.HttpUpdateReporter();

/// <summary>
/// Gets or sets the differential patch pipeline. Supports parallel application of incremental patches and progress reporting.
Expand Down Expand Up @@ -161,12 +162,7 @@ public virtual async Task ExecuteAsync()
}
finally
{
await VersionService.Report(_configinfo.ReportUrl
, version.RecordId
, status
, version.AppType
, _configinfo.Scheme
, _configinfo.Token);
await Reporter.ReportAsync(new UpdateReport(version.RecordId, status, version.AppType ?? 1));

// Delete only this version's zip file — other AppType packages
// in TempPath may still be needed by a downstream process.
Expand Down
1 change: 1 addition & 0 deletions src/c#/GeneralUpdate.Core/Strategy/ClientStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ public void Create(GlobalConfigInfo parameter)
if (_osStrategy is AbstractStrategy abs)
{
if (_pendingDiffPipeline != null) abs.DiffPipeline = _pendingDiffPipeline;
abs.Reporter = this.Reporter;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/c#/GeneralUpdate.Core/Strategy/UpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public void Create(GlobalConfigInfo parameter)
if (_osStrategy is AbstractStrategy abs)
{
if (_pendingDiffPipeline != null) abs.DiffPipeline = _pendingDiffPipeline;
abs.Reporter = this.Reporter;
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/ClientTest/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using GeneralUpdate.Core;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Download;
using GeneralUpdate.Core.Download.Reporting;
using GeneralUpdate.Core.Event;
using GeneralUpdate.Core.Hooks;

Expand Down
Loading