Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 12 additions & 0 deletions src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,18 @@ private void ApplyRuntimeOptions()
_configInfo.Encoding = GetOption(UpdateOptions.Encoding);
_configInfo.Format = GetOption(UpdateOptions.Format);
_configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;

// Download behaviour
_configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency);
_configInfo.EnableResume = GetOption(UpdateOptions.EnableResume);
_configInfo.RetryCount = GetOption(UpdateOptions.RetryCount);
_configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval);
_configInfo.VerifyChecksum = GetOption(UpdateOptions.VerifyChecksum);

// Update behaviour
_configInfo.BackupEnabled = GetOption(UpdateOptions.BackupEnabled);
_configInfo.PatchEnabled = GetOption(UpdateOptions.PatchEnabled);
_configInfo.DiffMode = GetOption(UpdateOptions.DiffMode);
}

/// <summary>
Expand Down
48 changes: 47 additions & 1 deletion src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;

Expand Down Expand Up @@ -104,7 +105,7 @@
/// Directory path containing driver files for update.
/// Used when DriveEnabled is true to locate driver files for installation.
/// </summary>
public string DriverDirectory { get; set; }

Check warning on line 108 in src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs

View workflow job for this annotation

GitHub Actions / aot-verify

'GlobalConfigInfo.DriverDirectory' hides inherited member 'BaseConfigInfo.DriverDirectory'. Use the new keyword if hiding was intended.

Check warning on line 108 in src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest)

'GlobalConfigInfo.DriverDirectory' hides inherited member 'BaseConfigInfo.DriverDirectory'. Use the new keyword if hiding was intended.

Check warning on line 108 in src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest)

'GlobalConfigInfo.DriverDirectory' hides inherited member 'BaseConfigInfo.DriverDirectory'. Use the new keyword if hiding was intended.

/// <summary>
/// Indicates whether differential patch update is enabled.
Expand All @@ -112,9 +113,54 @@
/// </summary>
public bool? PatchEnabled { get; set; }

/// <summary>
/// Whether to back up the current version before applying an update.
/// Computed from UpdateOption.BackupEnabled, defaults to true.
/// </summary>
public bool? BackupEnabled { get; set; }

/// <summary>
/// Directory path where the current version files are backed up before update.
/// Computed by combining InstallPath with a versioned directory name.
/// </summary>
public string BackupDirectory { get; set; }
}

// ═══ Download/update behaviour options (wired from UpdateOptions) ═══

/// <summary>
/// Maximum number of concurrent download operations.
/// Computed from UpdateOption.MaxConcurrency, defaults to 3.
/// Valid range: 1 to <see cref="Environment.ProcessorCount"/> * 2.
/// </summary>
public int MaxConcurrency { get; set; } = 3;

/// <summary>
/// Whether to resume interrupted downloads via HTTP Range requests.
/// Computed from UpdateOption.EnableResume, defaults to true.
/// </summary>
public bool EnableResume { get; set; } = true;

/// <summary>
/// Maximum number of retry attempts for failed download operations.
/// Computed from UpdateOption.RetryCount, defaults to 3.
/// </summary>
public int RetryCount { get; set; } = 3;

/// <summary>
/// Initial retry interval for exponential back-off.
/// Computed from UpdateOption.RetryInterval, defaults to 1 second.
/// </summary>
public TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds(1);

/// <summary>
/// Whether to perform SHA256 checksum verification after download.
/// Computed from UpdateOption.VerifyChecksum, defaults to true.
/// </summary>
public bool VerifyChecksum { get; set; } = true;

/// <summary>
/// Diff/patch generation mode — Serial or Parallel.
/// Computed from UpdateOption.DiffMode, defaults to <see cref="Configuration.DiffMode.Serial"/>.
/// </summary>
public DiffMode DiffMode { get; set; } = DiffMode.Serial;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@
namespace GeneralUpdate.Core.Download.Executors;

