Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
40 changes: 28 additions & 12 deletions src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,22 +280,38 @@ private void InitializeFromEnvironment()
};
}

/// <summary>
/// Applies configured UpdateOptions to _configInfo using null-coalescing or
/// conditional assignment to avoid overwriting values already populated by
/// InitializeFromEnvironment() (Upgrade path) or GlobalConfigInfo defaults.
/// </summary>
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);
// Core runtime — use ??= so Upgrade path (InitializeFromEnvironment) values
// are not overwritten by GetOption defaults
_configInfo.Encoding ??= GetOption(UpdateOptions.Encoding);
_configInfo.Format ??= GetOption(UpdateOptions.Format);
if (_configInfo.DownloadTimeOut <= 0)
_configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;

// PatchEnabled / BackupEnabled: use ??= so null stays null
// (bool? with ??= means false from user is preserved, null gets default)
_configInfo.PatchEnabled ??= GetOption(UpdateOptions.PatchEnabled);
_configInfo.BackupEnabled ??= GetOption(UpdateOptions.BackupEnabled);

// Download behaviour — preserve existing values (Upgrade path or property initializers)
// and only apply UpdateOptions when the current value is at its unset default
if (_configInfo.MaxConcurrency <= 0)
_configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency);
if (_configInfo.RetryCount <= 0)
_configInfo.RetryCount = GetOption(UpdateOptions.RetryCount);
if (_configInfo.RetryInterval <= TimeSpan.Zero)
_configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval);

// Booleans: default "false" is meaningful, but property initializers set them to "true".
// Only apply from UpdateOptions when they differ from the property initializer default.
_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);
}

Expand Down
38 changes: 21 additions & 17 deletions src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,28 @@ public ClientUpdateStrategy UseUpdatePrecheck(Func<UpdateInfoEventArgs, bool> fu

private async Task ExecuteWorkflowAsync()
{
var defaultEncoding = Encoding.UTF8;
var defaultTimeout = 60;
if (true /* silent check would read from options */)
{
// Standard mode
await ExecuteStandardWorkflowAsync(defaultEncoding, defaultTimeout);
}
// Standard mode — silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync().
// Encoding, Format, and DownloadTimeOut are normally set by
// Bootstrap.ApplyRuntimeOptions(); the fallback ensures sensible defaults
// when the strategy is used without Bootstrap wiring.
ApplyStrategyDefaults();
await ExecuteStandardWorkflowAsync();
}

/// <summary>
/// Applies sensible fallback defaults for runtime options that may not
/// have been set by Bootstrap.ApplyRuntimeOptions(). Uses null-coalescing
/// so previously-assigned values (from UpdateOptions) are never overwritten.
/// </summary>
private void ApplyStrategyDefaults()
{
_configInfo!.Encoding ??= Encoding.UTF8;
_configInfo.Format ??= "ZIP";
if (_configInfo.DownloadTimeOut <= 0)
_configInfo.DownloadTimeOut = 60;
}

private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
private async Task ExecuteStandardWorkflowAsync()
{
GeneralTracer.Info($"ClientUpdateStrategy: validating client={_configInfo!.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}");

Expand Down Expand Up @@ -145,7 +157,6 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
await SafeReportUpdateStartedAsync(hooksCtx).ConfigureAwait(false);

InitBlackList();
ApplyRuntimeOptions(encoding, timeout);

_configInfo.TempPath = StorageManager.GetTempDirectory("main_temp");
_configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath,
Expand All @@ -172,7 +183,7 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
Hash = a.SHA256,
Url = a.Url,
Version = a.Version,
Format = "ZIP"
Format = _configInfo.Format ?? "ZIP"
}).ToList();
Comment on lines 165 to 172

_configInfo.ProcessInfo = JsonSerializer.Serialize(
Expand Down Expand Up @@ -236,13 +247,6 @@ private static IStrategy ResolveOsStrategy()
throw new PlatformNotSupportedException("The current operating system is not supported!");
}

private void ApplyRuntimeOptions(Encoding encoding, int timeout)
{
_configInfo!.Encoding = encoding;
_configInfo.Format = Format.ZIP;
_configInfo.DownloadTimeOut = timeout;
}

