Skip to content

Commit 3d12375

Browse files
authored
feat: wire UpdateOptions configuration to runtime behavior (#399)
* feat: wire UpdateOptions configuration to runtime behavior Closes #398 - Add DownloadOrchestratorOptions config class to bundle download settings - Extend GlobalConfigInfo with MaxConcurrency, EnableResume, RetryCount, RetryInterval, VerifyChecksum, BackupEnabled, DiffMode properties - Wire GeneralUpdateBootstrap.ApplyRuntimeOptions to read all 8 options - Wire MaxConcurrency through DownloadOrchestratorOptions -> orchestrator - Wire EnableResume through HttpDownloadExecutor (skip Range header when false) - Wire RetryCount + RetryInterval through DefaultRetryPolicy - Wire VerifyChecksum to conditionally skip SHA256 verification - Wire BackupEnabled to conditionally skip backup in ClientUpdateStrategy - Wire DiffMode to control Serial/Parallel download mode in orchestrator - Wire PatchEnabled assignment from UpdateOptions to GlobalConfigInfo - Add 56 unit tests across 3 test files covering all wired options * fix: address copilot review comments for PR #399 - Fix effectiveConcurrency to use _options.MaxConcurrency as primary value (method param maxConcurrency now acts as override for backward compat) - Fix OS-dependent /tmp path to use Path.GetTempPath() in test - Add ConcurrencyTracker to verify Serial mode peak concurrency <= 1 - Strengthen resume test assertion to exact file length (50 bytes) - Expose GetOption via TestableBootstrap subclass for value assertions - Replace weak Assert.NotNull with Assert.Equal value checks in Bootstrap tests
1 parent 341d00d commit 3d12375

9 files changed

Lines changed: 979 additions & 18 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,18 @@ private void ApplyRuntimeOptions()
285285
_configInfo.Encoding = GetOption(UpdateOptions.Encoding);
286286
_configInfo.Format = GetOption(UpdateOptions.Format);
287287
_configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;
288+
289+
// Download behaviour
290+
_configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency);
291+
_configInfo.EnableResume = GetOption(UpdateOptions.EnableResume);
292+
_configInfo.RetryCount = GetOption(UpdateOptions.RetryCount);
293+
_configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval);
294+
_configInfo.VerifyChecksum = GetOption(UpdateOptions.VerifyChecksum);
295+
296+
// Update behaviour
297+
_configInfo.BackupEnabled = GetOption(UpdateOptions.BackupEnabled);
298+
_configInfo.PatchEnabled = GetOption(UpdateOptions.PatchEnabled);
299+
_configInfo.DiffMode = GetOption(UpdateOptions.DiffMode);
288300
}
289301

290302
/// <summary>

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Text;
34

@@ -112,9 +113,54 @@ public class GlobalConfigInfo : BaseConfigInfo
112113
/// </summary>
113114
public bool? PatchEnabled { get; set; }
114115

116+
/// <summary>
117+
/// Whether to back up the current version before applying an update.
118+
/// Computed from UpdateOption.BackupEnabled, defaults to true.
119+
/// </summary>
120+
public bool? BackupEnabled { get; set; }
121+
115122
/// <summary>
116123
/// Directory path where the current version files are backed up before update.
117124
/// Computed by combining InstallPath with a versioned directory name.
118125
/// </summary>
119126
public string BackupDirectory { get; set; }
120-
}
127+
128+
// ═══ Download/update behaviour options (wired from UpdateOptions) ═══
129+
130+
/// <summary>
131+
/// Maximum number of concurrent download operations.
132+
/// Computed from UpdateOption.MaxConcurrency, defaults to 3.
133+
/// Valid range: 1 to <see cref="Environment.ProcessorCount"/> * 2.
134+
/// </summary>
135+
public int MaxConcurrency { get; set; } = 3;
136+
137+
/// <summary>
138+
/// Whether to resume interrupted downloads via HTTP Range requests.
139+
/// Computed from UpdateOption.EnableResume, defaults to true.
140+
/// </summary>
141+
public bool EnableResume { get; set; } = true;
142+
143+
/// <summary>
144+
/// Maximum number of retry attempts for failed download operations.
145+
/// Computed from UpdateOption.RetryCount, defaults to 3.
146+
/// </summary>
147+
public int RetryCount { get; set; } = 3;
148+
149+
/// <summary>
150+
/// Initial retry interval for exponential back-off.
151+
/// Computed from UpdateOption.RetryInterval, defaults to 1 second.
152+
/// </summary>
153+
public TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds(1);
154+
155+
/// <summary>
156+
/// Whether to perform SHA256 checksum verification after download.
157+
/// Computed from UpdateOption.VerifyChecksum, defaults to true.
158+
/// </summary>
159+
public bool VerifyChecksum { get; set; } = true;
160+
161+
/// <summary>
162+
/// Diff/patch generation mode — Serial or Parallel.
163+
/// Computed from UpdateOption.DiffMode, defaults to <see cref="Configuration.DiffMode.Serial"/>.
164+
/// </summary>
165+
public DiffMode DiffMode { get; set; } = DiffMode.Serial;
166+
}