/// <summary>
/// HTTP-based download executor with Range/resume support.
/// HTTP-based download executor with optional Range/resume support.
/// Uses the shared HttpClient from VersionService for consistent SSL/auth handling.
/// </summary>
public class HttpDownloadExecutor : IDownloadExecutor
{
private readonly HttpClient _client;
private readonly TimeSpan _timeout;
private readonly bool _enableResume;

public HttpDownloadExecutor(HttpClient client, TimeSpan? timeout = null)
public HttpDownloadExecutor(HttpClient client, TimeSpan? timeout = null, bool enableResume = true)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_timeout = timeout ?? TimeSpan.FromSeconds(30);
_enableResume = enableResume;
}

public async Task<DownloadResult> ExecuteAsync(
Expand All @@ -34,8 +36,8 @@ public async Task<DownloadResult> ExecuteAsync(
long totalBytes = -1;
long existingBytes = 0;

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

// Request resume from existing position
if (existingBytes > 0)
// Request resume from existing position (skip when resume is disabled)
if (_enableResume && existingBytes > 0)
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(existingBytes, null);

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

// If server doesn't support Range, discard partial file
if (existingBytes > 0 && response.StatusCode != System.Net.HttpStatusCode.PartialContent)
if (_enableResume && existingBytes > 0 && response.StatusCode != System.Net.HttpStatusCode.PartialContent)
{
existingBytes = 0;
File.Delete(destPath);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;

namespace GeneralUpdate.Core.Download.Models;

/// <summary>
/// Bundles all configurable download behaviour options into a single value object.
/// Used by <see cref="Orchestrators.DefaultDownloadOrchestrator"/> and
/// <see cref="Executors.HttpDownloadExecutor"/> to avoid constructor parameter explosion.
/// </summary>
public class DownloadOrchestratorOptions
{
/// <summary>
/// Maximum number of concurrent download operations.
/// Valid range: 1 to <see cref="Environment.ProcessorCount"/> * 2.
/// Default: 3.
/// </summary>
public int MaxConcurrency { get; set; } = 3;

/// <summary>
/// Whether to resume interrupted downloads via HTTP Range requests.
/// Default: true.
/// </summary>
public bool EnableResume { get; set; } = true;

/// <summary>
/// Maximum number of retry attempts for failed download operations.
/// Default: 3.
/// </summary>
public int RetryCount { get; set; } = 3;

/// <summary>
/// Initial retry interval for exponential back-off.
/// Actual delay before N-th retry = <c>RetryInterval * 2^(N-1)</c>.
/// Default: 1 second.
/// </summary>
public TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds(1);

/// <summary>
/// Whether to perform SHA256 checksum verification after download.
/// Default: true.
/// </summary>
public bool VerifyChecksum { get; set; } = true;

/// <summary>
/// Diff/patch generation mode — Serial or Parallel.
/// When <see cref="Configuration.DiffMode.Serial"/>, <see cref="MaxConcurrency"/> is forced to 1.
/// Default: <see cref="Configuration.DiffMode.Serial"/>.
/// </summary>
public Configuration.DiffMode DiffMode { get; set; } = Configuration.DiffMode.Serial;

/// <summary>
/// HTTP download timeout duration.
/// Default: 30 seconds.
/// </summary>
public TimeSpan DownloadTimeout { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Creates a <see cref="DownloadOrchestratorOptions"/> from <see cref="Configuration.GlobalConfigInfo"/>.
/// </summary>
public static DownloadOrchestratorOptions From(Configuration.GlobalConfigInfo config)
{
return new DownloadOrchestratorOptions
{
MaxConcurrency = SanitizeMaxConcurrency(config.MaxConcurrency),
EnableResume = config.EnableResume,
RetryCount = Math.Max(0, config.RetryCount),
RetryInterval = config.RetryInterval,
VerifyChecksum = config.VerifyChecksum,
DiffMode = config.DiffMode,
DownloadTimeout = TimeSpan.FromSeconds(config.DownloadTimeOut > 0 ? config.DownloadTimeOut : 30),
};
}

/// <summary>Clamps <paramref name="value"/> to [1, ProcessorCount * 2].</summary>
public static int SanitizeMaxConcurrency(int value)
{
var max = Math.Max(1, Environment.ProcessorCount * 2);
if (value < 1) return 1;
if (value > max) return max;
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Download.Abstractions;
using GeneralUpdate.Core.Download.Executors;
using GeneralUpdate.Core.Download.Policy;
Expand All @@ -16,17 +17,22 @@ namespace GeneralUpdate.Core.Download.Orchestrators;

/// <summary>
/// Default download orchestrator with parallel execution, concurrency limit,
/// SHA256 verification, and progress reporting.
/// SHA256 verification, resume support, and progress reporting.
///
/// All configurable behaviour is driven by <see cref="DownloadOrchestratorOptions"/>,
/// which maps to the <see cref="UpdateOptions"/> defined in the bootstrap layer.
/// </summary>
public class DefaultDownloadOrchestrator : IDownloadOrchestrator
{
private readonly HttpClient _httpClient;
private readonly IDownloadPolicy _policy;
private readonly DownloadOrchestratorOptions _options;

public DefaultDownloadOrchestrator(HttpClient httpClient, IDownloadPolicy? policy = null)
public DefaultDownloadOrchestrator(HttpClient httpClient, DownloadOrchestratorOptions? options = null, IDownloadPolicy? policy = null)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_policy = policy ?? new DefaultRetryPolicy();
_options = options ?? new DownloadOrchestratorOptions();
_policy = policy ?? new DefaultRetryPolicy(_options.RetryCount, _options.RetryInterval);
}

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

Directory.CreateDirectory(destDir);

// Resolve effective concurrency: Serial mode forces 1.
// Uses _options.MaxConcurrency as primary value; the method parameter
// maxConcurrency acts as an override (default 3).
var baseConcurrency = maxConcurrency > 0 ? maxConcurrency : _options.MaxConcurrency;
var effectiveConcurrency = _options.DiffMode == DiffMode.Serial
? 1
: DownloadOrchestratorOptions.SanitizeMaxConcurrency(Math.Max(1, baseConcurrency));

GeneralTracer.Info($"DefaultDownloadOrchestrator.ExecuteAsync: concurrency={effectiveConcurrency}, " +
$"resume={_options.EnableResume}, verifyChecksum={_options.VerifyChecksum}, diffMode={_options.DiffMode}");

var sw = Stopwatch.StartNew();
var results = new List<DownloadResult>();
using var sem = new SemaphoreSlim(maxConcurrency);
using var sem = new SemaphoreSlim(effectiveConcurrency);
long totalBytes = 0;

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

var executor = new HttpDownloadExecutor(_httpClient);
var executor = new HttpDownloadExecutor(_httpClient, _options.DownloadTimeout, _options.EnableResume);
var pipeline = new DefaultDownloadPipeline(asset.SHA256);

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

// Verify (SHA256)
// Verify (SHA256) — conditionally skipped when VerifyChecksum is false
if (!_options.VerifyChecksum)
{
GeneralTracer.Info($"DefaultDownloadOrchestrator: checksum verification skipped for {asset.Name} (VerifyChecksum=false).");
return downloadResult;
}

try
{
await pipeline.ProcessAsync(destPath, ct).ConfigureAwait(false);
Expand Down
16 changes: 12 additions & 4 deletions src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,20 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
BlackListManager.Instance.SkipDirectorys.ToList()),
ProcessInfoJsonContext.Default.ProcessInfo);

// Backup
Backup();
// Backup — conditionally skipped when BackupEnabled is false
if (_configInfo.BackupEnabled != false)
{
Backup();
}
else
{
GeneralTracer.Info("ClientUpdateStrategy: backup skipped (BackupEnabled=false).");
}

_osStrategy!.Create(_configInfo);

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