private void InitBlackList()
{
BlackListManager.Instance.AddBlackFiles(_configInfo!.BlackFiles);
Expand Down
7 changes: 5 additions & 2 deletions src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public class OSSUpdateStrategy : IStrategy
{
private GlobalConfigInfo? _configInfo;
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
private const int TimeOut = 60;
private const int DefaultTimeOut = 60;

private int ResolveTimeout()
=> _configInfo.DownloadTimeOut > 0 ? _configInfo.DownloadTimeOut : DefaultTimeOut;

/// <summary>Lifecycle hooks injected by the bootstrap.</summary>
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
Expand Down Expand Up @@ -157,7 +160,7 @@ private async Task DownloadAssetsAsync(List<DownloadAsset> assets)
}
else
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(TimeOut) };
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ResolveTimeout()) };
var orchestrator = new DefaultDownloadOrchestrator(httpClient);
await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false);
}
Expand Down
9 changes: 7 additions & 2 deletions src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,15 @@ private static IStrategy ResolveOsStrategy()
throw new PlatformNotSupportedException("The current operating system is not supported!");
}

/// <summary>
/// Applies sensible fallback defaults for runtime options that may not
/// have been set by Bootstrap.ApplyRuntimeOptions(). Uses null-coalescing
/// so previously-assigned values (from UpdateOptions) are never overwritten.
/// </summary>
private void ApplyRuntimeOptions()
{
_configInfo!.Encoding = Encoding.UTF8;
_configInfo.Format = Format.ZIP;
_configInfo!.Encoding ??= Encoding.UTF8;
_configInfo.Format ??= Format.ZIP;
}

// ════════════════════════════════════════════════════════════════
Expand Down
131 changes: 131 additions & 0 deletions tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Core;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.FileSystem;
Expand Down Expand Up @@ -78,6 +82,133 @@ [Fact] public void Hub_Configured() => Assert.NotNull(B().Option(UpdateOptions.H
new HubConfig { Url = "https://signalr.example.com/hub" }));
#endregion

#region Extension Injection — Hooks / Strategy / Policy / Differ / Pipeline / etc.

private sealed class StubHooks : GeneralUpdate.Core.Hooks.IUpdateHooks
{
public Task<bool> OnBeforeUpdateAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.FromResult(true);
public Task OnDownloadCompletedAsync(GeneralUpdate.Core.Hooks.DownloadContext ctx) => Task.CompletedTask;
public Task OnAfterUpdateAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.CompletedTask;
public Task OnUpdateErrorAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx, Exception ex) => Task.CompletedTask;
public Task OnBeforeStartAppAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.CompletedTask;
}

private sealed class StubStrategy : GeneralUpdate.Core.Strategy.IStrategy
{
public void Create(GlobalConfigInfo parameter) { }
public void Execute() { }
public Task ExecuteAsync() => Task.CompletedTask;
public void StartApp() { }
}

private sealed class StubSslPolicy : GeneralUpdate.Core.Security.ISslValidationPolicy
{
public bool ValidateCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate,
System.Security.Cryptography.X509Certificates.X509Chain? chain,
System.Net.Security.SslPolicyErrors sslPolicyErrors) => true;
}

private sealed class StubBinaryDiffer : GeneralUpdate.Core.Differential.IBinaryDiffer
{
public Task CleanAsync(string oldFilePath, string newFilePath, string patchFilePath,
CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task DirtyAsync(string oldFilePath, string newFilePath, string patchFilePath,
CancellationToken cancellationToken = default) => Task.CompletedTask;
}

private sealed class StubPipelineFactory : GeneralUpdate.Core.Pipeline.IUpdatePipelineFactory
{
public Task ExecutePipelineAsync(GeneralUpdate.Core.Pipeline.PipelineContext context, CancellationToken token = default) => Task.CompletedTask;
}

private sealed class StubDownloadPolicy : GeneralUpdate.Core.Download.Abstractions.IDownloadPolicy
{
public Task<T> ExecuteAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken token = default) => action(token);
}

private sealed class StubDownloadExecutor : GeneralUpdate.Core.Download.Abstractions.IDownloadExecutor
{
public Task<GeneralUpdate.Core.Download.Models.DownloadResult> ExecuteAsync(string url, string destPath,
IProgress<GeneralUpdate.Core.Download.Models.DownloadProgress>? progress = null, CancellationToken token = default)
=> Task.FromResult(new GeneralUpdate.Core.Download.Models.DownloadResult(url, destPath, 0, TimeSpan.Zero, 0, true, null));
}

private sealed class StubDownloadSource : GeneralUpdate.Core.Download.Abstractions.IDownloadSource
{
public Task<IReadOnlyList<GeneralUpdate.Core.Download.Models.DownloadAsset>> ListAsync(CancellationToken token = default)
=> Task.FromResult<IReadOnlyList<GeneralUpdate.Core.Download.Models.DownloadAsset>>(Array.Empty<GeneralUpdate.Core.Download.Models.DownloadAsset>());
}