src/c#/GeneralUpdate.Core/Download/Executors/HttpDownloadExecutor.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@
1010
namespace GeneralUpdate.Core.Download.Executors;
1111

1212
/// <summary>
13-
/// HTTP-based download executor with Range/resume support.
13+
/// HTTP-based download executor with optional Range/resume support.
1414
/// Uses the shared HttpClient from VersionService for consistent SSL/auth handling.
1515
/// </summary>
1616
public class HttpDownloadExecutor : IDownloadExecutor
1717
{
1818
private readonly HttpClient _client;
1919
private readonly TimeSpan _timeout;
20+
private readonly bool _enableResume;
2021

21-
public HttpDownloadExecutor(HttpClient client, TimeSpan? timeout = null)
22+
public HttpDownloadExecutor(HttpClient client, TimeSpan? timeout = null, bool enableResume = true)
2223
{
2324
_client = client ?? throw new ArgumentNullException(nameof(client));
2425
_timeout = timeout ?? TimeSpan.FromSeconds(30);
26+
_enableResume = enableResume;
2527
}
2628

2729
public async Task<DownloadResult> ExecuteAsync(
@@ -34,8 +36,8 @@ public async Task<DownloadResult> ExecuteAsync(
3436
long totalBytes = -1;
3537
long existingBytes = 0;
3638

37-
// Check for existing partial file (resume support)
38-
if (File.Exists(destPath))
39+
// Check for existing partial file (resume support; skip when disabled)
40+
if (_enableResume && File.Exists(destPath))
3941
{
4042
existingBytes = new FileInfo(destPath).Length;
4143
}
@@ -44,8 +46,8 @@ public async Task<DownloadResult> ExecuteAsync(
4446
{
4547
using var request = new HttpRequestMessage(HttpMethod.Get, url);
4648

47-
// Request resume from existing position
48-
if (existingBytes > 0)
49+
// Request resume from existing position (skip when resume is disabled)
50+
if (_enableResume && existingBytes > 0)
4951
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(existingBytes, null);
5052

5153
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
@@ -56,7 +58,7 @@ public async Task<DownloadResult> ExecuteAsync(
5658
.ConfigureAwait(false);
5759

5860
// If server doesn't support Range, discard partial file
59-
if (existingBytes > 0 && response.StatusCode != System.Net.HttpStatusCode.PartialContent)
61+
if (_enableResume && existingBytes > 0 && response.StatusCode != System.Net.HttpStatusCode.PartialContent)
6062
{
6163
existingBytes = 0;
6264
File.Delete(destPath);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
3+
namespace GeneralUpdate.Core.Download.Models;
4+
5+
/// <summary>
6+
/// Bundles all configurable download behaviour options into a single value object.
7+
/// Used by <see cref="Orchestrators.DefaultDownloadOrchestrator"/> and
8+
/// <see cref="Executors.HttpDownloadExecutor"/> to avoid constructor parameter explosion.
9+
/// </summary>
10+
public class DownloadOrchestratorOptions
11+
{
12+
/// <summary>
13+
/// Maximum number of concurrent download operations.
14+
/// Valid range: 1 to <see cref="Environment.ProcessorCount"/> * 2.
15+
/// Default: 3.
16+
/// </summary>
17+
public int MaxConcurrency { get; set; } = 3;
18+
19+
/// <summary>
20+
/// Whether to resume interrupted downloads via HTTP Range requests.
21+
/// Default: true.
22+
/// </summary>
23+
public bool EnableResume { get; set; } = true;
24+
25+
/// <summary>
26+
/// Maximum number of retry attempts for failed download operations.
27+
/// Default: 3.
28+
/// </summary>
29+
public int RetryCount { get; set; } = 3;
30+
31+
/// <summary>
32+
/// Initial retry interval for exponential back-off.
33+
/// Actual delay before N-th retry = <c>RetryInterval * 2^(N-1)</c>.
34+
/// Default: 1 second.
35+
/// </summary>
36+
public TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds(1);
37+
38+
/// <summary>
39+
/// Whether to perform SHA256 checksum verification after download.
40+
/// Default: true.
41+
/// </summary>
42+
public bool VerifyChecksum { get; set; } = true;
43+
44+
/// <summary>
45+
/// Diff/patch generation mode — Serial or Parallel.
46+
/// When <see cref="Configuration.DiffMode.Serial"/>, <see cref="MaxConcurrency"/> is forced to 1.
47+
/// Default: <see cref="Configuration.DiffMode.Serial"/>.
48+
/// </summary>
49+
public Configuration.DiffMode DiffMode { get; set; } = Configuration.DiffMode.Serial;
50+
51+
/// <summary>
52+
/// HTTP download timeout duration.
53+
/// Default: 30 seconds.
54+
/// </summary>
55+
public TimeSpan DownloadTimeout { get; set; } = TimeSpan.FromSeconds(30);
56+
57+
/// <summary>
58+
/// Creates a <see cref="DownloadOrchestratorOptions"/> from <see cref="Configuration.GlobalConfigInfo"/>.
59+
/// </summary>
60+
public static DownloadOrchestratorOptions From(Configuration.GlobalConfigInfo config)
61+
{
62+
return new DownloadOrchestratorOptions
63+
{
64+
MaxConcurrency = SanitizeMaxConcurrency(config.MaxConcurrency),
65+
EnableResume = config.EnableResume,
66+
RetryCount = Math.Max(0, config.RetryCount),
67+
RetryInterval = config.RetryInterval,
68+
VerifyChecksum = config.VerifyChecksum,
69+
DiffMode = config.DiffMode,
70+
DownloadTimeout = TimeSpan.FromSeconds(config.DownloadTimeOut > 0 ? config.DownloadTimeOut : 30),
71+
};
72+
}
73+
74+
/// <summary>Clamps <paramref name="value"/> to [1, ProcessorCount * 2].</summary>
75+
public static int SanitizeMaxConcurrency(int value)
76+
{
77+
var max = Math.Max(1, Environment.ProcessorCount * 2);
78+
if (value < 1) return 1;
79+
if (value > max) return max;
80+
return value;
81+
}
82+
}

src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net.Http;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using GeneralUpdate.Core.Configuration;
910
using GeneralUpdate.Core.Download.Abstractions;
1011
using GeneralUpdate.Core.Download.Executors;
1112
using GeneralUpdate.Core.Download.Policy;
@@ -16,17 +17,22 @@ namespace GeneralUpdate.Core.Download.Orchestrators;
1617

1718
/// <summary>
1819
/// Default download orchestrator with parallel execution, concurrency limit,
19-
/// SHA256 verification, and progress reporting.
20+
/// SHA256 verification, resume support, and progress reporting.
21+
///
22+
/// All configurable behaviour is driven by <see cref="DownloadOrchestratorOptions"/>,
23+
/// which maps to the <see cref="UpdateOptions"/> defined in the bootstrap layer.
2024
/// </summary>
2125
public class DefaultDownloadOrchestrator : IDownloadOrchestrator
2226
{
2327
private readonly HttpClient _httpClient;
2428
private readonly IDownloadPolicy _policy;
29+
private readonly DownloadOrchestratorOptions _options;
2530

26-
public DefaultDownloadOrchestrator(HttpClient httpClient, IDownloadPolicy? policy = null)
31+
public DefaultDownloadOrchestrator(HttpClient httpClient, DownloadOrchestratorOptions? options = null, IDownloadPolicy? policy = null)
2732
{
2833
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
29-
_policy = policy ?? new DefaultRetryPolicy();
34+
_options = options ?? new DownloadOrchestratorOptions();
35+
_policy = policy ?? new DefaultRetryPolicy(_options.RetryCount, _options.RetryInterval);
3036
}
3137

3238
/// <summary>Execute downloads for all assets in the plan.</summary>
@@ -42,9 +48,20 @@ public async Task<DownloadReport> ExecuteAsync(
4248

4349
Directory.CreateDirectory(destDir);
4450

51+
// Resolve effective concurrency: Serial mode forces 1.
52+
// Uses _options.MaxConcurrency as primary value; the method parameter
53+
// maxConcurrency acts as an override (default 3).
54+
var baseConcurrency = maxConcurrency > 0 ? maxConcurrency : _options.MaxConcurrency;
55+
var effectiveConcurrency = _options.DiffMode == DiffMode.Serial
56+
? 1
57+
: DownloadOrchestratorOptions.SanitizeMaxConcurrency(Math.Max(1, baseConcurrency));
58+
59+
GeneralTracer.Info($"DefaultDownloadOrchestrator.ExecuteAsync: concurrency={effectiveConcurrency}, " +
60+
$"resume={_options.EnableResume}, verifyChecksum={_options.VerifyChecksum}, diffMode={_options.DiffMode}");
61+
4562
var sw = Stopwatch.StartNew();
4663
var results = new List<DownloadResult>();
47-
using var sem = new SemaphoreSlim(maxConcurrency);
64+
using var sem = new SemaphoreSlim(effectiveConcurrency);
4865
long totalBytes = 0;
4966

5067
var tasks = plan.Assets.Select(async asset =>
@@ -55,7 +72,7 @@ public async Task<DownloadReport> ExecuteAsync(
5572
var fileName = GetFileName(asset);
5673
var destPath = Path.Combine(destDir, fileName);
5774

58-
var executor = new HttpDownloadExecutor(_httpClient);
75+
var executor = new HttpDownloadExecutor(_httpClient, _options.DownloadTimeout, _options.EnableResume);
5976
var pipeline = new DefaultDownloadPipeline(asset.SHA256);
6077

6178
var result = await _policy.ExecuteAsync(async ct =>
@@ -69,7 +86,13 @@ public async Task<DownloadReport> ExecuteAsync(
6986
if (!downloadResult.Success)
7087
return downloadResult;
7188

72-
// Verify (SHA256)
89+
// Verify (SHA256) — conditionally skipped when VerifyChecksum is false
90+
if (!_options.VerifyChecksum)
91+
{
92+
GeneralTracer.Info($"DefaultDownloadOrchestrator: checksum verification skipped for {asset.Name} (VerifyChecksum=false).");
93+
return downloadResult;
94+
}
95+
7396
try
7497
{
7598
await pipeline.ProcessAsync(destPath, ct).ConfigureAwait(false);

src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,20 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
183183
BlackListManager.Instance.SkipDirectorys.ToList()),
184184
ProcessInfoJsonContext.Default.ProcessInfo);
185185

186-
// Backup
187-
Backup();
186+
// Backup — conditionally skipped when BackupEnabled is false
187+
if (_configInfo.BackupEnabled != false)
188+
{
189+
Backup();
190+
}
191+
else
192+
{
193+
GeneralTracer.Info("ClientUpdateStrategy: backup skipped (BackupEnabled=false).");
194+
}
188195

189196
_osStrategy!.Create(_configInfo);
190197

191-
// Download via orchestrator
198+
// Download via orchestrator — wired with options from GlobalConfigInfo
199+
var orchOptions = Download.Models.DownloadOrchestratorOptions.From(_configInfo);
192200
GeneralTracer.Info($"ClientUpdateStrategy: downloading {downloadPlan.Assets.Count} asset(s).");
193201
if (_orchestrator != null)
194202
{
@@ -199,7 +207,7 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
199207
var httpClient = new System.Net.Http.HttpClient();
200208
try
201209
{
202-
var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator(httpClient);
210+
var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator(httpClient, orchOptions);
203211
await orchestrator.ExecuteAsync(downloadPlan, _configInfo.TempPath).ConfigureAwait(false);
204212
}
205213
finally { httpClient.Dispose(); }

0 commit comments

Comments
 (0)