Skip to content

Commit e9eb58c

Browse files
authored
Batch 6: Silent polling + SignalR Hub download source (#372)
- Create SilentPollOrchestrator: configurable poll interval, background download using new download abstractions, optional auto-install, cancellable - Create HubDownloadSource: SignalR Hub as IDownloadSource, parses incoming push messages into DownloadAssets via DownloadPlanBuilder.MapToAsset - Add LaunchSilentAsync to GeneralUpdateBootstrap: When Silent=true, starts background poll loop and returns immediately - Add SilentPollIntervalMinutes to UpdateOptions - Add MapToAsset public helper back to DownloadPlanBuilder - Mark old SilentUpdateMode as [Obsolete] Closes #371
1 parent 4bdb160 commit e9eb58c

6 files changed

Lines changed: 371 additions & 0 deletions

File tree

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ public GeneralUpdateBootstrap()
5050
public override async Task<GeneralUpdateBootstrap> LaunchAsync()
5151
{
5252
int appType = GetOption(UpdateOptions.AppType);
53+
54+
// Silent mode: start background poll and return immediately
55+
if (appType == AppType.ClientApp && GetOption(UpdateOptions.Silent))
56+
{
57+
await LaunchSilentAsync().ConfigureAwait(false);
58+
return this;
59+
}
60+
5361
return appType switch
5462
{
5563
AppType.ClientApp => await LaunchWithStrategy(new ClientUpdateStrategy()),
@@ -250,6 +258,35 @@ private void ApplyRuntimeOptions()
250258
_configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;
251259
}
252260

261+
/// <summary>
262+
/// Silent update mode — starts a background poll loop and returns immediately.
263+
/// The orchestrator checks for updates periodically and prepares them.
264+
/// When the host process exits, the prepared update is applied.
265+
/// </summary>
266+
private async Task LaunchSilentAsync()
267+
{
268+
GeneralTracer.Info("GeneralUpdateBootstrap: starting silent update mode.");
269+
270+
var pollMinutes = GetOption(UpdateOptions.SilentPollIntervalMinutes);
271+
var autoInstall = GetOption(UpdateOptions.SilentAutoInstall);
272+
273+
var silentOptions = new Silent.SilentOptions
274+
{
275+
PollInterval = TimeSpan.FromMinutes(pollMinutes),
276+
AutoInstall = autoInstall
277+
};
278+
279+
var hooks = ResolveExtension<Hooks.IUpdateHooks>() ?? new Hooks.NoOpUpdateHooks();
280+
var reporter = ResolveExtension<Download.Reporting.IUpdateReporter>() ?? new Download.Reporting.NoOpUpdateReporter();
281+
282+
var orchestrator = new Silent.SilentPollOrchestrator(_configInfo, silentOptions)
283+
.WithHooks(hooks)
284+
.WithReporter(reporter);
285+
286+
await orchestrator.StartAsync().ConfigureAwait(false);
287+
GeneralTracer.Info("GeneralUpdateBootstrap: silent update mode started, returning to caller.");
288+
}
289+
253290
private void InitBlackList()
254291
{
255292
BlackListManager.Instance.AddBlackFiles(_configInfo.BlackFiles);

src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static class UpdateOptions
3434
public static UpdateOption<string?> UpgradeClientVersion { get; } = UpdateOption.ValueOf<string?>("UPGRADECLIENTVERSION", null);
3535
public static UpdateOption<int?> Platform { get; } = UpdateOption.ValueOf<int?>("PLATFORM", null);
3636
public static UpdateOption<bool> SilentAutoInstall { get; } = UpdateOption.ValueOf<bool>("SILENTAUTOINSTALL", false);
37+
public static UpdateOption<int> SilentPollIntervalMinutes { get; } = UpdateOption.ValueOf<int>("SILENTPOLLINTERVALMINUTES", 60);
3738
public static UpdateOption<int> MaxConcurrency { get; } = UpdateOption.ValueOf<int>("MAXCONCURRENCY", 3);
3839
public static UpdateOption<bool> EnableResume { get; } = UpdateOption.ValueOf<bool>("ENABLERESUME", true);
3940
public static UpdateOption<int> RetryCount { get; } = UpdateOption.ValueOf<int>("RETRYCOUNT", 3);

src/c#/GeneralUpdate.Core/Download/DownloadPlanBuilder.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using GeneralUpdate.Core.Download.Abstractions;
45
using GeneralUpdate.Core.Download.Models;
56

67
namespace GeneralUpdate.Core.Download;
@@ -86,6 +87,25 @@ private static bool IsCompatible(string? minClientVersion, string currentVersion
8687
return cur >= min;
8788
}
8889

90+
/// <summary>Map a PacketDTO to a DownloadAsset. Public for use by download sources.</summary>
91+
public static DownloadAsset MapToAsset(Abstractions.PacketDTO p)
92+
{
93+
return new DownloadAsset(
94+
Name: p.Name ?? p.Version ?? "unknown",
95+
Url: p.Url ?? string.Empty,
96+
Size: p.Size ?? 0,
97+
SHA256: p.Hash,
98+
Version: p.Version ?? "0.0.0",
99+
IsCrossVersion: p.IsCrossVersion == true,
100+
FromVersion: p.FromVersion,
101+
MinClientVersion: p.MinClientVersion,
102+
SourceArchiveHash: p.SourceArchiveHash,
103+
TargetArchiveHash: p.TargetArchiveHash,
104+
IsForcibly: p.IsForcibly == true,
105+
IsFreeze: p.IsFreeze == true
106+
);
107+
}
108+
89109
/// <summary>Parse a version string, returning null on failure.</summary>
90110
private static Version? ParseVersion(string? version)
91111
{
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using GeneralUpdate.Core.Download.Abstractions;
8+
using GeneralUpdate.Core.Download.Models;
9+
using GeneralUpdate.Core.Hubs;
10+
using GeneralUpdate.Core.JsonContext;
11+
12+
namespace GeneralUpdate.Core.Download.Sources;
13+
14+
/// <summary>
15+
/// SignalR Hub download source — receives update push notifications
16+
/// and converts them to DownloadAssets for the orchestrator.
17+
/// </summary>
18+
public class HubDownloadSource : IDownloadSource, IDisposable
19+
{
20+
private readonly string _hubUrl;
21+
private readonly string? _token;
22+
private readonly string? _appKey;
23+
private readonly ConcurrentBag<DownloadAsset> _assets = new();
24+
private readonly TaskCompletionSource<bool> _initializedTcs = new();
25+
private UpgradeHubService? _hub;
26+
27+
public HubDownloadSource(string hubUrl, string? token = null, string? appKey = null)
28+
{
29+
_hubUrl = hubUrl;
30+
_token = token;
31+
_appKey = appKey;
32+
}
33+
34+
/// <summary>Start listening to the SignalR hub.</summary>
35+
public async Task StartAsync()
36+
{
37+
try
38+
{
39+
_hub = new UpgradeHubService(_hubUrl, _token, _appKey);
40+
_hub.AddListenerReceive(OnReceiveMessage);
41+
await _hub.StartAsync().ConfigureAwait(false);
42+
_initializedTcs.TrySetResult(true);
43+
}
44+
catch (Exception ex)
45+
{
46+
_initializedTcs.TrySetException(ex);
47+
}
48+
}
49+
50+
private void OnReceiveMessage(string json)
51+
{
52+
try
53+
{
54+
var packet = System.Text.Json.JsonSerializer.Deserialize<PacketDTO>(json);
55+
if (packet != null)
56+
{
57+
var asset = DownloadPlanBuilder.MapToAsset(packet);
58+
_assets.Add(asset);
59+
}
60+
}
61+
catch (Exception ex)
62+
{
63+
GeneralTracer.Warn($"HubDownloadSource: failed to parse message: {ex.Message}");
64+
}
65+
}
66+
67+
/// <summary>Get accumulated download assets from hub pushes.</summary>
68+
public async Task<IReadOnlyList<DownloadAsset>> ListAsync(CancellationToken token = default)
69+
{
70+
// Wait for hub initialization
71+
await _initializedTcs.Task.ConfigureAwait(false);
72+
73+
// Wait a brief moment for any pending messages to arrive
74+
try { await Task.Delay(100, token).ConfigureAwait(false); }
75+
catch (OperationCanceledException) { }
76+
77+
return _assets.ToList();
78+
}
79+
80+
public void Dispose()
81+
{
82+
_hub?.DisposeAsync().GetAwaiter().GetResult();
83+
}
84+
}

0 commit comments

Comments
 (0)