private sealed class StubDownloadPipeline : GeneralUpdate.Core.Download.Abstractions.IDownloadPipeline
{
public Task<string> ProcessAsync(string downloadedPath, CancellationToken token = default) => Task.FromResult("");
}

private sealed class StubUpdateReporter : GeneralUpdate.Core.Download.Reporting.IUpdateReporter
{
public Task ReportAsync(GeneralUpdate.Core.Download.Reporting.UpdateReport report, CancellationToken token = default) => Task.CompletedTask;
}

private sealed class StubUpdateAuth : GeneralUpdate.Core.Security.IHttpAuthProvider
{
public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) => Task.CompletedTask;
}

private sealed class StubDownloadOrchestrator : GeneralUpdate.Core.Download.Abstractions.IDownloadOrchestrator
{
public Task<GeneralUpdate.Core.Download.Abstractions.DownloadReport> ExecuteAsync(
GeneralUpdate.Core.Download.Models.DownloadPlan plan, string destDir, int maxConcurrency = 3,
IProgress<GeneralUpdate.Core.Download.Models.DownloadProgress>? progress = null, CancellationToken token = default)
=> Task.FromResult(new GeneralUpdate.Core.Download.Abstractions.DownloadReport(Array.Empty<GeneralUpdate.Core.Download.Models.DownloadResult>(), 0, TimeSpan.Zero, 0, 0));
}

private sealed class StubCleanStrategy : GeneralUpdate.Core.Differential.ICleanStrategy
{
public Task ExecuteAsync(string sourcePath, string targetPath, string patchPath) => Task.CompletedTask;
}

private sealed class StubDirtyStrategy : GeneralUpdate.Core.Differential.IDirtyStrategy
{
public Task ExecuteAsync(string appPath, string patchPath) => Task.CompletedTask;
}

[Fact] public void Inject_Hooks() => Assert.NotNull(B().Hooks<StubHooks>());
[Fact] public void Inject_Strategy() => Assert.NotNull(B().Strategy<StubStrategy>());
[Fact] public void Inject_SslPolicy() => Assert.NotNull(B().SslPolicy<StubSslPolicy>());
[Fact] public void Inject_BinaryDiffer() => Assert.NotNull(B().BinaryDiffer<StubBinaryDiffer>());
[Fact] public void Inject_PipelineFactory() => Assert.NotNull(B().PipelineFactory<StubPipelineFactory>());
[Fact] public void Inject_DownloadPolicy() => Assert.NotNull(B().DownloadPolicy<StubDownloadPolicy>());
[Fact] public void Inject_DownloadExecutor() => Assert.NotNull(B().DownloadExecutor<StubDownloadExecutor>());
[Fact] public void Inject_DownloadSource() => Assert.NotNull(B().DownloadSource<StubDownloadSource>());
[Fact] public void Inject_DownloadPipeline() => Assert.NotNull(B().DownloadPipeline<StubDownloadPipeline>());
[Fact] public void Inject_UpdateReporter() => Assert.NotNull(B().UpdateReporter<StubUpdateReporter>());
[Fact] public void Inject_UpdateAuth() => Assert.NotNull(B().UpdateAuth<StubUpdateAuth>());
[Fact] public void Inject_DownloadOrchestrator() => Assert.NotNull(B().DownloadOrchestrator<StubDownloadOrchestrator>());
[Fact] public void Inject_CleanStrategy() => Assert.NotNull(B().CleanStrategy<StubCleanStrategy>());
[Fact] public void Inject_DirtyStrategy() => Assert.NotNull(B().DirtyStrategy<StubDirtyStrategy>());

[Fact]
public void Chain_AllExtensionsInjected()
{
var b = B()
.Hooks<StubHooks>()
.UpdateReporter<StubUpdateReporter>()
.DownloadPolicy<StubDownloadPolicy>()
.DownloadExecutor<StubDownloadExecutor>()
.DownloadSource<StubDownloadSource>()
.DownloadPipeline<StubDownloadPipeline>()
.DownloadOrchestrator<StubDownloadOrchestrator>()
.BinaryDiffer<StubBinaryDiffer>()
.CleanStrategy<StubCleanStrategy>()
.DirtyStrategy<StubDirtyStrategy>()
.SslPolicy<StubSslPolicy>()
.UpdateAuth<StubUpdateAuth>()
.PipelineFactory<StubPipelineFactory>()
.Strategy<StubStrategy>();
Assert.NotNull(b);
}
#endregion

#region Full Combination Chains
[Fact] public void Chain_AllFrameworkOptions()
{
Expand Down
Loading