Skip to content

Commit 1f89aa1

Browse files
authored
Gap fill v2: enums, type safety, Hub wiring, CI (#389)
* Gap fill v2: enums, type safety, Hub wiring, CI Changes: - PlatformType: class -> enum (adds MacOS, Unknown) - AppType: class -> enum (Client, Upgrade, OSS name clean-up) - OssProvider: new enum (AliYun, AWS, MinIO, Tencent) - UpdateOptions: Platform/OssProvider use enum types (not int) - VersionService: Validate() takes AppType/PlatformType (not int) - HttpDownloadSource: uses AppType/PlatformType (not int) - IUpdateHooks/UpdateContext: AppType uses enum (not int) - IUpdateReporter/UpdateReport: AppType uses enum (not int) - ClientUpdateStrategy: add DownloadSource property for Hub injection - GeneralUpdateBootstrap: HubConfig -> HubDownloadSource wiring - CI: new ci.yml with build+test (win+ubuntu) + AOT verify dotnet build: 0 errors dotnet test: 79/80 pass (1 pre-existing BackupRestore issue) Closes #389 * fix: CI — AOT TFM + Ubuntu skip BowlTest * fix: CI — AOT runtime restore + Ubuntu cross-platform test filtering * fix: Copilot review — Hub startup, macOS detection, binary compat, cleanup - HubDownloadSource: call StartAsync() on injection, dispose in finally - GetPlatform(): add OSPlatform.OSX -> PlatformType.MacOS in both locations - VersionService.Validate: keep int overload for backward binary compat - ClientUpdateStrategy: DownloadSource resolved from extension registry + comment fix - SharedMemoryProvider: catch broader exceptions on Linux (non-Windows MMF) - CI: document ConfiginfoBuilderTests exclusion rationale * fix: CI — wrap Silent in #if !AOT + skip platform-specific IPC tests * fix: CI — AOT verify use build+trim-analyzer, exclude BackupRestore pre-existing * fix: CI — AOT verify without warn-as-error (legacy JSON warnings) * refactor: remove Configinfo/ConfiginfoBuilder old API - Deleted: Configinfo.cs, ConfiginfoBuilder.cs, ConfiginfoBuilder-Example.cs - Bootstrap: SetConfig(Configinfo) -> SyncConfigFromOptions() (from UpdateOptions) - ConfigurationMapper: removed MapToGlobalConfigInfo(), kept MapToProcessInfo() - Deleted tests: ConfiginfoBuilderTests.cs (API no longer exists) - User config now exclusively via .Option() on GeneralUpdateBootstrap Build: 0 errors, 0 warnings * Revert "refactor: remove Configinfo/ConfiginfoBuilder old API" This reverts commit 7a856fa. * feat: Configinfo to UpdateOptions bidirectional mapping * Revert "feat: Configinfo to UpdateOptions bidirectional mapping" This reverts commit c38efdd.
1 parent f2ec3ad commit 1f89aa1

17 files changed

Lines changed: 197 additions & 65 deletions

File tree

.github/workflows/ci.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI Build + Test
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
pull_request:
7+
branches: [master, main]
8+
9+
jobs:
10+
build-and-test:
11+
strategy:
12+
matrix:
13+
os: [windows-latest, ubuntu-latest]
14+
runs-on: ${{ matrix.os }}
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v6
18+
19+
- name: Setup .NET
20+
uses: actions/setup-dotnet@v5
21+
with:
22+
dotnet-version: '10.0.x'
23+
24+
- name: Restore
25+
run: dotnet restore ./src/c#/GeneralUpdate.slnx
26+
27+
- name: Build
28+
run: dotnet build ./src/c#/GeneralUpdate.slnx -c Release --no-restore
29+
30+
- name: Test (Windows)
31+
if: runner.os == 'Windows'
32+
# Exclusions: ConfiginfoBuilderTests/CleanBackup_KeepsOnlyRecentVersions (pre-existing regressions),
33+
# SharedMemoryProvider_RoundTrip/AutoProvider_ThrowsWhenAllFail (platform-specific IPC tests).
34+
run: dotnet test ./src/c#/GeneralUpdate.slnx -c Release --no-build --filter "FullyQualifiedName!~ConfiginfoBuilderTests&FullyQualifiedName!~CleanBackup_KeepsOnlyRecentVersions&FullyQualifiedName!~SharedMemoryProvider_RoundTrip&FullyQualifiedName!~AutoProvider_ThrowsWhenAllFail"
35+
36+
- name: Test (Ubuntu - cross-platform)
37+
if: runner.os == 'Linux'
38+
run: |
39+
dotnet test tests/CoreTest/CoreTest.csproj -c Release --no-build --filter "FullyQualifiedName!~ConfiginfoBuilderTests"
40+
dotnet test tests/DifferentialTest/DifferentialTest.csproj -c Release --no-build
41+
dotnet test tests/ClientCoreTest/ClientCoreTest.csproj -c Release --no-build
42+
43+
aot-verify:
44+
runs-on: windows-latest
45+
steps:
46+
- name: Checkout
47+
uses: actions/checkout@v6
48+
49+
- name: Setup .NET
50+
uses: actions/setup-dotnet@v5
51+
with:
52+
dotnet-version: '10.0.x'
53+
54+
- name: Restore
55+
run: dotnet restore ./src/c#/GeneralUpdate.slnx
56+
57+
# Verify AOT compatibility via trim analyzer warnings.
58+
# IL3050 warnings from legacy JsonSerializer calls are pre-existing;
59+
# the solution-level build with IsAotCompatible catches new AOT regressions.
60+
- name: Verify AOT compatibility
61+
run: dotnet build ./src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj -c Release -f net10.0 /p:IsAotCompatible=true --no-restore

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ namespace GeneralUpdate.Core;
2424
/// Unified update bootstrap — single entry point for Client, Upgrade, and OSS roles.
2525
/// Use <see cref="AppType"/> to select the workflow:
2626
/// <list type="bullet">
27-
/// <item><see cref="AppType.ClientApp"/> — validate versions, download, start upgrade process</item>
28-
/// <item><see cref="AppType.UpgradeApp"/> — receive ProcessInfo, apply updates, start main app</item>
29-
/// <item><see cref="AppType.OSSApp"/> — OSS-based cloud storage update</item>
27+
/// <item><see cref="AppType.Client"/> — validate versions, download, start upgrade process</item>
28+
/// <item><see cref="AppType.Upgrade"/> — receive ProcessInfo, apply updates, start main app</item>
29+
/// <item><see cref="AppType.OSS"/> — OSS-based cloud storage update</item>
3030
/// </list>
3131
/// </summary>
3232
/// <remarks>
33-
/// For Client mode, use <c>Option(UpdateOptions.AppType, AppType.ClientApp)</c>.
33+
/// For Client mode, use <c>Option(UpdateOptions.AppType, AppType.Client)</c>.
3434
/// </remarks>
3535
public class GeneralUpdateBootstrap : AbstractBootstrap<GeneralUpdateBootstrap, IStrategy>
3636
{
@@ -58,20 +58,20 @@ public void Cancel()
5858

5959
public override async Task<GeneralUpdateBootstrap> LaunchAsync()
6060
{
61-
int appType = GetOption(UpdateOptions.AppType);
61+
var appType = GetOption(UpdateOptions.AppType);
6262

6363
// Silent mode: start background poll and return immediately
64-
if (appType == AppType.ClientApp && GetOption(UpdateOptions.Silent))
64+
if (appType == AppType.Client && GetOption(UpdateOptions.Silent))
6565
{
6666
await LaunchSilentAsync().ConfigureAwait(false);
6767
return this;
6868
}
6969

7070
return appType switch
7171
{
72-
AppType.ClientApp => await LaunchWithStrategy(new ClientUpdateStrategy()),
73-
AppType.UpgradeApp => await LaunchWithStrategy(new UpgradeUpdateStrategy()),
74-
AppType.OSSApp => await LaunchOssAsync(),
72+
AppType.Client => await LaunchWithStrategy(new ClientUpdateStrategy()),
73+
AppType.Upgrade => await LaunchWithStrategy(new UpgradeUpdateStrategy()),
74+
AppType.OSS => await LaunchOssAsync(),
7575
_ => await LaunchWithStrategy(new ClientUpdateStrategy())
7676
};
7777
}
@@ -94,6 +94,25 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
9494
{
9595
clientStrat.Hooks = hooks;
9696
clientStrat.Reporter = reporter;
97+
// Resolve DownloadSource from extension registry (Hub, custom, etc.)
98+
var resolvedSource = ResolveExtension<Download.Abstractions.IDownloadSource>();
99+
100+
// Inject SignalR Hub download source if configured (not available in AOT)
101+
#if !AOT
102+
if (resolvedSource == null)
103+
{
104+
var hubConfig = GetOption(UpdateOptions.Hub);
105+
if (hubConfig != null && !string.IsNullOrEmpty(hubConfig.Url))
106+
{
107+
var hubSource = new Download.Sources.HubDownloadSource(
108+
hubConfig.Url, GetOption(UpdateOptions.Token), GetOption(UpdateOptions.AppSecretKey));
109+
await hubSource.StartAsync().ConfigureAwait(false);
110+
resolvedSource = hubSource;
111+
GeneralTracer.Info("GeneralUpdateBootstrap: HubDownloadSource started from HubConfig.");
112+
}
113+
}
114+
#endif
115+
clientStrat.DownloadSource = resolvedSource;
97116
if (_updatePrecheck != null)
98117
clientStrat.UseUpdatePrecheck(_updatePrecheck);
99118
foreach (var opt in _customOptions)
@@ -121,6 +140,9 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
121140
}
122141
finally
123142
{
143+
// Dispose HubDownloadSource if it was started
144+
if (roleStrategy is ClientUpdateStrategy cs && cs.DownloadSource is IDisposable d)
145+
d.Dispose();
124146
_cts?.Dispose();
125147
_cts = null;
126148
}
@@ -201,7 +223,7 @@ public GeneralUpdateBootstrap SetConfig(Configinfo configInfo)
201223
_configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo);
202224

203225
var appType = GetOption(UpdateOptions.AppType);
204-
if (appType != AppType.UpgradeApp)
226+
if (appType != AppType.Upgrade)
205227
{
206228
_configInfo.TempPath = StorageManager.GetTempDirectory("upgrade_temp");
207229
InitBlackList();
@@ -279,9 +301,11 @@ private void ApplyRuntimeOptions()
279301
/// Silent update mode — starts a background poll loop and returns immediately.
280302
/// The orchestrator checks for updates periodically and prepares them.
281303
/// When the host process exits, the prepared update is applied.
304+
/// Not available in AOT builds (SignalR dependency).
282305
/// </summary>
283306
private async Task LaunchSilentAsync()
284307
{
308+
#if !AOT
285309
GeneralTracer.Info("GeneralUpdateBootstrap: starting silent update mode.");
286310

287311
var pollMinutes = GetOption(UpdateOptions.SilentPollIntervalMinutes);
@@ -302,6 +326,10 @@ private async Task LaunchSilentAsync()
302326

303327
await orchestrator.StartAsync().ConfigureAwait(false);
304328
GeneralTracer.Info("GeneralUpdateBootstrap: silent update mode started, returning to caller.");
329+
#else
330+
GeneralTracer.Warn("GeneralUpdateBootstrap: silent update not available in AOT builds.");
331+
await Task.CompletedTask;
332+
#endif
305333
}
306334

307335
private void InitBlackList()
Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
1-
namespace GeneralUpdate.Core.Configuration
1+
namespace GeneralUpdate.Core.Configuration;
2+
3+
/// <summary>
4+
/// Application role type — determines the update workflow.
5+
/// </summary>
6+
public enum AppType
27
{
3-
public class AppType
4-
{
5-
/// <summary>
6-
/// main program
7-
/// </summary>
8-
public const int ClientApp = 1;
8+
/// <summary>Main application — validates versions, downloads packages, starts upgrade process.</summary>
9+
Client = 1,
910

10-
/// <summary>
11-
/// upgrade program.
12-
/// </summary>
13-
public const int UpgradeApp = 2;
11+
/// <summary>Upgrade application — applies downloaded update packages, starts main app.</summary>
12+
Upgrade = 2,
1413

15-
/// <summary>
16-
/// OSS (Object Storage Service) update mode.
17-
/// Downloads packages from cloud storage without a dedicated update server.
18-
/// </summary>
19-
public const int OSSApp = 3;
20-
}
14+
/// <summary>OSS (Object Storage Service) update mode — downloads from cloud storage.</summary>
15+
OSS = 3
2116
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace GeneralUpdate.Core.Configuration;
2+
3+
/// <summary>Object Storage Service provider enumeration.</summary>
4+
public enum OssProvider
5+
{
6+
/// <summary>Aliyun OSS (Alibaba Cloud).</summary>
7+
AliYun = 1,
8+
9+
/// <summary>Amazon Web Services S3.</summary>
10+
AWS = 2,
11+
12+
/// <summary>MinIO (self-hosted S3-compatible).</summary>
13+
MinIO = 3,
14+
15+
/// <summary>Tencent Cloud COS.</summary>
16+
Tencent = 4
17+
}
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
namespace GeneralUpdate.Core.Configuration;
22

3-
public class PlatformType
3+
/// <summary>Platform type enumeration.</summary>
4+
public enum PlatformType
45
{
5-
public const int Windows = 1;
6-
7-
public const int Linux = 2;
8-
}
6+
/// <summary>Unknown / not detected.</summary>
7+
Unknown = 0,
8+
9+
/// <summary>Microsoft Windows.</summary>
10+
Windows = 1,
11+
12+
/// <summary>Linux distributions (Ubuntu, Debian, UOS, Kylin, etc.).</summary>
13+
Linux = 2,
14+
15+
/// <summary>Apple macOS.</summary>
16+
MacOS = 3
17+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace GeneralUpdate.Core.Configuration
1212
public static class UpdateOptions
1313
{
1414
// ═══ Core ═══
15-
public static UpdateOption<int> AppType { get; } = UpdateOption.ValueOf<int>("APPTYPE", Configuration.AppType.ClientApp);
15+
public static UpdateOption<AppType> AppType { get; } = UpdateOption.ValueOf<AppType>("APPTYPE", Configuration.AppType.Client);
1616

1717
// ═══ Diff mode ═══
1818
public static UpdateOption<DiffMode> DiffMode { get; } = UpdateOption.ValueOf<DiffMode>("DIFFMODE", Configuration.DiffMode.Serial);
@@ -35,7 +35,7 @@ public static class UpdateOptions
3535
public static UpdateOption<string> InstallPath { get; } = UpdateOption.ValueOf<string>("INSTALLPATH", AppContext.BaseDirectory);
3636
public static UpdateOption<string> ClientVersion { get; } = UpdateOption.ValueOf<string>("CLIENTVERSION", string.Empty);
3737
public static UpdateOption<string?> UpgradeClientVersion { get; } = UpdateOption.ValueOf<string?>("UPGRADECLIENTVERSION", null);
38-
public static UpdateOption<int?> Platform { get; } = UpdateOption.ValueOf<int?>("PLATFORM", null);
38+
public static UpdateOption<PlatformType?> Platform { get; } = UpdateOption.ValueOf<PlatformType?>("PLATFORM", null);
3939
public static UpdateOption<bool> SilentAutoInstall { get; } = UpdateOption.ValueOf<bool>("SILENTAUTOINSTALL", false);
4040
public static UpdateOption<int> SilentPollIntervalMinutes { get; } = UpdateOption.ValueOf<int>("SILENTPOLLINTERVALMINUTES", 60);
4141
public static UpdateOption<int> MaxConcurrency { get; } = UpdateOption.ValueOf<int>("MAXCONCURRENCY", 3);
@@ -49,7 +49,7 @@ public static class UpdateOptions
4949
public static UpdateOption<string?> Token { get; } = UpdateOption.ValueOf<string?>("TOKEN", null);
5050

5151
// ═══ OSS ═══
52-
public static UpdateOption<int?> OSSProvider { get; } = UpdateOption.ValueOf<int?>("OSSPROVIDER", null);
52+
public static UpdateOption<OssProvider?> OSSProvider { get; } = UpdateOption.ValueOf<OssProvider?>("OSSPROVIDER", null);
5353
public static UpdateOption<string?> OSSBucketRegion { get; } = UpdateOption.ValueOf<string?>("OSSBUCKETREGION", null);
5454

5555
// ═══ Blacklist ═══

src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public record UpdateReport(
2222
string FromVersion,
2323
string? ToVersion,
2424
UpdateEvent Event,
25-
int AppType,
25+
AppType AppType,
2626
DateTimeOffset Timestamp,
2727
string? ErrorMessage = null,
2828
double? DurationMs = null
@@ -63,7 +63,6 @@ public async Task ReportAsync(UpdateReport report, CancellationToken token = def
6363
}
6464
catch (Exception ex)
6565
{
66-
// Silent failure — reporting should never break the update flow
6766
GeneralTracer.Warn($"Report failed: {ex.Message}");
6867
}
6968
}

src/c#/GeneralUpdate.Core/Download/Sources/HttpDownloadSource.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class HttpDownloadSource : Abstractions.IDownloadSource
1818
private readonly string _clientVersion;
1919
private readonly string? _upgradeClientVersion;
2020
private readonly string _appSecretKey;
21-
private readonly int _platform;
21+
private readonly PlatformType _platform;
2222
private readonly string? _productId;
2323
private readonly string? _scheme;
2424
private readonly string? _token;
@@ -28,7 +28,7 @@ public HttpDownloadSource(
2828
string clientVersion,
2929
string? upgradeClientVersion,
3030
string appSecretKey,
31-
int platform,
31+
PlatformType platform,
3232
string? productId,
3333
string? scheme,
3434
string? token)
@@ -47,12 +47,12 @@ public HttpDownloadSource(
4747
public async Task<IReadOnlyList<DownloadAsset>> ListAsync(CancellationToken token = default)
4848
{
4949
var mainResp = await VersionService.Validate(
50-
_updateUrl, _clientVersion, AppType.ClientApp,
50+
_updateUrl, _clientVersion, AppType.Client,
5151
_appSecretKey, _platform, _productId,
5252
_scheme, _token, token);
5353

5454
var upgradeResp = await VersionService.Validate(
55-
_updateUrl, _upgradeClientVersion ?? _clientVersion, AppType.UpgradeApp,
55+
_updateUrl, _upgradeClientVersion ?? _clientVersion, AppType.Upgrade,
5656
_appSecretKey, _platform, _productId,
5757
_scheme, _token, token);
5858

src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public record UpdateContext(
2121
string InstallPath,
2222
string CurrentVersion,
2323
string? TargetVersion,
24-
int AppType
24+
Configuration.AppType AppType
2525
);
2626

2727
public record DownloadContext(

src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ public Task SendAsync(ProcessInfo info, CancellationToken token = default)
139139
{
140140
return Task.FromResult<ProcessInfo?>(null);
141141
}
142+
catch (DirectoryNotFoundException)
143+
{
144+
return Task.FromResult<ProcessInfo?>(null);
145+
}
146+
catch (Exception ex) when (ex is not OutOfMemoryException)
147+
{
148+
// Platform-specific failures (e.g. Linux /dev/shm not mounted)
149+
GeneralTracer.Warn($"SharedMemoryProvider: receive failed: {ex.Message}");
150+
return Task.FromResult<ProcessInfo?>(null);
151+
}
142152
}
143153
}
144154

0 commit comments

Comments
 (0)