Skip to content

Commit b8a7cef

Browse files
authored
Merge pull request #483 from GeneralLibrary/fix/silent-update
fix: silent update - manifest identity fields and upgrade process launch
2 parents 6501260 + d974b58 commit b8a7cef

6 files changed

Lines changed: 148 additions & 11 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ public class GeneralUpdateBootstrap : AbstractBootstrap<GeneralUpdateBootstrap,
7272
private Func<UpdateInfoEventArgs, bool>? _updatePrecheck;
7373
private CancellationTokenSource? _cts;
7474
private DiffPipelineBuilder? _diffPipelineBuilder;
75+
private Silent.SilentPollOrchestrator? _silentOrchestrator;
76+
77+
/// <summary>
78+
/// When silent mode is active, provides access to the background polling orchestrator.
79+
/// Returns <c>null</c> in non-silent modes or before <see cref="LaunchAsync"/> is called.
80+
/// </summary>
81+
public Silent.SilentPollOrchestrator? SilentOrchestrator => _silentOrchestrator;
7582

7683
public GeneralUpdateBootstrap()
7784
{
@@ -383,6 +390,7 @@ private async Task<GeneralUpdateBootstrap> LaunchSilentAsync()
383390
};
384391

385392
var orchestrator = new Silent.SilentPollOrchestrator(strategy, _configInfo, silentOptions);
393+
_silentOrchestrator = orchestrator;
386394
await orchestrator.StartAsync().ConfigureAwait(false);
387395
GeneralTracer.Info("GeneralUpdateBootstrap: silent update mode started, returning to caller.");
388396

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,26 @@ public static void Discover(UpdateContext context)
2525

2626
if (manifest == null) return;
2727

28-
// Only fill empty fields — caller-provided values take precedence.
29-
if (string.IsNullOrWhiteSpace(context.MainAppName) && !string.IsNullOrWhiteSpace(manifest.MainAppName))
28+
// Identity fields whose defaults are mere fallbacks, not explicit
29+
// user choices. If the manifest has a value, it MUST take precedence —
30+
// otherwise the default blocks the manifest value and causes issues:
31+
// • MainAppName "Client" → can't find the real executable
32+
// • UpdateAppName "Update.exe" → can't launch the upgrade process
33+
// • ClientVersion "1.0.0.0" → endless update loop (version never updates)
34+
if (!string.IsNullOrWhiteSpace(manifest.MainAppName))
3035
context.MainAppName = manifest.MainAppName;
31-
if (string.IsNullOrWhiteSpace(context.UpdateAppName) && !string.IsNullOrWhiteSpace(manifest.UpdateAppName))
36+
if (!string.IsNullOrWhiteSpace(manifest.UpdateAppName))
3237
context.UpdateAppName = manifest.UpdateAppName;
33-
if (string.IsNullOrWhiteSpace(context.ClientVersion) && !string.IsNullOrWhiteSpace(manifest.ClientVersion))
38+
if (!string.IsNullOrWhiteSpace(manifest.UpdatePath))
39+
context.UpdatePath = manifest.UpdatePath;
40+
if (!string.IsNullOrWhiteSpace(manifest.ClientVersion))
3441
context.ClientVersion = manifest.ClientVersion;
42+
43+
// Remaining fields — only fill when empty (caller-provided values win).
3544
if (string.IsNullOrWhiteSpace(context.UpgradeClientVersion) && !string.IsNullOrWhiteSpace(manifest.UpgradeClientVersion))
3645
context.UpgradeClientVersion = manifest.UpgradeClientVersion;
3746
if (string.IsNullOrWhiteSpace(context.ProductId) && !string.IsNullOrWhiteSpace(manifest.ProductId))
3847
context.ProductId = manifest.ProductId;
39-
if (string.IsNullOrWhiteSpace(context.UpdatePath) && !string.IsNullOrWhiteSpace(manifest.UpdatePath))
40-
context.UpdatePath = manifest.UpdatePath;
4148
if (context.AppType == null && !string.IsNullOrWhiteSpace(manifest.AppType)
4249
&& Enum.TryParse<AppType>(manifest.AppType, out var at))
4350
context.AppType = at;

src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,44 @@ private void OnProcessExit(object? sender, EventArgs e)
168168
}
169169
}
170170

171+
/// <summary>
172+
/// Explicitly attempts to launch the upgrade process if an update was prepared.
173+
/// Call this before process exit as a fallback for environments where
174+
/// <see cref="AppDomain.ProcessExit"/> does not fire reliably (e.g. console
175+
/// apps terminated via Ctrl+C).
176+
/// </summary>
177+
/// <returns><c>true</c> if the upgrade process was launched; <c>false</c> otherwise.</returns>
178+
public bool TryLaunchUpgrade()
179+
{
180+
if (Volatile.Read(ref _prepared) != 1 || Interlocked.Exchange(ref _updaterStarted, 1) == 1)
181+
return false;
182+
183+
try
184+
{
185+
if (!_strategy.HasPreparedClientUpdate)
186+
{
187+
GeneralTracer.Info("SilentPollOrchestrator: no client packages staged, skipping upgrade launch.");
188+
return false;
189+
}
190+
191+
_strategy.LaunchUpgradeProcessSync();
192+
GeneralTracer.Info("SilentPollOrchestrator: upgrade process launched via ClientStrategy (explicit).");
193+
return true;
194+
}
195+
catch (Exception ex)
196+
{
197+
GeneralTracer.Error("SilentPollOrchestrator: TryLaunchUpgrade failed.", ex);
198+
Console.Error.WriteLine($"[SilentPollOrchestrator] Failed to launch upgrade: {ex.Message}");
199+
return false;
200+
}
201+
}
202+
203+
/// <summary>
204+
/// Whether the orchestrator has prepared a client update and is waiting for
205+
/// process exit to launch the upgrade process.
206+
/// </summary>
207+
public bool HasPreparedUpdate => Volatile.Read(ref _prepared) == 1 && _strategy.HasPreparedClientUpdate;
208+
171209
/// <summary>
172210
/// Releases all resources used by the <see cref="SilentPollOrchestrator"/>.
173211
/// </summary>

tests/ClientTest/ClientTest.csproj

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,28 @@
1111
<ProjectReference Include="..\..\src\c#\GeneralUpdate.Core\GeneralUpdate.Core.csproj" />
1212
</ItemGroup>
1313

14+
<ItemGroup>
15+
<None Update="generalupdate.manifest.json">
16+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17+
</None>
18+
</ItemGroup>
19+
20+
<!-- Copy UpgradeTest output as Update.exe into ClientTest output for silent update testing -->
21+
<Target Name="CopyUpgradeTest" AfterTargets="Build">
22+
<PropertyGroup>
23+
<UpgradeTestDir>..\UpgradeTest\bin\$(Configuration)\net10.0</UpgradeTestDir>
24+
</PropertyGroup>
25+
<ItemGroup>
26+
<UpgradeTestFiles Include="$(UpgradeTestDir)\**\*" Exclude="$(UpgradeTestDir)\**\*.pdb;$(UpgradeTestDir)\**\*.json" />
27+
</ItemGroup>
28+
<Copy SourceFiles="@(UpgradeTestFiles)"
29+
DestinationFolder="$(OutputPath)"
30+
SkipUnchangedFiles="true"
31+
Condition="Exists('$(UpgradeTestDir)')" />
32+
<Exec Command="copy /Y &quot;$(OutputPath)UpgradeTest.exe&quot; &quot;$(OutputPath)Update.exe&quot;"
33+
Condition="Exists('$(OutputPath)UpgradeTest.exe') AND '$(OS)' == 'Windows_NT'" />
34+
<Message Text="[ClientTest] UpgradeTest files copied to output (Update.exe ready)." Importance="high"
35+
Condition="Exists('$(OutputPath)Update.exe')" />
36+
</Target>
37+
1438
</Project>

tests/ClientTest/Program.cs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
try
99
{
10-
Console.WriteLine("=== GeneralUpdate Client Test ===");
10+
Console.WriteLine("=== GeneralUpdate Client Test (Silent Mode) ===");
1111
Console.WriteLine($"Started at {DateTime.Now}");
1212
Console.WriteLine($"Running from: {AppDomain.CurrentDomain.BaseDirectory}");
1313

@@ -17,13 +17,15 @@
1717
var appSecretKey = Environment.GetEnvironmentVariable("APP_SECRET_KEY") ?? "dfeb5833-975e-4afb-88f1-6278ee9aeff6";
1818

1919
Console.WriteLine($"UpdateUrl: {updateUrl}");
20+
Console.WriteLine($"Silent mode: ENABLED (poll every 1 minute)");
2021
Console.WriteLine();
2122

22-
// Identity metadata (MainAppName, ClientVersion, UpdateAppName, UpdatePath, …)
23-
// is auto-discovered from generalupdate.manifest.json (generated by GeneralUpdate.Tools).
24-
await new GeneralUpdateBootstrap()
23+
// Silent mode: polls server in background, prepares update, launches Upgrade on exit.
24+
var bootstrap = await new GeneralUpdateBootstrap()
2525
.SetSource(updateUrl, appSecretKey, reportUrl)
2626
.SetOption(Option.AppType, AppType.Client)
27+
.SetOption(Option.Silent, true)
28+
.SetOption(Option.SilentPollIntervalMinutes, 1)
2729
.Hooks<ClientTestHooks>()
2830
.AddListenerMultiDownloadStatistics(OnDownloadStatistics)
2931
.AddListenerMultiDownloadCompleted(OnDownloadCompleted)
@@ -33,7 +35,56 @@
3335
.AddListenerUpdateInfo(OnUpdateInfo)
3436
.LaunchAsync();
3537

36-
Console.WriteLine("Client test completed.");
38+
var orchestrator = bootstrap.SilentOrchestrator;
39+
40+
Console.WriteLine();
41+
Console.WriteLine("╔════════════════════════════════════════════╗");
42+
Console.WriteLine("║ Silent poll running in background. ║");
43+
Console.WriteLine("║ Press Ctrl+C or Enter to exit. ║");
44+
Console.WriteLine("║ On exit, Upgrade process will be launched ║");
45+
Console.WriteLine("║ if an update has been prepared. ║");
46+
Console.WriteLine("╚════════════════════════════════════════════╝");
47+
Console.WriteLine();
48+
49+
// Keep the process alive so the background poll loop can work.
50+
// When the user presses Ctrl+C or Enter, the process exits and
51+
// ProcessExit fires, which triggers the upgrade launch.
52+
var cts = new CancellationTokenSource();
53+
Console.CancelKeyPress += (_, e) =>
54+
{
55+
Console.WriteLine();
56+
Console.WriteLine("[Shutdown] Ctrl+C pressed. Exiting...");
57+
e.Cancel = true; // Prevent immediate kill — let ProcessExit fire
58+
cts.Cancel();
59+
};
60+
61+
try
62+
{
63+
await Task.Delay(Timeout.Infinite, cts.Token);
64+
}
65+
catch (OperationCanceledException)
66+
{
67+
// Expected on Ctrl+C — graceful shutdown
68+
}
69+
70+
// Explicitly launch the upgrade process before exiting.
71+
// ProcessExit may not fire reliably in all scenarios (e.g. console Ctrl+C),
72+
// so we call TryLaunchUpgrade() directly as the primary launch path.
73+
// If ProcessExit also fires later, the _updaterStarted guard prevents a double-launch.
74+
Console.WriteLine("[Shutdown] Launching upgrade process...");
75+
if (orchestrator != null && orchestrator.HasPreparedUpdate)
76+
{
77+
var launched = orchestrator.TryLaunchUpgrade();
78+
Console.WriteLine(launched
79+
? "[Shutdown] Upgrade process launched successfully."
80+
: "[Shutdown] No update prepared or upgrade already launched.");
81+
}
82+
else
83+
{
84+
Console.WriteLine("[Shutdown] No orchestrator or no update prepared.");
85+
}
86+
87+
Console.WriteLine("[Shutdown] Client test exiting gracefully.");
3788
}
3889
catch (Exception ex)
3990
{
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"mainAppName": "ClientTest.exe",
3+
"clientVersion": "1.0.0",
4+
"appType": "Client",
5+
"updateAppName": "UpgradeTest.exe",
6+
"upgradeClientVersion": "2.0.0.0",
7+
"productId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a",
8+
"updatePath": "update/"
9+
}

0 commit comments

Comments
 (0)