diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs
index a3fe499d..4ab380f8 100644
--- a/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs
+++ b/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs
@@ -4,7 +4,7 @@ namespace DifferentialTest.Pipeline
{
///
/// 分支覆盖点:
- /// 1. MaxDegreeOfParallelism — 默认 = Environment.ProcessorCount
+ /// 1. MaxDegreeOfParallelism — 默认 = 2
/// 2. StopOnFirstError — 默认 = false
/// 3. DeletePatchAfterApply — 默认 = true
/// 4. 属性 set/get — 修改后正确返回
@@ -15,12 +15,12 @@ namespace DifferentialTest.Pipeline
///
public class DiffPipelineOptionsTests
{
- [Fact(DisplayName = "默认构造_MaxDegreeOfParallelism为Environment.ProcessorCount")]
- public void DefaultConstructor_MaxDegreeOfParallelism_EqualsProcessorCount()
+ [Fact(DisplayName = "默认构造_MaxDegreeOfParallelism为2")]
+ public void DefaultConstructor_MaxDegreeOfParallelism_Equals2()
{
var options = new DiffPipelineOptions();
- Assert.Equal(Environment.ProcessorCount, options.MaxDegreeOfParallelism);
+ Assert.Equal(2, options.MaxDegreeOfParallelism);
}
[Fact(DisplayName = "默认构造_StopOnFirstError为false")]
diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
index 12bc917b..f8c0c055 100644
--- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
+++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
@@ -65,6 +65,7 @@ public void Cancel()
public override async Task LaunchAsync()
{
var appType = GetOption(UpdateOptions.AppType);
+ _configInfo.AppType = appType;
// Silent mode: start background poll and return immediately
if (appType == AppType.Client && GetOption(UpdateOptions.Silent))
@@ -75,9 +76,9 @@ public override async Task LaunchAsync()
return appType switch
{
- AppType.Client => await LaunchWithStrategy(new ClientUpdateStrategy()),
+ AppType.Client => await LaunchWithStrategy(new ClientUpdateStrategy()),
AppType.Upgrade => await LaunchWithStrategy(new UpgradeUpdateStrategy()),
- AppType.OSSClient => await LaunchWithStrategy(new OSSUpdateStrategy(AppType.OSSClient)),
+ AppType.OSSClient => await LaunchWithStrategy(new OSSUpdateStrategy(AppType.OSSClient)),
AppType.OSSUpgrade => await LaunchWithStrategy(new OSSUpdateStrategy(AppType.OSSUpgrade)),
_ => await LaunchWithStrategy(new ClientUpdateStrategy())
};
@@ -94,68 +95,40 @@ private async Task LaunchWithStrategy(IStrategy roleStra
// Resolve hooks and reporter from extensions
var hooks = ResolveExtension() ?? new Hooks.NoOpUpdateHooks();
- var reporter = ResolveExtension() ?? new Download.Reporting.NoOpUpdateReporter();
+ var reporter = ResolveExtension() ??
+ new Download.Reporting.NoOpUpdateReporter();
- // Configure client-specific callbacks
- if (roleStrategy is ClientUpdateStrategy clientStrat)
- {
- clientStrat.Hooks = hooks;
- clientStrat.Reporter = reporter;
- // Resolve DownloadSource from extension registry (Hub, custom, etc.)
- var resolvedSource = ResolveExtension();
-
- // Inject SignalR Hub download source if configured
- if (resolvedSource == null)
- {
- var hubConfig = GetOption(UpdateOptions.Hub);
- if (hubConfig != null && !string.IsNullOrEmpty(hubConfig.Url))
- {
- var hubSource = new Download.Sources.HubDownloadSource(
- hubConfig.Url, _configInfo.Token, _configInfo.AppSecretKey);
- await hubSource.StartAsync().ConfigureAwait(false);
- resolvedSource = hubSource;
- GeneralTracer.Info("GeneralUpdateBootstrap: HubDownloadSource started from HubConfig.");
- }
- }
- clientStrat.DownloadSource = resolvedSource;
- if (_updatePrecheck != null)
- clientStrat.UseUpdatePrecheck(_updatePrecheck);
- await CallSmallBowlHomeAsync(_configInfo.Bowl).ConfigureAwait(false);
- }
- else if (roleStrategy is UpgradeUpdateStrategy upgradeStrat)
- {
- upgradeStrat.Hooks = hooks;
- upgradeStrat.Reporter = reporter;
- }
- else if (roleStrategy is OSSUpdateStrategy ossStrat)
- {
- ossStrat.Hooks = hooks;
- ossStrat.Reporter = reporter;
- }
+ // ── Phase 1: inject all dependencies before Create ──
+ roleStrategy.Hooks = hooks;
+ roleStrategy.Reporter = reporter;
- roleStrategy.Create(_configInfo);
-
var binaryDiffer = ResolveExtension();
var dirtyStrategy = ResolveExtension();
+ var diffPipeline = BuildDiffPipeline();
- if (roleStrategy is ClientUpdateStrategy cs2)
- {
- if (binaryDiffer != null) cs2.SetBinaryDiffer(binaryDiffer);
- if (dirtyStrategy != null) cs2.SetDirtyStrategy(dirtyStrategy);
- }
- else if (roleStrategy is UpgradeUpdateStrategy us2)
+ switch (roleStrategy)
{
- if (binaryDiffer != null) us2.SetBinaryDiffer(binaryDiffer);
- if (dirtyStrategy != null) us2.SetDirtyStrategy(dirtyStrategy);
+ case ClientUpdateStrategy cs:
+ cs.DownloadSource = ResolveExtension();
+
+ if (_updatePrecheck != null)
+ cs.UseUpdatePrecheck(_updatePrecheck);
+
+ await CallSmallBowlHomeAsync(_configInfo.Bowl).ConfigureAwait(false);
+
+ if (binaryDiffer != null) cs.SetBinaryDiffer(binaryDiffer);
+ if (dirtyStrategy != null) cs.SetDirtyStrategy(dirtyStrategy);
+ cs.SetDiffPipeline(diffPipeline);
+ break;
+
+ case UpgradeUpdateStrategy us:
+ if (binaryDiffer != null) us.SetBinaryDiffer(binaryDiffer);
+ if (dirtyStrategy != null) us.SetDirtyStrategy(dirtyStrategy);
+ us.SetDiffPipeline(diffPipeline);
+ break;
}
- // Build DiffPipeline — user‑configured or default with BsdiffDiffer,
- // parallelism=2, and progress reporter wired to AddListenerProgress.
- var diffPipeline = BuildDiffPipeline();
- if (roleStrategy is ClientUpdateStrategy cs3)
- cs3.SetDiffPipeline(diffPipeline);
- else if (roleStrategy is UpgradeUpdateStrategy us3)
- us3.SetDiffPipeline(diffPipeline);
+ roleStrategy.Create(_configInfo);
// Check custom skip condition before executing update
if (_customSkipOption?.Invoke() == true)
@@ -173,24 +146,21 @@ private async Task LaunchWithStrategy(IStrategy roleStra
}
finally
{
- // Dispose HubDownloadSource if it was started
- if (roleStrategy is ClientUpdateStrategy cs && cs.DownloadSource is IAsyncDisposable ad)
- await ad.DisposeAsync();
_cts?.Dispose();
_cts = null;
}
+
return this;
}
-
-
+
// ════════════════════════════════════════════════════════════════
// Configuration
// ════════════════════════════════════════════════════════════════
public GeneralUpdateBootstrap SetConfig(Configinfo configInfo)
- {
- configInfo.Validate();
- _configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo);
+ {
+ configInfo.Validate();
+ _configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo);
var appType = GetOption(UpdateOptions.AppType);
if (appType != AppType.Upgrade)
@@ -223,7 +193,7 @@ public GeneralUpdateBootstrap SetConfig(string filePath)
// Resolve filename-only paths to current directory
var hasPathChar = filePath.Contains(Path.DirectorySeparatorChar)
- || filePath.Contains(Path.AltDirectorySeparatorChar);
+ || filePath.Contains(Path.AltDirectorySeparatorChar);
var fullPath = hasPathChar
? Path.GetFullPath(filePath)
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePath);
@@ -277,16 +247,18 @@ private void InitializeFromEnvironment()
LastVersion = processInfo.LastVersion,
UpdateLogUrl = processInfo.UpdateLogUrl,
Encoding = Encoding.GetEncoding(processInfo.CompressEncoding),
- Format = processInfo.CompressFormat,
+ Format = ParseFormat(processInfo.CompressFormat),
DownloadTimeOut = processInfo.DownloadTimeOut,
AppSecretKey = processInfo.AppSecretKey,
UpdateVersions = processInfo.UpdateVersions,
- TempPath = StorageManager.GetTempDirectory("upgrade_temp"),
+ TempPath = processInfo.TempPath,
ReportUrl = processInfo.ReportUrl,
BackupDirectory = processInfo.BackupDirectory,
Scheme = processInfo.Scheme,
Token = processInfo.Token,
DriverDirectory = processInfo.DriverDirectory,
+ UpdatePath = processInfo.UpdatePath,
+ LaunchClientAfterUpdate = processInfo.LaunchClientAfterUpdate,
BlackFiles = processInfo.BlackFiles ?? BlackListDefaults.DefaultBlackFiles,
BlackFormats = processInfo.BlackFileFormats ?? BlackListDefaults.DefaultBlackFormats,
SkipDirectorys = processInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories
@@ -306,11 +278,7 @@ private void ApplyRuntimeOptions()
{
// Preserve Upgrade path values set by InitializeFromEnvironment()
_configInfo.Encoding ??= GetOption(UpdateOptions.Encoding);
- _configInfo.Format ??= GetOption(UpdateOptions.Format);
- // Normalize legacy "ZIP" default (UpdateOptions) to Format.ZIP (".zip")
- // so the pipeline constructs correct paths and CompressProvider matches its switch.
- if (_configInfo.Format == "ZIP")
- _configInfo.Format = Format.ZIP;
+ _configInfo.Format = GetOption(UpdateOptions.Format);
if (_configInfo.DownloadTimeOut <= 0)
_configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;
@@ -338,16 +306,15 @@ private async Task LaunchSilentAsync()
GeneralTracer.Info("GeneralUpdateBootstrap: starting silent update mode.");
var pollMinutes = GetOption(UpdateOptions.SilentPollIntervalMinutes);
- var autoInstall = GetOption(UpdateOptions.SilentAutoInstall);
var silentOptions = new Silent.SilentOptions
{
- PollInterval = TimeSpan.FromMinutes(pollMinutes),
- AutoInstall = autoInstall
+ PollInterval = TimeSpan.FromMinutes(pollMinutes)
};
var hooks = ResolveExtension() ?? new Hooks.NoOpUpdateHooks();
- var reporter = ResolveExtension() ?? new Download.Reporting.NoOpUpdateReporter();
+ var reporter = ResolveExtension() ??
+ new Download.Reporting.NoOpUpdateReporter();
var orchestrator = new Silent.SilentPollOrchestrator(_configInfo, silentOptions)
.WithHooks(hooks)
@@ -371,6 +338,16 @@ private DiffPipeline BuildDiffPipeline()
.Build();
}
+ private static Format ParseFormat(string? compressFormat)
+ {
+ if (string.IsNullOrWhiteSpace(compressFormat)) return Format.Zip;
+ return compressFormat switch
+ {
+ ".zip" => Format.Zip,
+ _ => Format.Zip
+ };
+ }
+
private void InitBlackList()
{
// Build blacklist matcher from GlobalConfigInfo and set on StorageManager.
@@ -378,7 +355,9 @@ private void InitBlackList()
var effectiveConfig = new BlackListConfig(
_configInfo.BlackFiles?.Count > 0 ? _configInfo.BlackFiles : BlackListDefaults.DefaultBlackFiles,
_configInfo.BlackFormats?.Count > 0 ? _configInfo.BlackFormats : BlackListDefaults.DefaultBlackFormats,
- _configInfo.SkipDirectorys?.Count > 0 ? _configInfo.SkipDirectorys : BlackListDefaults.DefaultSkipDirectories
+ _configInfo.SkipDirectorys?.Count > 0
+ ? _configInfo.SkipDirectorys
+ : BlackListDefaults.DefaultSkipDirectories
);
StorageManager.BlackListMatcher = new DefaultBlackListMatcher(effectiveConfig);
}
@@ -449,4 +428,4 @@ public GeneralUpdateBootstrap AddListenerProgress(
AddListener((s, e) => listener.OnProgress(e));
return this;
}
-}
+}
\ No newline at end of file
diff --git a/src/c#/GeneralUpdate.Core/Compress/CompressProvider.cs b/src/c#/GeneralUpdate.Core/Compress/CompressProvider.cs
index b975ea0d..d70db26a 100644
--- a/src/c#/GeneralUpdate.Core/Compress/CompressProvider.cs
+++ b/src/c#/GeneralUpdate.Core/Compress/CompressProvider.cs
@@ -8,21 +8,21 @@ public class CompressProvider
{
private CompressProvider() { }
- public static void Compress(string compressType, string sourcePath, string destinationPath, bool includeRootDirectory, Encoding encoding)
+ public static void Compress(Format compressType, string sourcePath, string destinationPath, bool includeRootDirectory, Encoding encoding)
{
var strategy = GetCompressionStrategy(compressType);
strategy.Compress(sourcePath, destinationPath, includeRootDirectory, encoding);
}
- public static void Decompress(string compressType, string archivePath, string destinationPath, Encoding encoding)
+ public static void Decompress(Format compressType, string archivePath, string destinationPath, Encoding encoding)
{
var strategy = GetCompressionStrategy(compressType);
strategy.Decompress(archivePath, destinationPath, encoding);
}
- private static ICompressionStrategy GetCompressionStrategy(string compressType) => compressType switch
+ private static ICompressionStrategy GetCompressionStrategy(Format compressType) => compressType switch
{
- Format.ZIP => new ZipCompressionStrategy(),
+ Format.Zip => new ZipCompressionStrategy(),
_ => throw new ArgumentException("Compression format is not supported!")
};
}
\ No newline at end of file
diff --git a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs
index 2790c66c..2a364b0e 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs
@@ -11,13 +11,14 @@
namespace GeneralUpdate.Core.Configuration
{
public abstract class AbstractBootstrap
- where TBootstrap : AbstractBootstrap
- where TStrategy : IStrategy
+ where TBootstrap : AbstractBootstrap
+ where TStrategy : IStrategy
{
private readonly ConcurrentDictionary _options;
/// User-registered extension types for lazy instantiation.
private readonly Dictionary _extensions = new();
+
/// Registered singleton instances (e.g., BlackListConfig).
private readonly Dictionary _instances = new();
@@ -48,47 +49,88 @@ protected T GetOption(UpdateOption? option)
// ═══════════ Extension point registration ═══════════
public TBootstrap Strategy() where T : IStrategy, new()
- { _extensions[typeof(IStrategy)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(IStrategy)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap Hooks() where T : Hooks.IUpdateHooks, new()
- { _extensions[typeof(Hooks.IUpdateHooks)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Hooks.IUpdateHooks)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap SslPolicy() where T : Security.ISslValidationPolicy, new()
- { _extensions[typeof(Security.ISslValidationPolicy)] = typeof(T); return (TBootstrap)this; }
-
-
+ {
+ _extensions[typeof(Security.ISslValidationPolicy)] = typeof(T);
+ return (TBootstrap)this;
+ }
+
public TBootstrap PipelineFactory() where T : Pipeline.IUpdatePipelineFactory, new()
- { _extensions[typeof(Pipeline.IUpdatePipelineFactory)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Pipeline.IUpdatePipelineFactory)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap DownloadPolicy() where T : Download.Abstractions.IDownloadPolicy, new()
- { _extensions[typeof(Download.Abstractions.IDownloadPolicy)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Download.Abstractions.IDownloadPolicy)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap DownloadExecutor() where T : Download.Abstractions.IDownloadExecutor, new()
- { _extensions[typeof(Download.Abstractions.IDownloadExecutor)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Download.Abstractions.IDownloadExecutor)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap DownloadSource() where T : Download.Abstractions.IDownloadSource, new()
- { _extensions[typeof(Download.Abstractions.IDownloadSource)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Download.Abstractions.IDownloadSource)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap DownloadPipeline() where T : Download.Abstractions.IDownloadPipeline, new()
- { _extensions[typeof(Download.Abstractions.IDownloadPipeline)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Download.Abstractions.IDownloadPipeline)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap UpdateReporter() where T : Download.Reporting.IUpdateReporter, new()
- { _extensions[typeof(Download.Reporting.IUpdateReporter)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Download.Reporting.IUpdateReporter)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap UpdateAuth() where T : Security.IHttpAuthProvider, new()
- { _extensions[typeof(Security.IHttpAuthProvider)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Security.IHttpAuthProvider)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap DownloadOrchestrator() where T : Download.Abstractions.IDownloadOrchestrator, new()
- { _extensions[typeof(Download.Abstractions.IDownloadOrchestrator)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Download.Abstractions.IDownloadOrchestrator)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap CleanStrategy() where T : Differential.ICleanStrategy, new()
- { _extensions[typeof(Differential.ICleanStrategy)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Differential.ICleanStrategy)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap DirtyStrategy() where T : Differential.IDirtyStrategy, new()
- { _extensions[typeof(Differential.IDirtyStrategy)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(Differential.IDirtyStrategy)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap BinaryDiffer() where T : IBinaryDiffer, new()
- { _extensions[typeof(IBinaryDiffer)] = typeof(T); return (TBootstrap)this; }
+ {
+ _extensions[typeof(IBinaryDiffer)] = typeof(T);
+ return (TBootstrap)this;
+ }
public TBootstrap ConfigureBlackList(BlackListConfig config)
{
@@ -117,4 +159,4 @@ public TBootstrap ConfigureBlackList(Action c
return null;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/c#/GeneralUpdate.Core/Configuration/BaseConfigInfo.cs b/src/c#/GeneralUpdate.Core/Configuration/BaseConfigInfo.cs
index d805fedd..fa497ec3 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/BaseConfigInfo.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/BaseConfigInfo.cs
@@ -11,14 +11,14 @@ namespace GeneralUpdate.Core.Configuration
public abstract class BaseConfigInfo
{
///
- /// The name of the application that needs to be started after update.
- /// This is the executable name without extension (e.g., "MyApp" for MyApp.exe).
+ /// The name of the upgrade application executable (e.g., "Update.exe").
+ /// Used when the client launches the upgrade process.
/// Default value is "Update.exe".
///
- public string AppName { get; set; } = "Update.exe";
+ public string UpdateAppName { get; set; } = "Update.exe";
///
- /// The name of the main application without file extension.
+ /// The name of the main application executable.
/// Used to identify the primary application process that will be updated.
///
public string MainAppName { get; set; }
@@ -30,6 +30,15 @@ public abstract class BaseConfigInfo
///
public string InstallPath { get; set; } = AppDomain.CurrentDomain.BaseDirectory;
+ ///
+ /// Optional directory where the upgrade executable resides.
+ /// Can be an absolute path or a relative path (resolved against ).
+ /// When set, the upgrade process is launched from this directory instead of .
+ /// When null or empty, falls back to (backward-compatible).
+ /// Example: "Upgrade" → InstallPath/Upgrade/UpdateAppName
+ ///
+ public string UpdatePath { get; set; }
+
///
/// The URL address for the update log webpage.
/// Users can view detailed changelog information at this address.
@@ -95,5 +104,12 @@ public abstract class BaseConfigInfo
/// Used when DriveEnabled is true to locate and install driver files during updates.
///
public string DriverDirectory { get; set; }
+
+ ///
+ /// Current update role — determines which app launches.
+ /// launches UpdateAppName (upgrade process).
+ /// launches MainAppName + Bowl.
+ ///
+ public AppType? AppType { get; set; }
}
}
diff --git a/src/c#/GeneralUpdate.Core/Configuration/Configinfo.cs b/src/c#/GeneralUpdate.Core/Configuration/Configinfo.cs
index 5b7a2626..f580b6bf 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/Configinfo.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/Configinfo.cs
@@ -36,8 +36,8 @@ public void Validate()
if (!string.IsNullOrWhiteSpace(UpdateLogUrl) && !Uri.IsWellFormedUriString(UpdateLogUrl, UriKind.Absolute))
throw new ArgumentException("Invalid UpdateLogUrl");
- if (string.IsNullOrWhiteSpace(AppName))
- throw new ArgumentException("AppName cannot be empty");
+ if (string.IsNullOrWhiteSpace(UpdateAppName))
+ throw new ArgumentException("UpdateAppName cannot be empty");
if (string.IsNullOrWhiteSpace(MainAppName))
throw new ArgumentException("MainAppName cannot be empty");
diff --git a/src/c#/GeneralUpdate.Core/Configuration/ConfiginfoBuilder.cs b/src/c#/GeneralUpdate.Core/Configuration/ConfiginfoBuilder.cs
index ab072d20..f8c54823 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/ConfiginfoBuilder.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/ConfiginfoBuilder.cs
@@ -14,7 +14,7 @@ namespace GeneralUpdate.Core.Configuration
public class ConfiginfoBuilder
{
// Configurable default values
- // Note: AppName and InstallPath defaults are set in Configinfo class itself
+ // Note: UpdateAppName and InstallPath defaults are set in Configinfo class itself
// These are ConfiginfoBuilder-specific defaults to support the builder pattern
private string _updateUrl;
private string _token;
@@ -91,8 +91,8 @@ private static ConfiginfoBuilder LoadFromConfigFile()
builder.SetToken(config.Token);
if (!string.IsNullOrWhiteSpace(config.Scheme))
builder.SetScheme(config.Scheme);
- if (!string.IsNullOrWhiteSpace(config.AppName))
- builder.SetAppName(config.AppName);
+ if (!string.IsNullOrWhiteSpace(config.UpdateAppName))
+ builder.SetUpgradeAppName(config.UpdateAppName);
if (!string.IsNullOrWhiteSpace(config.MainAppName))
builder.SetMainAppName(config.MainAppName);
if (!string.IsNullOrWhiteSpace(config.ClientVersion))
@@ -177,10 +177,10 @@ public ConfiginfoBuilder SetScheme(string scheme)
///
/// The name of the application executable.
/// The current ConfiginfoBuilder instance for method chaining.
- public ConfiginfoBuilder SetAppName(string appName)
+ public ConfiginfoBuilder SetUpgradeAppName(string appName)
{
if (string.IsNullOrWhiteSpace(appName))
- throw new ArgumentException("AppName cannot be null or empty.", nameof(appName));
+ throw new ArgumentException("UpdateAppName cannot be null or empty.", nameof(appName));
_appName = appName;
return this;
@@ -369,7 +369,7 @@ public Configinfo Build()
UpdateUrl = _updateUrl,
Token = _token,
Scheme = _scheme,
- AppName = _appName,
+ UpdateAppName = _appName,
MainAppName = _mainAppName,
ClientVersion = _clientVersion,
UpgradeClientVersion = _upgradeClientVersion,
diff --git a/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs b/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs
index d4a89fdc..9b7ddbec 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs
@@ -29,7 +29,7 @@ public static GlobalConfigInfo MapToGlobalConfigInfo(Configinfo source, GlobalCo
return target;
// Map common fields from base configuration
- target.AppName = source.AppName;
+ target.UpdateAppName = source.UpdateAppName;
target.MainAppName = source.MainAppName;
target.ClientVersion = source.ClientVersion;
target.InstallPath = source.InstallPath;
@@ -43,6 +43,8 @@ public static GlobalConfigInfo MapToGlobalConfigInfo(Configinfo source, GlobalCo
target.Scheme = source.Scheme;
target.Token = source.Token;
target.DriverDirectory = source.DriverDirectory;
+ target.AppType = source.AppType;
+ target.UpdatePath = source.UpdatePath;
// Map GlobalConfigInfo-specific fields
target.UpdateUrl = source.UpdateUrl;
@@ -76,13 +78,13 @@ public static ProcessInfo MapToProcessInfo(
// Create ProcessInfo with all required parameters in a single location
// Centralized parameter mapping for ProcessInfo creation
return new ProcessInfo(
- appName: source.MainAppName, // Maps MainAppName to ProcessInfo.AppName
+ appName: source.MainAppName, // Maps MainAppName to ProcessInfo.UpdateAppName
installPath: source.InstallPath,
currentVersion: source.ClientVersion, // Maps ClientVersion to ProcessInfo.CurrentVersion
lastVersion: source.LastVersion, // Computed value set before calling this method
updateLogUrl: source.UpdateLogUrl,
compressEncoding: source.Encoding, // Computed value set before calling this method
- compressFormat: source.Format, // Computed value set before calling this method
+ compressFormat: source.Format.ToExtension(), // Computed value set before calling this method
downloadTimeOut: source.DownloadTimeOut, // Computed value set before calling this method
appSecretKey: source.AppSecretKey,
updateVersions: updateVersions, // From API response
@@ -92,9 +94,12 @@ public static ProcessInfo MapToProcessInfo(
scheme: source.Scheme,
token: source.Token,
driverDirectory: source.DriverDirectory, // Driver directory for driver updates
+ tempPath: source.TempPath, // Client's download temp path so upgrade can find packages
blackFileFormats: blackFileFormats, // From BlackListManager
blackFiles: blackFiles, // From BlackListManager
- skipDirectories: skipDirectories // From BlackListManager
+ skipDirectories: skipDirectories, // From BlackListManager
+ upgradePath: source.UpdatePath, // Custom upgrade directory
+ launchClient: source.LaunchClientAfterUpdate
);
}
@@ -113,7 +118,7 @@ public static void CopyBaseFields(TSource source, TTarget targ
if (source == null || target == null)
return;
- target.AppName = source.AppName;
+ target.UpdateAppName = source.UpdateAppName;
target.MainAppName = source.MainAppName;
target.InstallPath = source.InstallPath;
target.UpdateLogUrl = source.UpdateLogUrl;
@@ -127,6 +132,8 @@ public static void CopyBaseFields(TSource source, TTarget targ
target.Scheme = source.Scheme;
target.Token = source.Token;
target.DriverDirectory = source.DriverDirectory;
+ target.AppType = source.AppType;
+ target.UpdatePath = source.UpdatePath;
}
}
}
diff --git a/src/c#/GeneralUpdate.Core/Configuration/Format.cs b/src/c#/GeneralUpdate.Core/Configuration/Format.cs
index b46115d5..aa434eec 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/Format.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/Format.cs
@@ -1,11 +1,26 @@
namespace GeneralUpdate.Core.Configuration
{
///
- /// Compression format constants for update packages.
+ /// Compression format for update packages.
///
- public class Format
+ public enum Format
{
- /// ZIP compression format extension.
- public const string ZIP = ".zip";
+ /// ZIP compression format.
+ Zip
}
-}
\ No newline at end of file
+
+ ///
+ /// Extension methods for .
+ ///
+ public static class FormatExtensions
+ {
+ ///
+ /// Returns the file extension for the format (including leading dot).
+ ///
+ public static string ToExtension(this Format format) => format switch
+ {
+ Format.Zip => ".zip",
+ _ => ".zip"
+ };
+ }
+}
diff --git a/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs b/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs
index 03a478fe..8707782a 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfo.cs
@@ -42,10 +42,10 @@ public class GlobalConfigInfo : BaseConfigInfo
// Runtime computed fields (calculated during workflow execution)
///
- /// The compression format of update packages (e.g., "ZIP", "7Z").
+ /// The compression format of update packages.
/// Computed from UpdateOption.Format or defaults to ZIP.
///
- public string Format { get; set; }
+ public Format Format { get; set; }
///
/// Indicates whether the upgrade application itself needs to be updated.
@@ -59,6 +59,12 @@ public class GlobalConfigInfo : BaseConfigInfo
///
public bool IsMainUpdate { get; set; }
+ ///
+ /// Whether to launch the client app after the upgrade process completes.
+ /// Default true. Set to false in silent mode when the caller wants to control restart timing.
+ ///
+ public bool LaunchClientAfterUpdate { get; set; } = true;
+
///
/// List of version information objects to be updated.
/// Populated from the update server response based on IsUpgradeUpdate/IsMainUpdate flags.
@@ -117,10 +123,10 @@ public class GlobalConfigInfo : BaseConfigInfo
///
/// Maximum number of concurrent download operations.
- /// Computed from UpdateOption.MaxConcurrency, defaults to 3.
+ /// Computed from UpdateOption.MaxConcurrency, defaults to 2.
/// Valid range: 1 to * 2.
///
- public int MaxConcurrency { get; set; } = 3;
+ public int MaxConcurrency { get; set; } = 2;
///
/// Whether to resume interrupted downloads via HTTP Range requests.
diff --git a/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfoOSS.cs b/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfoOSS.cs
index 9cd3723b..9a4b31cb 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfoOSS.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/GlobalConfigInfoOSS.cs
@@ -8,8 +8,8 @@ public class GlobalConfigInfoOSS
[JsonPropertyName("Url")]
public string Url { get; set; }
- [JsonPropertyName("AppName")]
- public string AppName { get; set; }
+ [JsonPropertyName("UpdateAppName")]
+ public string UpdateAppName { get; set; }
[JsonPropertyName("CurrentVersion")]
public string CurrentVersion { get; set; }
@@ -27,7 +27,7 @@ public GlobalConfigInfoOSS()
public GlobalConfigInfoOSS(string url, string appName, string currentVersion, string versionFileName)
{
Url = url ?? throw new ArgumentNullException(nameof(url));
- AppName = appName ?? throw new ArgumentNullException(nameof(appName));
+ UpdateAppName = appName ?? throw new ArgumentNullException(nameof(appName));
CurrentVersion = currentVersion ?? throw new ArgumentNullException(nameof(currentVersion));
VersionFileName = versionFileName ?? "versions.json";
}
diff --git a/src/c#/GeneralUpdate.Core/Configuration/ProcessInfo.cs b/src/c#/GeneralUpdate.Core/Configuration/ProcessInfo.cs
index 376b8438..8c5510d4 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/ProcessInfo.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/ProcessInfo.cs
@@ -43,9 +43,12 @@ public ProcessInfo() { }
/// The URL scheme for update requests
/// The authentication token
/// The directory path containing driver files
+ /// The temp directory where the client downloaded update packages
/// List of file format extensions to skip
/// List of specific files to skip
/// List of directories to skip
+ /// Optional directory where the upgrade executable resides (defaults to InstallPath)
+ /// Whether to launch the client app after upgrade completes (default true)
/// Thrown when required parameters are null
/// Thrown when parameters fail validation
public ProcessInfo(string appName
@@ -64,9 +67,12 @@ public ProcessInfo(string appName
, string scheme
, string token
, string driverDirectory
+ , string tempPath
, List blackFileFormats
, List blackFiles
- , List skipDirectories)
+ , List skipDirectories
+ , string upgradePath = null
+ , bool launchClient = true)
{
// Validate required string parameters
AppName = appName ?? throw new ArgumentNullException(nameof(appName));
@@ -98,18 +104,25 @@ public ProcessInfo(string appName
Scheme = scheme;
Token = token;
DriverDirectory = driverDirectory;
+ TempPath = tempPath;
// Set blacklist parameters
BlackFileFormats = blackFileFormats;
BlackFiles = blackFiles;
SkipDirectorys = skipDirectories;
+
+ // Set upgrade path (optional — defaults to InstallPath if not set)
+ UpdatePath = upgradePath;
+
+ // Set launch flag (default true — backward compatible)
+ LaunchClientAfterUpdate = launchClient;
}
///
/// The name of the application to start after the update completes.
/// Note: In ProcessInfo, this field holds the MainAppName value from other config classes.
///
- [JsonPropertyName("AppName")]
+ [JsonPropertyName("UpdateAppName")]
public string AppName { get; set; }
///
@@ -229,5 +242,26 @@ public ProcessInfo(string appName
///
[JsonPropertyName("DriverDirectory")]
public string DriverDirectory { get; set; }
+
+ ///
+ /// The temp directory where the client downloaded update packages.
+ /// The upgrade process reads packages from this path via the pipeline.
+ ///
+ [JsonPropertyName("TempPath")]
+ public string TempPath { get; set; }
+
+ ///
+ /// Optional directory where the upgrade executable resides.
+ /// When set, the upgrade process is launched from this path instead of .
+ ///
+ [JsonPropertyName("UpdatePath")]
+ public string UpdatePath { get; set; }
+
+ ///
+ /// Whether to launch the client application after the upgrade process finishes.
+ /// Default true. Set to false to keep the app stopped after update.
+ ///
+ [JsonPropertyName("LaunchClientAfterUpdate")]
+ public bool LaunchClientAfterUpdate { get; set; } = true;
}
}
\ No newline at end of file
diff --git a/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs b/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs
index 171967e4..f99a1027 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/UpdateOptions.cs
@@ -23,8 +23,8 @@ public static class UpdateOptions
/// Compression encoding for update packages.
public static UpdateOption Encoding { get; } = UpdateOption.ValueOf("COMPRESSENCODING", System.Text.Encoding.UTF8);
- /// Compression format (e.g., "ZIP").
- public static UpdateOption Format { get; } = UpdateOption.ValueOf("COMPRESSFORMAT", "ZIP");
+ /// Compression format for update packages.
+ public static UpdateOption Format { get; } = UpdateOption.ValueOf("COMPRESSFORMAT", Configuration.Format.Zip);
/// Download timeout in seconds.
public static UpdateOption DownloadTimeout { get; } = UpdateOption.ValueOf("DOWNLOADTIMEOUT", 30);
@@ -39,9 +39,6 @@ public static class UpdateOptions
public static UpdateOption Silent { get; } = UpdateOption.ValueOf("ENABLESILENTUPDATE", false);
// ═══ Silent mode ═══
- /// Whether silent updates auto-install without user intervention.
- public static UpdateOption SilentAutoInstall { get; } = UpdateOption.ValueOf("SILENTAUTOINSTALL", false);
-
/// Polling interval in minutes for silent update checks.
public static UpdateOption SilentPollIntervalMinutes { get; } = UpdateOption.ValueOf("SILENTPOLLINTERVALMINUTES", 60);
@@ -61,9 +58,5 @@ public static class UpdateOptions
/// Initial retry interval for exponential back-off. Default 1 second.
public static UpdateOption RetryInterval { get; } = UpdateOption.ValueOf("RETRYINTERVAL", TimeSpan.FromSeconds(1));
-
- // ═══ SignalR Hub ═══
- /// SignalR Hub configuration for push-based updates.
- public static UpdateOption Hub { get; } = UpdateOption.ValueOf("HUB", null);
}
}
diff --git a/src/c#/GeneralUpdate.Core/Configuration/VersionInfo.cs b/src/c#/GeneralUpdate.Core/Configuration/VersionInfo.cs
index eb2f1457..7943390b 100644
--- a/src/c#/GeneralUpdate.Core/Configuration/VersionInfo.cs
+++ b/src/c#/GeneralUpdate.Core/Configuration/VersionInfo.cs
@@ -58,4 +58,28 @@ public class VersionInfo
///
[JsonPropertyName("updateLog")]
public string? UpdateLog { get; set; }
+
+ /// URL expiry time (UTC) for signed download URLs.
+ [JsonPropertyName("urlExpireTimeUtc")]
+ public DateTime? UrlExpireTimeUtc { get; set; }
+
+ /// Upgrade mode: 1=VersionChain, 2=CrossVersion.
+ [JsonPropertyName("upgradeMode")]
+ public int? UpgradeMode { get; set; }
+
+ /// Whether this is a cross-version packet.
+ [JsonPropertyName("isCrossVersion")]
+ public bool? IsCrossVersion { get; set; }
+
+ /// Source version for cross-version packets.
+ [JsonPropertyName("fromVersion")]
+ public string? FromVersion { get; set; }
+
+ /// Target version for cross-version packets.
+ [JsonPropertyName("toVersion")]
+ public string? ToVersion { get; set; }
+
+ /// Whether this packet is frozen (archived, not for active updates).
+ [JsonPropertyName("isFreeze")]
+ public bool? IsFreeze { get; set; }
}
\ No newline at end of file
diff --git a/src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadSource.cs b/src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadSource.cs
index f5a989a1..4f9be7eb 100644
--- a/src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadSource.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadSource.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Core.Download.Models;
@@ -8,7 +7,7 @@ namespace GeneralUpdate.Core.Download.Abstractions;
/// Source of download assets (e.g. HTTP API, OSS bucket, SignalR Hub).
public interface IDownloadSource
{
- Task> ListAsync(CancellationToken token = default);
+ Task ListAsync(CancellationToken token = default);
}
/// Post-download processing pipeline (verify, decompress, decrypt).
diff --git a/src/c#/GeneralUpdate.Core/Download/Abstractions/PacketDTO.cs b/src/c#/GeneralUpdate.Core/Download/Abstractions/PacketDTO.cs
deleted file mode 100644
index acb67bf8..00000000
--- a/src/c#/GeneralUpdate.Core/Download/Abstractions/PacketDTO.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace GeneralUpdate.Core.Download.Abstractions;
-
-/// Server-side packet descriptor DTO (mirrors server contract).
-public record PacketDTO
-{
- public string? Name { get; set; }
- public string? Hash { get; set; }
- public DateTime? ReleaseDate { get; set; }
- public string? Url { get; set; }
- public string? Version { get; set; }
- public int? AppType { get; set; }
- public int? Platform { get; set; }
- public string? ProductId { get; set; }
- public bool? IsForcibly { get; set; }
- public bool? IsFreeze { get; set; }
- public string? Format { get; set; }
- public long? Size { get; set; }
- public string? FromVersion { get; set; }
- public bool? IsCrossVersion { get; set; }
- public string? MinClientVersion { get; set; }
- public string? SourceArchiveHash { get; set; }
- public string? TargetArchiveHash { get; set; }
-}
-
-public record VersionRequest(
- string AppName,
- string ClientVersion,
- string? UpgradeClientVersion,
- int? Platform,
- string? ProductId
-);
-
-public record VersionResponse(
- bool HasUpdate,
- IReadOnlyList? Packets
-);
diff --git a/src/c#/GeneralUpdate.Core/Download/DownloadPlanBuilder.cs b/src/c#/GeneralUpdate.Core/Download/DownloadPlanBuilder.cs
index 37c11abd..36ae174e 100644
--- a/src/c#/GeneralUpdate.Core/Download/DownloadPlanBuilder.cs
+++ b/src/c#/GeneralUpdate.Core/Download/DownloadPlanBuilder.cs
@@ -8,13 +8,16 @@ namespace GeneralUpdate.Core.Download;
///
/// Builds a DownloadPlan from download assets.
-/// Handles cross-version package selection, version chain building,
-/// frozen package filtering, and forced update marking.
+/// Handles frozen package filtering, forced update marking, and MinClientVersion compatibility.
+/// Does not distinguish between cross-version and version-chain — each package carries its own metadata.
///
public static class DownloadPlanBuilder
{
///
/// Build a download plan from a list of download assets.
+ /// Does not distinguish between cross-version and version-chain packages —
+ /// the server decides what to return, and each package carries its own
+ /// for downstream handling.
///
/// Assets from the download source.
/// Current client version string.
@@ -34,45 +37,22 @@ public static DownloadPlan Build(IEnumerable assets, string curre
// 2. Check for forced update
var isForcibly = active.Any(a => a.IsForcibly);
- // 3. Look for a cross-version package that matches our current version
- var crossVersion = active
- .Where(a => a.IsCrossVersion
- && !string.IsNullOrEmpty(a.FromVersion)
- && VersionEquals(a.FromVersion!, currentVersion))
- .OrderByDescending(a => ParseVersion(a.Version))
- .FirstOrDefault();
-
- if (crossVersion != null)
- {
- // Single download — jump directly to target version
- return new DownloadPlan(new[] { crossVersion }, isForcibly);
- }
-
- // 4. Build version chain from non-cross-version packages
- var chain = BuildVersionChain(active.Where(a => !a.IsCrossVersion), currentVersion);
- if (chain.Count == 0) return DownloadPlan.Empty;
-
- return new DownloadPlan(chain, isForcibly);
- }
-
- ///
- /// Build a version chain: keep versions higher than current,
- /// check MinClientVersion compatibility.
- ///
- private static List BuildVersionChain(IEnumerable assets, string currentVersion)
- {
- var current = ParseVersion(currentVersion);
-
- return assets
+ // 3. Filter and sort: keep only packages higher than current version,
+ // respecting MinClientVersion compatibility.
+ var candidates = active
.Where(a =>
{
var pv = ParseVersion(a.Version);
if (pv == null) return false;
- return pv > current;
+ return pv > ParseVersion(currentVersion);
})
.Where(a => IsCompatible(a.MinClientVersion, currentVersion))
.OrderBy(a => ParseVersion(a.Version))
.ToList();
+
+ if (candidates.Count == 0) return DownloadPlan.Empty;
+
+ return new DownloadPlan(candidates, isForcibly);
}
///
@@ -88,37 +68,10 @@ private static bool IsCompatible(string? minClientVersion, string currentVersion
return cur >= min;
}
- /// Map a PacketDTO to a DownloadAsset. Public for use by download sources.
- public static DownloadAsset MapToAsset(Abstractions.PacketDTO p)
- {
- return new DownloadAsset(
- Name: p.Name ?? p.Version ?? "unknown",
- Url: p.Url ?? string.Empty,
- Size: p.Size ?? 0,
- SHA256: p.Hash,
- Version: p.Version ?? "0.0.0",
- IsCrossVersion: p.IsCrossVersion == true,
- FromVersion: p.FromVersion,
- MinClientVersion: p.MinClientVersion,
- SourceArchiveHash: p.SourceArchiveHash,
- TargetArchiveHash: p.TargetArchiveHash,
- IsForcibly: p.IsForcibly == true,
- IsFreeze: p.IsFreeze == true
- );
- }
-
/// Parse a version string, returning null on failure.
private static Version? ParseVersion(string? version)
{
if (string.IsNullOrWhiteSpace(version)) return null;
return Version.TryParse(version, out var v) ? v : null;
}
-
- /// Compare two version strings for equality.
- private static bool VersionEquals(string a, string b)
- {
- var va = ParseVersion(a);
- var vb = ParseVersion(b);
- return va != null && vb != null && va == vb;
- }
}
diff --git a/src/c#/GeneralUpdate.Core/Download/Executors/HttpDownloadExecutor.cs b/src/c#/GeneralUpdate.Core/Download/Executors/HttpDownloadExecutor.cs
index fb0e7d62..916b5baf 100644
--- a/src/c#/GeneralUpdate.Core/Download/Executors/HttpDownloadExecutor.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Executors/HttpDownloadExecutor.cs
@@ -46,6 +46,13 @@ public async Task ExecuteAsync(
if (_enableResume && existingBytes > 0)
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(existingBytes, null);
+ // Apply per-asset auth if provided by server (e.g. GeneralSpacestation signed URLs or Bearer tokens)
+ if (!string.IsNullOrEmpty(asset.AuthScheme) && !string.IsNullOrEmpty(asset.AuthToken))
+ {
+ request.Headers.Authorization =
+ new System.Net.Http.Headers.AuthenticationHeaderValue(asset.AuthScheme, asset.AuthToken);
+ }
+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
cts.CancelAfter(_timeout);
diff --git a/src/c#/GeneralUpdate.Core/Download/Models/DownloadAsset.cs b/src/c#/GeneralUpdate.Core/Download/Models/DownloadAsset.cs
index b14ee548..e5369770 100644
--- a/src/c#/GeneralUpdate.Core/Download/Models/DownloadAsset.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Models/DownloadAsset.cs
@@ -2,7 +2,7 @@
namespace GeneralUpdate.Core.Download.Models;
-/// Download resource descriptor — maps from server PacketDTO.
+/// Download resource descriptor — maps from server PacketDTO / VerificationResultDTO.
public record DownloadAsset(
string Name,
string Url,
@@ -16,7 +16,12 @@ public record DownloadAsset(
string? SourceArchiveHash = null,
string? TargetArchiveHash = null,
bool IsForcibly = false,
- bool IsFreeze = false
+ bool IsFreeze = false,
+ int RecordId = 0,
+ int? UpgradeMode = null,
+ int? AppType = null,
+ string? AuthScheme = null,
+ string? AuthToken = null
);
/// Ordered download plan built from server response.
diff --git a/src/c#/GeneralUpdate.Core/Download/Models/DownloadOrchestratorOptions.cs b/src/c#/GeneralUpdate.Core/Download/Models/DownloadOrchestratorOptions.cs
index 7870df70..560ac95f 100644
--- a/src/c#/GeneralUpdate.Core/Download/Models/DownloadOrchestratorOptions.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Models/DownloadOrchestratorOptions.cs
@@ -12,9 +12,9 @@ public class DownloadOrchestratorOptions
///
/// Maximum number of concurrent download operations.
/// Valid range: 1 to * 2.
- /// Default: 3.
+ /// Default: 2.
///
- public int MaxConcurrency { get; set; } = 3;
+ public int MaxConcurrency { get; set; } = 2;
///
/// Whether to resume interrupted downloads via HTTP Range requests.
@@ -54,6 +54,13 @@ public class DownloadOrchestratorOptions
///
public TimeSpan DownloadTimeout { get; set; } = TimeSpan.FromSeconds(30);
+ ///
+ /// File format for downloaded packages.
+ /// Ensures the downloaded filename matches what the pipeline expects.
+ /// Default: .
+ ///
+ public Configuration.Format Format { get; set; } = Configuration.Format.Zip;
+
///
/// Creates a from .
///
@@ -68,6 +75,7 @@ public static DownloadOrchestratorOptions From(Configuration.GlobalConfigInfo co
VerifyChecksum = config.VerifyChecksum,
DiffMode = config.DiffMode,
DownloadTimeout = TimeSpan.FromSeconds(config.DownloadTimeOut > 0 ? config.DownloadTimeOut : 30),
+ Format = config.Format,
};
}
diff --git a/src/c#/GeneralUpdate.Core/Download/Models/DownloadSourceResult.cs b/src/c#/GeneralUpdate.Core/Download/Models/DownloadSourceResult.cs
new file mode 100644
index 00000000..1a35ddbe
--- /dev/null
+++ b/src/c#/GeneralUpdate.Core/Download/Models/DownloadSourceResult.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+
+namespace GeneralUpdate.Core.Download.Models;
+
+///
+/// Result from ,
+/// carrying the download assets plus flags indicating which side (main/upgrade)
+/// the server returned version information for.
+///
+public class DownloadSourceResult
+{
+ public IReadOnlyList Assets { get; init; } = Array.Empty();
+ public bool HasMainUpdate { get; init; }
+ public bool HasUpgradeUpdate { get; init; }
+}
diff --git a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs
index bafad077..fa1cdac4 100644
--- a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs
@@ -145,8 +145,17 @@ public async Task ExecuteAsync(
results.Count(r => !r.Success));
}
- private static string GetFileName(DownloadAsset asset)
+ private string GetFileName(DownloadAsset asset)
{
+ if (!string.IsNullOrEmpty(asset.Name))
+ {
+ var name = asset.Name;
+ var ext = _options.Format.ToExtension();
+ if (!name.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
+ name += ext;
+ return name;
+ }
+
try
{
var name = Path.GetFileName(new Uri(asset.Url).AbsolutePath);
diff --git a/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs b/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs
index 4fed106c..764fef69 100644
--- a/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs
@@ -5,60 +5,42 @@
using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Core;
-using GeneralUpdate.Core.Configuration;
namespace GeneralUpdate.Core.Download.Reporting;
-/// Reports update lifecycle events to the server.
+/// Reports update lifecycle events to the server, compatible with GeneralSpacestation API.
public interface IUpdateReporter
{
Task ReportAsync(UpdateReport report, CancellationToken token = default);
}
-public enum UpdateEvent { UpdateStarted, DownloadCompleted, UpdateApplied, UpdateFailed, AppStarted }
+/// Update status codes matching GeneralSpacestation ReportDTO contract (1=updating, 2=success, 3=failure).
+public enum UpdateStatus { Updating = 1, Success = 2, Failure = 3 }
-public record UpdateReport(
- string AppName,
- string FromVersion,
- string? ToVersion,
- UpdateEvent Event,
- AppType AppType,
- DateTimeOffset Timestamp,
- string? ErrorMessage = null,
- double? DurationMs = null
-);
+/// Spacestation-compatible update report: recordId from verification, status (1=updating,2=success,3=failure), type (1=upgrade,2=push).
+public record UpdateReport(int RecordId, int Status = 1, int Type = 1);
-/// HTTP POST reporter with optional HMAC signing.
+/// HTTP POST reporter that serializes UpdateReport as JSON matching Spacestation ReportDTO.
public class HttpUpdateReporter : IUpdateReporter
{
private readonly HttpClient _client;
private readonly string _reportUrl;
- private readonly string? _secretKey;
- public HttpUpdateReporter(HttpClient client, string reportUrl, string? secretKey = null)
+ public HttpUpdateReporter(HttpClient client, string reportUrl)
{
_client = client;
_reportUrl = reportUrl;
- _secretKey = secretKey;
}
public async Task ReportAsync(UpdateReport report, CancellationToken token = default)
{
try
{
- var json = JsonSerializer.Serialize(report);
+ var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
using var request = new HttpRequestMessage(HttpMethod.Post, _reportUrl);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
- if (!string.IsNullOrEmpty(_secretKey))
- {
- var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
- var sig = ComputeHmac($"{json}|{ts}", _secretKey);
- request.Headers.Add("X-Update-Timestamp", ts);
- request.Headers.Add("X-Update-Signature", sig);
- }
-
await _client.SendAsync(request, token).ConfigureAwait(false);
}
catch (Exception ex)
@@ -66,13 +48,6 @@ public async Task ReportAsync(UpdateReport report, CancellationToken token = def
GeneralTracer.Warn($"Report failed: {ex.Message}");
}
}
-
- private static string ComputeHmac(string data, string key)
- {
- var h = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes(key))
- .ComputeHash(Encoding.UTF8.GetBytes(data));
- return BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
- }
}
/// No-op reporter used when ReportUrl is not configured.
diff --git a/src/c#/GeneralUpdate.Core/Download/Sources/HttpDownloadSource.cs b/src/c#/GeneralUpdate.Core/Download/Sources/HttpDownloadSource.cs
index 8dc0d25c..b6f4342b 100644
--- a/src/c#/GeneralUpdate.Core/Download/Sources/HttpDownloadSource.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Sources/HttpDownloadSource.cs
@@ -43,8 +43,8 @@ public HttpDownloadSource(
_token = token;
}
- /// Call version API and return download assets.
- public async Task> ListAsync(CancellationToken token = default)
+ /// Call version API and return download assets with per-side validation flags.
+ public async Task ListAsync(CancellationToken token = default)
{
var mainResp = await VersionService.Validate(
_updateUrl, _clientVersion, AppType.Client,
@@ -56,6 +56,9 @@ public async Task> ListAsync(CancellationToken toke
_appSecretKey, _platform, _productId,
_scheme, _token, token);
+ var hasMainUpdate = mainResp?.Body?.Count > 0;
+ var hasUpgradeUpdate = upgradeResp?.Body?.Count > 0;
+
var assets = new List();
if (mainResp?.Body != null)
@@ -70,7 +73,13 @@ public async Task> ListAsync(CancellationToken toke
assets.Add(MapVersionInfo(v));
}
- return assets;
+ // Deduplicate by URL — both Validate calls may return the same packages
+ return new DownloadSourceResult
+ {
+ Assets = assets.GroupBy(a => a.Url).Select(g => g.First()).ToList(),
+ HasMainUpdate = hasMainUpdate,
+ HasUpgradeUpdate = hasUpgradeUpdate
+ };
}
private static DownloadAsset MapVersionInfo(VersionInfo v)
@@ -81,7 +90,16 @@ private static DownloadAsset MapVersionInfo(VersionInfo v)
Size: v.Size ?? 0,
SHA256: v.Hash,
Version: v.Version ?? "0.0.0",
- IsForcibly: v.IsForcibly == true
+ IsForcibly: v.IsForcibly == true,
+ IsFreeze: v.IsFreeze == true,
+ RecordId: v.RecordId,
+ UpgradeMode: v.UpgradeMode,
+ AppType: v.AppType,
+ IsCrossVersion: v.IsCrossVersion == true,
+ FromVersion: v.FromVersion,
+ TargetArchiveHash: v.Hash,
+ AuthScheme: v.AuthScheme,
+ AuthToken: v.AuthToken
);
}
}
diff --git a/src/c#/GeneralUpdate.Core/Download/Sources/HubDownloadSource.cs b/src/c#/GeneralUpdate.Core/Download/Sources/HubDownloadSource.cs
deleted file mode 100644
index 73437270..00000000
--- a/src/c#/GeneralUpdate.Core/Download/Sources/HubDownloadSource.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using GeneralUpdate.Core.Download.Abstractions;
-using GeneralUpdate.Core.Download.Models;
-using GeneralUpdate.Core.Hubs;
-using GeneralUpdate.Core.JsonContext;
-
-namespace GeneralUpdate.Core.Download.Sources;
-
-///
-/// SignalR Hub download source — receives update push notifications
-/// and converts them to DownloadAssets for the orchestrator.
-///
-public class HubDownloadSource : IDownloadSource, IAsyncDisposable
-{
- private readonly string _hubUrl;
- private readonly string? _token;
- private readonly string? _appKey;
- private readonly ConcurrentBag _assets = new();
- private readonly TaskCompletionSource _initializedTcs = new();
- private UpgradeHubService? _hub;
-
- public HubDownloadSource(string hubUrl, string? token = null, string? appKey = null)
- {
- _hubUrl = hubUrl;
- _token = token;
- _appKey = appKey;
- }
-
- /// Start listening to the SignalR hub.
- public async Task StartAsync()
- {
- try
- {
- _hub = new UpgradeHubService(_hubUrl, _token, _appKey);
- _hub.AddListenerReceive(OnReceiveMessage);
- await _hub.StartAsync().ConfigureAwait(false);
- _initializedTcs.TrySetResult(true);
- }
- catch (Exception ex)
- {
- _initializedTcs.TrySetException(ex);
- }
- }
-
- private void OnReceiveMessage(string json)
- {
- try
- {
- var packet = System.Text.Json.JsonSerializer.Deserialize(json, HttpParameterJsonContext.Default.PacketDTO);
- if (packet != null)
- {
- var asset = DownloadPlanBuilder.MapToAsset(packet);
- _assets.Add(asset);
- }
- }
- catch (Exception ex)
- {
- GeneralTracer.Warn($"HubDownloadSource: failed to parse message: {ex.Message}");
- }
- }
-
- /// Get accumulated download assets from hub pushes.
- public async Task> ListAsync(CancellationToken token = default)
- {
- // Wait for hub initialization
- await _initializedTcs.Task.ConfigureAwait(false);
-
- // Wait a brief moment for any pending messages to arrive
- try { await Task.Delay(100, token).ConfigureAwait(false); }
- catch (OperationCanceledException) { }
-
- return _assets.ToList();
- }
-
- public async ValueTask DisposeAsync()
- {
- if (_hub != null)
- await _hub.DisposeAsync();
- }
-}
diff --git a/src/c#/GeneralUpdate.Core/Download/Sources/OssDownloadSource.cs b/src/c#/GeneralUpdate.Core/Download/Sources/OssDownloadSource.cs
index 7c177322..2f371c25 100644
--- a/src/c#/GeneralUpdate.Core/Download/Sources/OssDownloadSource.cs
+++ b/src/c#/GeneralUpdate.Core/Download/Sources/OssDownloadSource.cs
@@ -35,7 +35,7 @@ public OssDownloadSource(HttpClient httpClient, string versionJsonUrl, TimeSpan?
}
///
- public async Task> ListAsync(CancellationToken token = default)
+ public async Task ListAsync(CancellationToken token = default)
{
// Download and parse the version JSON from OSS
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
@@ -49,10 +49,10 @@ public async Task> ListAsync(CancellationToken toke
var versions = System.Text.Json.JsonSerializer.Deserialize(json, VersionOSSJsonContext.Default.ListVersionOSS);
if (versions == null || versions.Count == 0)
- return Array.Empty();
+ return new DownloadSourceResult { Assets = Array.Empty() };
// Convert VersionOSS to DownloadAsset, ordered by publish time
- return versions
+ var assets = versions
.OrderBy(v => v.PubTime)
.Select(v =>
{
@@ -74,5 +74,7 @@ public async Task> ListAsync(CancellationToken toke
})
.ToList()
.AsReadOnly();
+
+ return new DownloadSourceResult { Assets = assets };
}
}
diff --git a/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs b/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs
index 99a23e7c..972019b9 100644
--- a/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs
+++ b/src/c#/GeneralUpdate.Core/FileSystem/BlackListDefaults.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using GeneralUpdate.Core.Configuration;
namespace GeneralUpdate.Core.FileSystem;
@@ -17,7 +18,7 @@ public static class BlackListDefaults
/// Default blacklisted file extensions.
public static readonly List DefaultBlackFormats = new()
- { ".patch", ".pdb", ".rar", ".tar", ".json", Configuration.Format.ZIP };
+ { ".patch", ".pdb", ".rar", ".tar", ".json", Configuration.Format.Zip.ToExtension() };
/// Default skipped directory prefixes.
public static readonly List DefaultSkipDirectories = new()
diff --git a/src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs b/src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs
index 0175f066..8be7774c 100644
--- a/src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs
+++ b/src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs
@@ -17,7 +17,7 @@ public interface IUpdateHooks
}
public record UpdateContext(
- string AppName,
+ string UpdateAppName,
string InstallPath,
string CurrentVersion,
string? TargetVersion,
@@ -48,7 +48,7 @@ public class UnixPermissionHooks : IUpdateHooks
{
public async Task OnBeforeStartAppAsync(UpdateContext ctx)
{
- var mainApp = Path.Combine(ctx.InstallPath, ctx.AppName);
+ var mainApp = Path.Combine(ctx.InstallPath, ctx.UpdateAppName);
if (File.Exists(mainApp))
await Task.Run(() => Process.Start("chmod", $"+x \"{mainApp}\"").WaitForExit());
}
diff --git a/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs
index 994b6c52..842cebe5 100644
--- a/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs
+++ b/src/c#/GeneralUpdate.Core/Ipc/IProcessInfoProvider.cs
@@ -18,22 +18,35 @@ public interface IProcessInfoProvider
}
///
-/// AES-encrypted temporary file IPC — simplest, most reliable cross-platform approach.
-/// File lives in %TEMP%/GeneralUpdate/ipc/ with a random name, auto-deleted after read.
+/// AES-encrypted temporary file IPC. Writes to a deterministic path under
+/// %TEMP%/GeneralUpdate/ipc/ so that both the client (sender) and upgrade (receiver)
+/// processes agree on the file location without needing out-of-band coordination.
+/// File is deleted after a successful read.
///
public class EncryptedFileProcessInfoProvider : IProcessInfoProvider
{
+ private const string FileName = "process_info.enc";
+
private static readonly byte[] Key = SHA256.Create()
.ComputeHash(Encoding.UTF8.GetBytes("GeneralUpdate.ProcessInfo.IPC.v1"));
private static readonly byte[] IV = new byte[16] { 0x47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private readonly string _filePath;
+ ///
+ /// Returns the deterministic IPC file path that both client and upgrade agree on.
+ ///
+ public static string GetDefaultFilePath(string? basePath = null)
+ {
+ var dir = basePath ?? Path.Combine(Path.GetTempPath(), "GeneralUpdate", "ipc");
+ return Path.Combine(dir, FileName);
+ }
+
public EncryptedFileProcessInfoProvider(string? basePath = null)
{
var dir = basePath ?? Path.Combine(Path.GetTempPath(), "GeneralUpdate", "ipc");
Directory.CreateDirectory(dir);
- _filePath = Path.Combine(dir, $"{Guid.NewGuid():N}.enc");
+ _filePath = Path.Combine(dir, FileName);
}
public Task SendAsync(ProcessInfo info, CancellationToken token = default)
@@ -42,7 +55,10 @@ public Task SendAsync(ProcessInfo info, CancellationToken token = default)
return Task.CompletedTask;
}
- /// Synchronous send — all I/O is synchronous under the hood.
+ ///
+ /// Encrypt and write to the deterministic IPC file.
+ /// Overwrites any existing file from a previous (stale) session.
+ ///
public void Send(ProcessInfo info)
{
var json = JsonSerializer.Serialize(info, ProcessInfoJsonContext.Default.ProcessInfo);
@@ -53,11 +69,20 @@ public void Send(ProcessInfo info)
public Task ReceiveAsync(CancellationToken token = default)
=> Task.FromResult(Receive());
- /// Synchronous receive — reads and deletes the encrypted file.
+ ///
+ /// Read and decrypt the IPC file, then delete it so a stale file is never re-read.
+ /// Returns null if the file does not exist or decryption fails.
+ ///
public ProcessInfo? Receive()
{
+ if (!File.Exists(_filePath)) return null;
+
var plain = IpcEncryption.DecryptFromFile(_filePath, Key, IV);
if (plain == null) return null;
+
+ try { File.Delete(_filePath); }
+ catch { /* best-effort cleanup */ }
+
var json = Encoding.UTF8.GetString(plain);
return JsonSerializer.Deserialize(json, ProcessInfoJsonContext.Default.ProcessInfo);
}
diff --git a/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs b/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs
index 1e89de87..45a1c56d 100644
--- a/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs
+++ b/src/c#/GeneralUpdate.Core/JsonContext/HttpParameterJsonContext.cs
@@ -11,7 +11,5 @@ namespace GeneralUpdate.Core.JsonContext;
[JsonSerializable(typeof(int?))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Dictionary))]
-[JsonSerializable(typeof(PacketDTO))]
-[JsonSerializable(typeof(List))]
[JsonSerializable(typeof(Configinfo))]
public partial class HttpParameterJsonContext: JsonSerializerContext;
\ No newline at end of file
diff --git a/src/c#/GeneralUpdate.Core/Network/VersionService.cs b/src/c#/GeneralUpdate.Core/Network/VersionService.cs
index acae4f09..2825212e 100644
--- a/src/c#/GeneralUpdate.Core/Network/VersionService.cs
+++ b/src/c#/GeneralUpdate.Core/Network/VersionService.cs
@@ -58,7 +58,7 @@ public static Task Validate(string url, string version,
string scheme = null, string token = null, CancellationToken ct = default)
{
var a = HttpAuthProviderFactory.Create(scheme, token, appKey);
- return new VersionService(a).ValidateAsync(url, version, (int)appType, (int)platform, productId, ct);
+ return new VersionService(a).ValidateAsync(url, version, (int)appType, appKey, (int)platform, productId, ct);
}
// Backward-compatible int overload (binary compat for existing callers)
@@ -69,14 +69,14 @@ public static Task Validate(string url, string version,
private async Task ReportAsync(string url, int recordId, int status, int? type, CancellationToken t = default)
{
- var p = new Dictionary { ["RecordId"] = recordId, ["Status"] = status, ["Type"] = type };
+ var p = new Dictionary { ["recordId"] = recordId, ["status"] = status, ["type"] = type };
await PostAsync>(url, p, ReportRespJsonContext.Default.BaseResponseDTOBoolean, t);
}
- private async Task ValidateAsync(string url, string v, int at, int pf, string pid,
+ private async Task ValidateAsync(string url, string v, int at, string appKey, int pf, string pid,
CancellationToken t = default)
{
- var p = new Dictionary { ["Version"] = v, ["AppType"] = at, ["Platform"] = pf, ["ProductId"] = pid };
+ var p = new Dictionary { ["version"] = v, ["appType"] = at, ["appKey"] = appKey, ["platform"] = pf, ["productId"] = pid, ["upgradeMode"] = 1 };
return await PostAsync(url, p, VersionRespJsonContext.Default.VersionRespDTO, t);
}
diff --git a/src/c#/GeneralUpdate.Core/Pipeline/CompressMiddleware.cs b/src/c#/GeneralUpdate.Core/Pipeline/CompressMiddleware.cs
index 17d1e6cd..ecc276ff 100644
--- a/src/c#/GeneralUpdate.Core/Pipeline/CompressMiddleware.cs
+++ b/src/c#/GeneralUpdate.Core/Pipeline/CompressMiddleware.cs
@@ -12,7 +12,7 @@ public Task InvokeAsync(PipelineContext context)
{
return Task.Run(() =>
{
- var format = context.Get("Format");
+ var format = context.Get("Format");
var sourcePath = context.Get("ZipFilePath");
var patchPath = context.Get("PatchPath");
var encoding = context.Get("Encoding");
diff --git a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs
index 814155e4..9d38d18c 100644
--- a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs
+++ b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineBuilder.cs
@@ -15,7 +15,7 @@ namespace GeneralUpdate.Core.Pipeline;
/// var pipeline = new DiffPipelineBuilder()
/// .UseDiffer(new StreamingHdiffDiffer())
/// .UseCleanMatcher(new DefaultCleanMatcher())
-/// .WithParallelism(Environment.ProcessorCount)
+/// .WithParallelism(2)
/// .Build();
/// await pipeline.CleanAsync(src, tgt, patch);
///
@@ -25,7 +25,7 @@ public class DiffPipelineBuilder
private IBinaryDiffer? _differ;
private ICleanMatcher? _cleanMatcher;
private IDirtyMatcher? _dirtyMatcher;
- private int _maxParallelism = Environment.ProcessorCount;
+ private int _maxParallelism = 2;
private bool _stopOnFirstError;
private IProgress? _progress;
@@ -60,7 +60,7 @@ public DiffPipelineBuilder UseDirtyMatcher(IDirtyMatcher matcher)
///
/// Sets the maximum degree of parallelism for file processing.
- /// Default: .
+ /// Default: 2.
///
public DiffPipelineBuilder WithParallelism(int maxDegreeOfParallelism)
{
diff --git a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs
index 64200856..ac1be88f 100644
--- a/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs
+++ b/src/c#/GeneralUpdate.Core/Pipeline/DiffPipelineOptions.cs
@@ -9,10 +9,10 @@ public sealed class DiffPipelineOptions
{
///
/// Gets or sets the maximum number of files to process concurrently.
- /// Default: .
+ /// Default: 2.
/// Set to 1 for sequential processing.
///
- public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount;
+ public int MaxDegreeOfParallelism { get; set; } = 2;
///
/// Gets or sets whether to stop processing on first error.
diff --git a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs
index 527df4ac..72a04563 100644
--- a/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs
+++ b/src/c#/GeneralUpdate.Core/Silent/SilentPollOrchestrator.cs
@@ -4,13 +4,11 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
-using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Download;
-using GeneralUpdate.Core.Download.Models;
using GeneralUpdate.Core.Download.Reporting;
using GeneralUpdate.Core.Download.Sources;
using GeneralUpdate.Core.Event;
@@ -24,8 +22,12 @@ namespace GeneralUpdate.Core.Silent;
///
/// Silent update poll orchestrator — periodically checks for updates,
-/// downloads them in the background, and optionally auto-installs.
-/// Replaces the legacy SilentUpdateMode class.
+/// downloads them in the background, and defers application to process exit.
+/// Follows the same AppType-split pattern as :
+/// - Upgrade (AppType=2) packages are applied in place during the poll cycle
+/// (they target UpdatePath, not the running app's InstallPath).
+/// - Client (AppType=1) packages are deferred — stored in ProcessInfo and
+/// handed off to the Upgrade process on exit.
///
public class SilentPollOrchestrator : IDisposable
{
@@ -38,6 +40,7 @@ public class SilentPollOrchestrator : IDisposable
private IUpdateHooks? _hooks;
private IUpdateReporter? _reporter;
private Configuration.ProcessInfo? _preparedProcessInfo;
+ private List _clientVersions = new();
public SilentPollOrchestrator(GlobalConfigInfo configInfo, SilentOptions options)
{
@@ -45,14 +48,12 @@ public SilentPollOrchestrator(GlobalConfigInfo configInfo, SilentOptions options
_options = options ?? throw new ArgumentNullException(nameof(options));
}
- /// Inject hooks and reporter for lifecycle callbacks during silent polling.
public SilentPollOrchestrator WithHooks(IUpdateHooks? hooks) { _hooks = hooks; return this; }
public SilentPollOrchestrator WithReporter(IUpdateReporter? reporter) { _reporter = reporter; return this; }
- /// Start background polling loop.
public Task StartAsync()
{
- GeneralTracer.Info($"SilentPollOrchestrator: starting. PollInterval={_options.PollInterval.TotalMinutes}min, AutoInstall={_options.AutoInstall}");
+ GeneralTracer.Info($"SilentPollOrchestrator: starting. PollInterval={_options.PollInterval.TotalMinutes}min");
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
_cts = new CancellationTokenSource();
@@ -67,7 +68,6 @@ public Task StartAsync()
return Task.CompletedTask;
}
- /// Stop polling and cancel any in-flight operation.
public void Stop()
{
_cts?.Cancel();
@@ -99,7 +99,6 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token)
{
GeneralTracer.Info($"SilentPollOrchestrator: checking for updates. Url={_configInfo.UpdateUrl}");
- // Use the new download source
var downloadSource = new HttpDownloadSource(
_configInfo.UpdateUrl,
_configInfo.ClientVersion,
@@ -110,8 +109,8 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token)
_configInfo.Scheme,
_configInfo.Token);
- var assets = await downloadSource.ListAsync(token).ConfigureAwait(false);
- var plan = DownloadPlanBuilder.Build(assets, _configInfo.ClientVersion);
+ var sourceResult = await downloadSource.ListAsync(token).ConfigureAwait(false);
+ var plan = DownloadPlanBuilder.Build(sourceResult.Assets, _configInfo.ClientVersion);
if (!plan.HasAssets)
{
@@ -126,9 +125,9 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token)
return;
}
- // ══— Hooks: allow cancellation before starting update ══—
+ // Hooks: allow cancellation before starting update
var updateCtx = new UpdateContext(
- _configInfo.MainAppName ?? _configInfo.AppName,
+ _configInfo.MainAppName ?? _configInfo.UpdateAppName,
_configInfo.InstallPath,
_configInfo.ClientVersion,
latestVersion,
@@ -147,166 +146,132 @@ private async Task PrepareUpdateIfNeededAsync(CancellationToken token)
catch (Exception ex) { GeneralTracer.Warn($"Hook OnBeforeUpdateAsync failed: {ex.Message}"); }
}
- // Configure matcher from config with defaults
- var effectiveConfig = new BlackListConfig(
- _configInfo.BlackFiles?.Count > 0 ? _configInfo.BlackFiles : BlackListDefaults.DefaultBlackFiles,
- _configInfo.BlackFormats?.Count > 0 ? _configInfo.BlackFormats : BlackListDefaults.DefaultBlackFormats,
- _configInfo.SkipDirectorys?.Count > 0 ? _configInfo.SkipDirectorys : BlackListDefaults.DefaultSkipDirectories
- );
- StorageManager.BlackListMatcher = new DefaultBlackListMatcher(effectiveConfig);
+ InitBlackList();
_configInfo.LastVersion = latestVersion;
- _configInfo.UpdateVersions = new List(); // legacy compat
_configInfo.TempPath = StorageManager.GetTempDirectory("silent_temp");
_configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath,
$"{StorageManager.DirectoryName}{_configInfo.ClientVersion}");
// Backup
- StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory,
- _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories);
-
- // Build ProcessInfo and store for IPC delivery on process exit
- _preparedProcessInfo = ConfigurationMapper.MapToProcessInfo(
- _configInfo, new List(),
- _configInfo.BlackFormats ?? BlackListDefaults.DefaultBlackFormats,
- _configInfo.BlackFiles ?? BlackListDefaults.DefaultBlackFiles,
- _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories);
- _configInfo.ProcessInfo = JsonSerializer.Serialize(_preparedProcessInfo, ProcessInfoJsonContext.Default.ProcessInfo);
-
- // ══— Reporter: update started ══—
- var startTime = DateTimeOffset.UtcNow;
+ if (_configInfo.BackupEnabled != false)
+ {
+ StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory,
+ _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories);
+ }
+
+ // Reporter: update started
if (_reporter != null)
{
- try
- {
- await _reporter.ReportAsync(new UpdateReport(
- updateCtx.AppName, updateCtx.CurrentVersion, updateCtx.TargetVersion,
- UpdateEvent.UpdateStarted, AppType.Client, startTime), token).ConfigureAwait(false);
- }
+ try { await _reporter.ReportAsync(new UpdateReport(0, (int)UpdateStatus.Updating, 1), token).ConfigureAwait(false); }
catch (Exception ex) { GeneralTracer.Warn($"Reporter UpdateStarted failed: {ex.Message}"); }
}
- // Download using new orchestrator
+ // Download all packages in background
GeneralTracer.Info($"SilentPollOrchestrator: downloading {plan.Assets.Count} asset(s).");
var httpClient = GeneralUpdate.Core.Network.HttpClientProvider.Shared;
- var downloadSuccessCount = 0;
- var downloadFailedCount = 0;
- var downloadTotalBytes = 0L;
- var downloadElapsed = TimeSpan.Zero;
try
{
var orchestrator = new Download.Orchestrators.DefaultDownloadOrchestrator(httpClient);
var report = await orchestrator.ExecuteAsync(plan, _configInfo.TempPath, token: token).ConfigureAwait(false);
- downloadSuccessCount = report.SuccessCount;
- downloadFailedCount = report.FailedCount;
- downloadTotalBytes = report.TotalBytes;
- downloadElapsed = report.TotalDuration;
- GeneralTracer.Info($"SilentPollOrchestrator: download complete. Success={downloadSuccessCount}, Failed={downloadFailedCount}");
+ GeneralTracer.Info($"SilentPollOrchestrator: download complete. Success={report.SuccessCount}, Failed={report.FailedCount}");
+
+ if (report.FailedCount > 0)
+ {
+ GeneralTracer.Error($"SilentPollOrchestrator: download had {report.FailedCount} failures, aborting update.");
+ return;
+ }
- // ══— Hooks + Reporter: download completed ══—
if (_hooks != null)
{
try
{
var downloadCtx = new DownloadContext(
plan.Assets.FirstOrDefault()?.Name ?? "update", latestVersion ?? "",
- downloadTotalBytes, downloadElapsed,
- _configInfo.TempPath, downloadFailedCount == 0);
+ report.TotalBytes, report.TotalDuration,
+ _configInfo.TempPath, report.FailedCount == 0);
await _hooks.OnDownloadCompletedAsync(downloadCtx).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Hook OnDownloadCompletedAsync failed: {ex.Message}"); }
}
-
if (_reporter != null)
{
- try
- {
- await _reporter.ReportAsync(new UpdateReport(
- updateCtx.AppName, updateCtx.CurrentVersion, updateCtx.TargetVersion,
- UpdateEvent.DownloadCompleted, AppType.Client, DateTimeOffset.UtcNow,
- DurationMs: downloadElapsed.TotalMilliseconds), token).ConfigureAwait(false);
- }
+ try { await _reporter.ReportAsync(new UpdateReport(0, (int)UpdateStatus.Updating, 1), token).ConfigureAwait(false); }
catch (Exception ex) { GeneralTracer.Warn($"Reporter DownloadCompleted failed: {ex.Message}"); }
}
-
- if (downloadFailedCount > 0)
- {
- GeneralTracer.Error($"SilentPollOrchestrator: download had {downloadFailedCount} failures, aborting update.");
- return;
- }
}
catch (Exception ex)
{
GeneralTracer.Error("SilentPollOrchestrator: download failed.", ex);
- if (_hooks != null)
+ TryReportError(updateCtx, ex);
+ return;
+ }
+
+ // Split packages by AppType — mirrors ClientUpdateStrategy
+ var downloadVersions = plan.Assets.Select(a => new VersionInfo
+ {
+ Name = a.Name,
+ Hash = a.SHA256,
+ Url = a.Url,
+ Version = a.Version,
+ Format = _configInfo.Format.ToExtension(),
+ AppType = a.AppType ?? (int)AppType.Client
+ }).ToList();
+
+ var upgradeVersions = downloadVersions.Where(v => v.AppType == (int)AppType.Upgrade).ToList();
+ _clientVersions = downloadVersions.Where(v => v.AppType == (int)AppType.Client).ToList();
+ GeneralTracer.Info($"SilentPollOrchestrator: Upgrade packages={upgradeVersions.Count}, Client packages={_clientVersions.Count}");
+
+ // Apply Upgrade packages in place — safe because they target UpdatePath
+ if (upgradeVersions.Count > 0)
+ {
+ GeneralTracer.Info("SilentPollOrchestrator: applying Upgrade packages.");
+ try
{
- try { await _hooks.OnUpdateErrorAsync(updateCtx, ex).ConfigureAwait(false); }
- catch (Exception hookEx) { GeneralTracer.Warn($"Hook OnUpdateErrorAsync failed: {hookEx.Message}"); }
+ _configInfo.UpdateVersions = upgradeVersions;
+ var strategy = CreateStrategy();
+ strategy.Create(_configInfo);
+ await strategy.ExecuteAsync().ConfigureAwait(false);
+ GeneralTracer.Info("SilentPollOrchestrator: Upgrade packages applied.");
}
- if (_reporter != null)
+ catch (Exception ex)
{
- try
- {
- await _reporter.ReportAsync(new UpdateReport(
- updateCtx.AppName, updateCtx.CurrentVersion, updateCtx.TargetVersion,
- UpdateEvent.UpdateFailed, AppType.Client, DateTimeOffset.UtcNow,
- ErrorMessage: ex.Message), token).ConfigureAwait(false);
- }
- catch (Exception reporterEx) { GeneralTracer.Warn($"Reporter UpdateFailed failed: {reporterEx.Message}"); }
+ GeneralTracer.Error("SilentPollOrchestrator: Upgrade package application failed.", ex);
+ TryReportError(updateCtx, ex);
+ return;
}
- return;
}
- finally { }
- // Execute pipeline
- try
+ // Build ProcessInfo with Client packages for IPC delivery on process exit
+ if (_clientVersions.Count > 0)
{
- var strategy = CreateStrategy();
- strategy.Create(_configInfo);
- await strategy.ExecuteAsync();
+ _configInfo.LaunchClientAfterUpdate = _options.LaunchClientAfterUpdate;
+ _preparedProcessInfo = ConfigurationMapper.MapToProcessInfo(
+ _configInfo, _clientVersions,
+ _configInfo.BlackFormats ?? BlackListDefaults.DefaultBlackFormats,
+ _configInfo.BlackFiles ?? BlackListDefaults.DefaultBlackFiles,
+ _configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories);
+ _configInfo.ProcessInfo = JsonSerializer.Serialize(_preparedProcessInfo, ProcessInfoJsonContext.Default.ProcessInfo);
- GeneralTracer.Info("SilentPollOrchestrator: update prepared.");
Interlocked.Exchange(ref _prepared, 1);
+ GeneralTracer.Info("SilentPollOrchestrator: update prepared, waiting for process exit.");
+ }
+ else if (upgradeVersions.Count > 0)
+ {
+ // Upgrade-only: packages already applied, no handoff needed
+ Interlocked.Exchange(ref _prepared, 1);
+ GeneralTracer.Info("SilentPollOrchestrator: upgrade-only update applied, no client handoff needed.");
+ }
- // ══— Hooks + Reporter: update applied ══—
- if (_hooks != null)
- {
- try { await _hooks.OnAfterUpdateAsync(updateCtx).ConfigureAwait(false); }
- catch (Exception ex) { GeneralTracer.Warn($"Hook OnAfterUpdateAsync failed: {ex.Message}"); }
- }
-
- if (_reporter != null)
- {
- try
- {
- var elapsedMs = (DateTimeOffset.UtcNow - startTime).TotalMilliseconds;
- await _reporter.ReportAsync(new UpdateReport(
- updateCtx.AppName, updateCtx.CurrentVersion, updateCtx.TargetVersion,
- UpdateEvent.UpdateApplied, AppType.Client, DateTimeOffset.UtcNow,
- DurationMs: elapsedMs), token).ConfigureAwait(false);
- }
- catch (Exception ex) { GeneralTracer.Warn($"Reporter UpdateApplied failed: {ex.Message}"); }
- }
+ if (_hooks != null)
+ {
+ try { await _hooks.OnAfterUpdateAsync(updateCtx).ConfigureAwait(false); }
+ catch (Exception ex) { GeneralTracer.Warn($"Hook OnAfterUpdateAsync failed: {ex.Message}"); }
}
- catch (Exception ex)
+ if (_reporter != null)
{
- GeneralTracer.Error("SilentPollOrchestrator: pipeline execution failed.", ex);
- if (_hooks != null)
- {
- try { await _hooks.OnUpdateErrorAsync(updateCtx, ex).ConfigureAwait(false); }
- catch (Exception hookEx) { GeneralTracer.Warn($"Hook OnUpdateErrorAsync failed: {hookEx.Message}"); }
- }
- if (_reporter != null)
- {
- try
- {
- await _reporter.ReportAsync(new UpdateReport(
- updateCtx.AppName, updateCtx.CurrentVersion, updateCtx.TargetVersion,
- UpdateEvent.UpdateFailed, AppType.Client, DateTimeOffset.UtcNow,
- ErrorMessage: ex.Message), token).ConfigureAwait(false);
- }
- catch (Exception reporterEx) { GeneralTracer.Warn($"Reporter UpdateFailed failed: {reporterEx.Message}"); }
- }
+ try { await _reporter.ReportAsync(new UpdateReport(0, (int)UpdateStatus.Success, 1), token).ConfigureAwait(false); }
+ catch (Exception ex) { GeneralTracer.Warn($"Reporter UpdateApplied failed: {ex.Message}"); }
}
}
@@ -316,24 +281,29 @@ private void OnProcessExit(object? sender, EventArgs e)
try
{
- var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName);
-
- // Start the upgrade process first — it will call ReceiveAsync in its constructor.
- // We then call SendAsync which creates the NamedPipe server; the upgrade's
- // client connects to it. Auto-fallback (SharedMemory > EncryptedFile) handles
- // timing gaps where the named pipe handshake doesn't complete in time.
- if (File.Exists(updaterPath))
+ // Resolve updater location — prefers UpdatePath, falls back to InstallPath
+ var updaterDir = !string.IsNullOrWhiteSpace(_configInfo.UpdatePath)
+ ? (Path.IsPathRooted(_configInfo.UpdatePath)
+ ? _configInfo.UpdatePath
+ : Path.Combine(_configInfo.InstallPath, _configInfo.UpdatePath))
+ : _configInfo.InstallPath;
+ var updaterPath = Path.Combine(updaterDir, _configInfo.UpdateAppName);
+
+ if (!File.Exists(updaterPath))
{
- GeneralTracer.Info($"SilentPollOrchestrator: launching updater {updaterPath}");
- Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath });
+ GeneralTracer.Warn($"SilentPollOrchestrator: updater not found at {updaterPath}, cannot launch.");
+ return;
}
- // Send ProcessInfo via AES-encrypted file IPC.
- if (_preparedProcessInfo != null)
+ // Send ProcessInfo with Client packages via encrypted file IPC BEFORE starting Upgrade
+ if (_preparedProcessInfo != null && _clientVersions.Count > 0)
{
new EncryptedFileProcessInfoProvider().Send(_preparedProcessInfo);
- GeneralTracer.Info("SilentPollOrchestrator: ProcessInfo sent via encrypted file IPC.");
+ GeneralTracer.Info($"SilentPollOrchestrator: ProcessInfo sent with {_clientVersions.Count} Client package(s).");
}
+
+ Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath });
+ GeneralTracer.Info($"SilentPollOrchestrator: launched updater {updaterPath}");
}
catch (Exception ex)
{
@@ -341,6 +311,30 @@ private void OnProcessExit(object? sender, EventArgs e)
}
}
+ private void TryReportError(UpdateContext ctx, Exception ex)
+ {
+ if (_hooks != null)
+ {
+ try { _hooks.OnUpdateErrorAsync(ctx, ex).GetAwaiter().GetResult(); }
+ catch (Exception hookEx) { GeneralTracer.Warn($"Hook OnUpdateErrorAsync failed: {hookEx.Message}"); }
+ }
+ if (_reporter != null)
+ {
+ try { _reporter.ReportAsync(new UpdateReport(0, (int)UpdateStatus.Failure, 1)).GetAwaiter().GetResult(); }
+ catch (Exception reporterEx) { GeneralTracer.Warn($"Reporter UpdateFailed failed: {reporterEx.Message}"); }
+ }
+ }
+
+ private void InitBlackList()
+ {
+ var effectiveConfig = new BlackListConfig(
+ _configInfo.BlackFiles?.Count > 0 ? _configInfo.BlackFiles : BlackListDefaults.DefaultBlackFiles,
+ _configInfo.BlackFormats?.Count > 0 ? _configInfo.BlackFormats : BlackListDefaults.DefaultBlackFormats,
+ _configInfo.SkipDirectorys?.Count > 0 ? _configInfo.SkipDirectorys : BlackListDefaults.DefaultSkipDirectories
+ );
+ StorageManager.BlackListMatcher = new DefaultBlackListMatcher(effectiveConfig);
+ }
+
private static IStrategy CreateStrategy()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return new WindowsStrategy();
@@ -372,12 +366,14 @@ public void Dispose()
}
}
-/// Silent polling configuration.
public sealed class SilentOptions
{
- /// Polling interval (default 1 hour).
public TimeSpan PollInterval { get; set; } = TimeSpan.FromHours(1);
- /// Whether to auto-install after download.
- public bool AutoInstall { get; set; } = false;
+ ///
+ /// Whether to launch the client application after the upgrade process finishes.
+ /// Default: true (current behavior). Set to false when the caller wants to
+ /// manually control restart timing (e.g. maintenance windows, orchestrated rollouts).
+ ///
+ public bool LaunchClientAfterUpdate { get; set; } = true;
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs
index 43976118..270d866b 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using GeneralUpdate.Core.Differential;
using GeneralUpdate.Core.FileSystem;
@@ -19,11 +20,11 @@ public abstract class AbstractStrategy : IStrategy
private const string Patchs = "patchs";
protected GlobalConfigInfo _configinfo = new();
- /// Optional hooks for pre/post update callbacks.
- protected IUpdateHooks? Hooks { get; set; }
+ /// Hooks for pre/post update callbacks.
+ public IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
- /// Optional reporter for update status reporting.
- protected IUpdateReporter? Reporter { get; set; }
+ /// Reporter for update status reporting.
+ public IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter();
/// Optional binary differ for differential patch updates.
public IDirtyStrategy? DirtyStrategy { get; set; }
@@ -33,7 +34,20 @@ public abstract class AbstractStrategy : IStrategy
/// DiffPipeline for parallel patch application with progress reporting.
public DiffPipeline? DiffPipeline { get; set; }
-
+
+ /// App to launch in . Set by the upper strategy.
+ public string? LaunchAppName { get; set; }
+
+ /// Whether to also start the Bowl companion process. Windows only. Set by the upper strategy.
+ public bool LaunchBowl { get; set; }
+
+ ///
+ /// When true, resolves the app from
+ /// first before falling back to .
+ /// Set by when launching the upgrade process.
+ ///
+ public bool UseUpdatePath { get; set; }
+
public virtual Task StartAppAsync() => throw new NotImplementedException();
public virtual async Task ExecuteAsync()
@@ -65,11 +79,15 @@ await VersionService.Report(_configinfo.ReportUrl
, version.AppType
, _configinfo.Scheme
, _configinfo.Token);
+
+ // Delete only this version's zip file — other AppType packages
+ // in TempPath may still be needed by a downstream process.
+ DeleteVersionZip(version);
}
}
Clear(patchPath);
- Clear(_configinfo.TempPath);
+ TryCleanTempPath();
await OnExecuteCompleteAsync();
}
catch (Exception e)
@@ -88,7 +106,7 @@ protected virtual PipelineContext CreatePipelineContext(VersionInfo version, str
{
var context = new PipelineContext();
// Common parameters
- context.Add("ZipFilePath", Path.Combine(_configinfo.TempPath, $"{version.Name}{_configinfo.Format}"));
+ context.Add("ZipFilePath", Path.Combine(_configinfo.TempPath, $"{version.Name}{_configinfo.Format.ToExtension()}"));
// Hash middleware
context.Add("Hash", version.Hash);
// Zip middleware
@@ -96,7 +114,9 @@ protected virtual PipelineContext CreatePipelineContext(VersionInfo version, str
context.Add("Name", version.Name);
context.Add("Encoding", _configinfo.Encoding);
// Patch middleware
- context.Add("SourcePath", _configinfo.InstallPath);
+ // For Upgrade packages, apply to UpdatePath if configured; otherwise fall back to InstallPath
+ var sourcePath = ResolveTargetPath(version);
+ context.Add("SourcePath", sourcePath);
context.Add("PatchPath", patchPath);
context.Add("PatchEnabled", _configinfo.PatchEnabled);
// Binary differ for differential patching
@@ -138,12 +158,56 @@ protected virtual void HandleExecuteException(Exception e)
///
protected static string CheckPath(string path, string name)
{
- if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(name))
+ if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(name))
return string.Empty;
var tempPath = Path.Combine(path, name);
return File.Exists(tempPath) ? tempPath : string.Empty;
}
+ ///
+ /// Resolves the full path for an executable, optionally checking
+ /// before falling back to InstallPath.
+ ///
+ /// The executable name.
+ /// When true, checks UpdatePath first.
+ /// Full path if found, empty string otherwise.
+ protected string ResolveAppPath(string name, bool preferUpdatePath = false)
+ {
+ if (preferUpdatePath && !string.IsNullOrWhiteSpace(_configinfo.UpdatePath))
+ {
+ var upgradeDir = ResolveUpdateDir();
+ var path = CheckPath(upgradeDir, name);
+ if (!string.IsNullOrEmpty(path))
+ return path;
+ }
+
+ return CheckPath(_configinfo.InstallPath, name);
+ }
+
+ ///
+ /// Resolves the target directory for applying a package.
+ /// For Upgrade (AppType=2) packages, uses if configured;
+ /// otherwise falls back to .
+ ///
+ protected string ResolveTargetPath(VersionInfo version)
+ {
+ if (version.AppType == 2 && !string.IsNullOrWhiteSpace(_configinfo.UpdatePath))
+ return ResolveUpdateDir();
+
+ return _configinfo.InstallPath;
+ }
+
+ ///
+ /// Resolves to an absolute path.
+ /// Relative paths are combined with .
+ ///
+ private string ResolveUpdateDir()
+ {
+ return Path.IsPathRooted(_configinfo.UpdatePath)
+ ? _configinfo.UpdatePath
+ : Path.Combine(_configinfo.InstallPath, _configinfo.UpdatePath);
+ }
+
// ═══ Safe hooks/reporter wrappers (shared by all strategy subclasses) ═══
// Note: Each subclass builds its own UpdateContext via BuildUpdateContext().
// Subclasses should call hooks/reporter through their own context-aware wrappers.
@@ -177,5 +241,55 @@ private static void Clear(string path)
if (Directory.Exists(path))
StorageManager.DeleteDirectory(path);
}
+
+ ///
+ /// Deletes the zip file for a processed version from TempPath.
+ /// Only removes the specific file — other packages in the same directory
+ /// may belong to a different AppType and must be kept for downstream processes.
+ ///
+ private void DeleteVersionZip(VersionInfo version)
+ {
+ if (string.IsNullOrWhiteSpace(_configinfo.TempPath)) return;
+
+ var zipPath = Path.Combine(_configinfo.TempPath, $"{version.Name}{_configinfo.Format.ToExtension()}");
+ try
+ {
+ if (File.Exists(zipPath))
+ {
+ File.SetAttributes(zipPath, FileAttributes.Normal);
+ File.Delete(zipPath);
+ GeneralTracer.Info($"AbstractStrategy: deleted processed zip {zipPath}");
+ }
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"AbstractStrategy: failed to delete zip {zipPath}. {ex.Message}");
+ }
+ }
+
+ ///
+ /// Removes TempPath if it is empty after all processed zips have been deleted.
+ /// The last process in the chain (usually Upgrade) will find an empty directory
+ /// and clean it up. Earlier processes skip this because other AppType packages
+ /// still remain in the directory.
+ ///
+ private void TryCleanTempPath()
+ {
+ try
+ {
+ var tempPath = _configinfo.TempPath;
+ if (string.IsNullOrWhiteSpace(tempPath) || !Directory.Exists(tempPath)) return;
+
+ if (!Directory.EnumerateFileSystemEntries(tempPath).Any())
+ {
+ Directory.Delete(tempPath, false);
+ GeneralTracer.Info($"AbstractStrategy: cleaned empty temp directory {tempPath}");
+ }
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"AbstractStrategy: failed to clean temp directory. {ex.Message}");
+ }
+ }
}
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
index b80167c9..dbaa4828 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
@@ -36,6 +36,17 @@ public class ClientUpdateStrategy : IStrategy
private IStrategy? _osStrategy;
private Func? _updatePrecheck;
private readonly Download.Abstractions.IDownloadOrchestrator? _orchestrator;
+ private int _mainRecordId;
+
+ /// Which side(s) need updating, determined by server validation.
+ private enum UpdateScenario
+ {
+ None,
+ UpgradeOnly,
+ MainOnly,
+ Both
+ }
+
/// Lifecycle hooks injected by the bootstrap.
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
/// Update status reporter injected by the bootstrap.
@@ -49,8 +60,12 @@ public void Create(GlobalConfigInfo parameter)
{
_configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter));
_osStrategy = ResolveOsStrategy();
- if (_pendingDirtyStrategy != null && _osStrategy is AbstractStrategy abs)
- abs.DirtyStrategy = _pendingDirtyStrategy;
+ if (_osStrategy is AbstractStrategy abs)
+ {
+ if (_pendingDirtyStrategy != null) abs.DirtyStrategy = _pendingDirtyStrategy;
+ if (_pendingBinaryDiffer != null) abs.BinaryDiffer = _pendingBinaryDiffer;
+ if (_pendingDiffPipeline != null) abs.DiffPipeline = _pendingDiffPipeline;
+ }
}
public async Task ExecuteAsync()
@@ -74,6 +89,8 @@ public async Task ExecuteAsync()
}
private IDirtyStrategy? _pendingDirtyStrategy;
+ private IBinaryDiffer? _pendingBinaryDiffer;
+ private DiffPipeline? _pendingDiffPipeline;
/// Sets the directory-level dirty strategy on the underlying OS-level strategy for differential patch updates.
/// Safe to call before or after Create(). If called before, the strategy is cached and applied when Create() resolves _osStrategy.
@@ -90,6 +107,8 @@ public void SetBinaryDiffer(IBinaryDiffer? binaryDiffer)
{
if (_osStrategy is AbstractStrategy abs)
abs.BinaryDiffer = binaryDiffer;
+ else
+ _pendingBinaryDiffer = binaryDiffer;
}
/// Sets the DiffPipeline on the underlying OS-level strategy for parallel patch application.
@@ -97,6 +116,8 @@ public void SetDiffPipeline(DiffPipeline? diffPipeline)
{
if (_osStrategy is AbstractStrategy abs)
abs.DiffPipeline = diffPipeline;
+ else
+ _pendingDiffPipeline = diffPipeline;
}
public async Task StartAppAsync()
@@ -116,7 +137,7 @@ public ClientUpdateStrategy UseUpdatePrecheck(Func fu
private async Task ExecuteWorkflowAsync()
{
- // Standard mode — silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync().
+ // Standard mode �?silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync().
// Runtime options (Encoding, Format, DownloadTimeOut, etc.) are already
// populated on _configInfo by Bootstrap.ApplyRuntimeOptions().
await ExecuteStandardWorkflowAsync();
@@ -137,17 +158,54 @@ private async Task ExecuteStandardWorkflowAsync()
_configInfo.Scheme,
_configInfo.Token);
- var assets = await downloadSource.ListAsync().ConfigureAwait(false);
- var downloadPlan = Download.DownloadPlanBuilder.Build(assets, _configInfo.ClientVersion);
+ // Call server validation — returns assets plus per-side flags from the two Validate calls
+ var sourceResult = await downloadSource.ListAsync().ConfigureAwait(false);
+ var downloadPlan = Download.DownloadPlanBuilder.Build(sourceResult.Assets, _configInfo.ClientVersion);
- // Detect update status
- _configInfo.IsMainUpdate = downloadPlan.HasAssets;
- _configInfo.IsUpgradeUpdate = assets.Any(a => a.Version != _configInfo.ClientVersion);
+ // Detect update status from SERVER validation results: IsMainUpdate is true only when
+ // the server returned version info for the Client call, IsUpgradeUpdate only when
+ // the server returned version info for the Upgrade call (requirement 1).
+ _configInfo.IsMainUpdate = sourceResult.HasMainUpdate;
+ _configInfo.IsUpgradeUpdate = sourceResult.HasUpgradeUpdate;
_configInfo.LastVersion = downloadPlan.Assets.LastOrDefault()?.Version;
- GeneralTracer.Info($"ClientUpdateStrategy: IsMainUpdate={_configInfo.IsMainUpdate}, IsUpgradeUpdate={_configInfo.IsUpgradeUpdate}, AssetCount={downloadPlan.Assets.Count}");
- // Dispatch update info event
- var updateInfoArgs = new UpdateInfoEventArgs(null);
+ var scenario = (_configInfo.IsMainUpdate, _configInfo.IsUpgradeUpdate) switch
+ {
+ (false, false) => UpdateScenario.None,
+ (false, true) => UpdateScenario.UpgradeOnly,
+ (true, false) => UpdateScenario.MainOnly,
+ (true, true) => UpdateScenario.Both,
+ };
+ GeneralTracer.Info($"ClientUpdateStrategy: Scenario={scenario}, AssetCount={downloadPlan.Assets.Count}");
+
+ // Dispatch update info event with populated version data (full GeneralSpacestation-compatible fields)
+ var versionInfos = downloadPlan.Assets.Select(a => new VersionInfo
+ {
+ RecordId = a.RecordId,
+ Name = a.Name,
+ Url = a.Url,
+ Size = a.Size,
+ Hash = a.SHA256,
+ Version = a.Version,
+ IsForcibly = a.IsForcibly,
+ IsFreeze = a.IsFreeze,
+ AppType = a.AppType,
+ UpgradeMode = a.UpgradeMode,
+ IsCrossVersion = a.IsCrossVersion,
+ FromVersion = a.FromVersion
+ }).ToList();
+
+ var versionResp = new VersionRespDTO
+ {
+ Code = versionInfos.Count > 0 ? 200 : 404,
+ Body = versionInfos,
+ Message = versionInfos.Count > 0 ? $"Found {versionInfos.Count} update(s)." : "No updates available."
+ };
+
+ var updateInfoArgs = new UpdateInfoEventArgs(versionResp);
+
+ // Capture the first RecordId for status reporting to GeneralSpacestation
+ _mainRecordId = downloadPlan.Assets.FirstOrDefault().RecordId;
EventManager.Instance.Dispatch(this, updateInfoArgs);
var isForcibly = downloadPlan.IsForcibly;
@@ -157,6 +215,13 @@ private async Task ExecuteStandardWorkflowAsync()
return;
}
+ // Scenario None: nothing to update — exit early
+ if (scenario == UpdateScenario.None)
+ {
+ GeneralTracer.Info("ClientUpdateStrategy: no update available for client or upgrade.");
+ return;
+ }
+
// Hooks: allow cancellation before download
var hooksCtx = BuildUpdateContext();
if (!await SafeOnBeforeUpdateAsync(hooksCtx).ConfigureAwait(false))
@@ -169,17 +234,10 @@ private async Task ExecuteStandardWorkflowAsync()
await SafeReportUpdateStartedAsync(hooksCtx).ConfigureAwait(false);
InitBlackList();
-
_configInfo.TempPath = StorageManager.GetTempDirectory("main_temp");
_configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath,
$"{StorageManager.DirectoryName}{_configInfo.ClientVersion}");
- if (!_configInfo.IsMainUpdate)
- {
- GeneralTracer.Info("ClientUpdateStrategy: no update available.");
- return;
- }
-
// Check failed version
if (!string.IsNullOrEmpty(_configInfo.LastVersion) && CheckFail(_configInfo.LastVersion))
{
@@ -199,7 +257,8 @@ private async Task ExecuteStandardWorkflowAsync()
_osStrategy!.Create(_configInfo);
- // Download via orchestrator — wired with options from GlobalConfigInfo
+ // Download ALL packages via orchestrator (requirement 6: client downloads everything
+ // regardless of whether client or upgrade needs updating)
var orchOptions = Download.Models.DownloadOrchestratorOptions.From(_configInfo);
GeneralTracer.Info($"ClientUpdateStrategy: downloading {downloadPlan.Assets.Count} asset(s).");
if (_orchestrator != null)
@@ -220,49 +279,66 @@ private async Task ExecuteStandardWorkflowAsync()
await SafeReportDownloadCompletedAsync(hooksCtx).ConfigureAwait(false);
await SafeOnDownloadCompletedAsync(hooksCtx).ConfigureAwait(false);
- // Phase: apply Upgrade packages — update Upgrade.exe itself before launching it.
- // Safe because MainApp and Upgrade.exe are different files (no lock conflict).
- var allVersions = downloadPlan.Assets.Select(a => new VersionInfo
- {
- Name = a.Name,
- Hash = a.SHA256,
- Url = a.Url,
- Version = a.Version,
- Format = _configInfo.Format ?? "ZIP",
- AppType = a.IsForcibly ? null : null // preserve original AppType
- }).ToList();
-
- // Rebuild the full VersionInfo list with AppType preserved from download source
+ // Build VersionInfo list with AppType preserved from server response.
var downloadVersions = downloadPlan.Assets.Select(a => new VersionInfo
{
Name = a.Name,
Hash = a.SHA256,
Url = a.Url,
Version = a.Version,
- Format = _configInfo.Format ?? "ZIP",
- AppType = _configInfo.IsUpgradeUpdate == true && a.Version != _configInfo.ClientVersion
- ? (int)AppType.Upgrade : (int)AppType.Client
+ Format = _configInfo.Format.ToExtension(),
+ AppType = a.AppType ?? (int)AppType.Client
}).ToList();
- // Split: Upgrade versions vs MainApp versions
var upgradeVersions = downloadVersions.Where(v => v.AppType == (int)AppType.Upgrade).ToList();
- var clientVersions = downloadVersions.Where(v => v.AppType != (int)AppType.Upgrade).ToList();
-
+ var clientVersions = downloadVersions.Where(v => v.AppType == (int)AppType.Client).ToList();
GeneralTracer.Info($"ClientUpdateStrategy: Upgrade packages={upgradeVersions.Count}, MainApp packages={clientVersions.Count}");
- // Apply Upgrade packages now (update Upgrade.exe before launching it)
- if (upgradeVersions.Count > 0)
+ // ── Dispatch by scenario — one switch, four states, zero nested if-else ──
+ switch (scenario)
{
- GeneralTracer.Info("ClientUpdateStrategy: applying Upgrade packages.");
- _configInfo.UpdateVersions = upgradeVersions;
- _osStrategy!.Create(_configInfo);
- await _osStrategy.ExecuteAsync();
+ case UpdateScenario.UpgradeOnly:
+ await ApplyUpgradePackagesAsync(upgradeVersions).ConfigureAwait(false);
+ await SafeOnAfterUpdateAsync(hooksCtx).ConfigureAwait(false);
+ await SafeReportUpdateAppliedAsync(hooksCtx).ConfigureAwait(false);
+ GeneralTracer.Info("ClientUpdateStrategy: Upgrade-only update applied, client continues running.");
+ break;
+
+ case UpdateScenario.MainOnly:
+ SendProcessIpc(clientVersions);
+ await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false);
+ await LaunchUpgradeProcessAsync().ConfigureAwait(false);
+ break;
+
+ case UpdateScenario.Both:
+ await ApplyUpgradePackagesAsync(upgradeVersions).ConfigureAwait(false);
+ await SafeOnAfterUpdateAsync(hooksCtx).ConfigureAwait(false);
+ await SafeReportUpdateAppliedAsync(hooksCtx).ConfigureAwait(false);
+ SendProcessIpc(clientVersions);
+ await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false);
+ await LaunchUpgradeProcessAsync().ConfigureAwait(false);
+ break;
}
+ }
+
+ #endregion
+
+ #region Scenario actions
- // Send IPC with remaining MainApp versions for the upgrade process
+ private async Task ApplyUpgradePackagesAsync(List upgradeVersions)
+ {
+ if (upgradeVersions.Count == 0) return;
+ GeneralTracer.Info("ClientUpdateStrategy: applying Upgrade packages in place.");
+ _configInfo!.UpdateVersions = upgradeVersions;
+ _osStrategy!.Create(_configInfo);
+ await _osStrategy.ExecuteAsync().ConfigureAwait(false);
+ }
+
+ private void SendProcessIpc(List clientVersions)
+ {
var processInfo = ConfigurationMapper.MapToProcessInfo(
- _configInfo, clientVersions,
- _configInfo.BlackFormats ?? BlackListDefaults.DefaultBlackFormats,
+ _configInfo!, clientVersions,
+ _configInfo!.BlackFormats ?? BlackListDefaults.DefaultBlackFormats,
_configInfo.BlackFiles ?? BlackListDefaults.DefaultBlackFiles,
_configInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories);
@@ -270,20 +346,18 @@ private async Task ExecuteStandardWorkflowAsync()
ProcessInfoJsonContext.Default.ProcessInfo);
new EncryptedFileProcessInfoProvider().Send(processInfo);
GeneralTracer.Info("ClientUpdateStrategy: ProcessInfo sent with MainApp versions only.");
+ }
- await SafeOnAfterUpdateAsync(hooksCtx).ConfigureAwait(false);
- await SafeReportUpdateAppliedAsync(hooksCtx).ConfigureAwait(false);
- await SafeOnBeforeStartAppAsync(hooksCtx).ConfigureAwait(false);
-
- // Launch the upgrade process to apply MainApp updates
- var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName);
- if (!File.Exists(updaterPath))
- throw new FileNotFoundException($"Upgrade application not found: {updaterPath}");
-
- GeneralTracer.Info($"ClientUpdateStrategy: launching upgrade process {updaterPath}");
- Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = updaterPath });
- GeneralTracer.Info("ClientUpdateStrategy: upgrade process launched, exiting.");
- await GracefulExit.CurrentProcessAsync().ConfigureAwait(false);
+ private async Task LaunchUpgradeProcessAsync()
+ {
+ if (_osStrategy is AbstractStrategy abs)
+ {
+ abs.LaunchAppName = _configInfo!.UpdateAppName;
+ abs.LaunchBowl = false;
+ abs.UseUpdatePath = !string.IsNullOrWhiteSpace(_configInfo.UpdatePath);
+ }
+ GeneralTracer.Info($"ClientUpdateStrategy: launching upgrade process {_configInfo!.UpdateAppName} via OS strategy.");
+ await _osStrategy!.StartAppAsync();
}
#endregion
@@ -366,7 +440,7 @@ private async Task CallSmallBowlHomeAsync(string processName)
private Hooks.UpdateContext BuildUpdateContext()
{
return new Hooks.UpdateContext(
- _configInfo?.AppName ?? "unknown",
+ _configInfo?.UpdateAppName ?? "unknown",
_configInfo?.InstallPath ?? AppDomain.CurrentDomain.BaseDirectory,
_configInfo?.ClientVersion ?? "0.0.0",
_configInfo?.LastVersion,
@@ -403,7 +477,7 @@ private async Task SafeOnDownloadCompletedAsync(Hooks.UpdateContext ctx)
try
{
var downloadCtx = new Hooks.DownloadContext(
- _configInfo?.MainAppName ?? _configInfo?.AppName ?? "unknown",
+ _configInfo?.MainAppName ?? _configInfo?.UpdateAppName ?? "unknown",
_configInfo?.LastVersion ?? "",
0, TimeSpan.Zero, _configInfo?.TempPath, true);
await Hooks.OnDownloadCompletedAsync(downloadCtx).ConfigureAwait(false);
@@ -415,10 +489,7 @@ private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx)
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateStarted, ctx.AppType, DateTimeOffset.UtcNow
- )).ConfigureAwait(false);
+ await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Updating, 1)).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); }
}
@@ -427,10 +498,7 @@ private async Task SafeReportDownloadCompletedAsync(Hooks.UpdateContext ctx)
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.DownloadCompleted, ctx.AppType, DateTimeOffset.UtcNow
- )).ConfigureAwait(false);
+ await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Updating, 1)).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Report DownloadCompleted failed: {ex.Message}"); }
}
@@ -439,11 +507,7 @@ private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exceptio
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow,
- ErrorMessage: error.Message
- )).ConfigureAwait(false);
+ await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Failure, 1)).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); }
}
@@ -452,10 +516,7 @@ private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx)
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateApplied, ctx.AppType, DateTimeOffset.UtcNow
- )).ConfigureAwait(false);
+ await Reporter.ReportAsync(new Download.Reporting.UpdateReport(_mainRecordId, (int)Download.Reporting.UpdateStatus.Success, 1)).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); }
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/IStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/IStrategy.cs
index 64a3531b..791daf02 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/IStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/IStrategy.cs
@@ -1,5 +1,7 @@
using System.Threading.Tasks;
using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.Hooks;
+using IUpdateReporter = GeneralUpdate.Core.Download.Reporting.IUpdateReporter;
namespace GeneralUpdate.Core.Strategy
{
@@ -8,6 +10,16 @@ namespace GeneralUpdate.Core.Strategy
///
public interface IStrategy
{
+ ///
+ /// Lifecycle hooks for pre/post update callbacks.
+ ///
+ IUpdateHooks Hooks { get; set; }
+
+ ///
+ /// Update status reporter.
+ ///
+ IUpdateReporter Reporter { get; set; }
+
///
/// Execution strategy.
///
diff --git a/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs
index 90f3255f..5d4d6984 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/LinuxStrategy.cs
@@ -27,23 +27,18 @@ protected override PipelineBuilder BuildPipeline(PipelineContext context)
return builder;
}
- protected override async Task OnExecuteCompleteAsync()
- {
- GeneralTracer.Info("GeneralUpdate.Core.LinuxStrategy.OnExecuteComplete: all versions processed, starting application.");
- await StartAppAsync();
- }
-
public override async Task StartAppAsync()
{
try
{
- var mainAppPath = CheckPath(_configinfo.InstallPath, _configinfo.MainAppName);
- if (string.IsNullOrEmpty(mainAppPath))
- throw new Exception($"Can't find the app {mainAppPath}!");
+ var appName = LaunchAppName ?? throw new InvalidOperationException("LaunchAppName must be set before calling StartAppAsync.");
+ var appPath = ResolveAppPath(appName, UseUpdatePath);
+ if (string.IsNullOrEmpty(appPath))
+ throw new Exception($"Can't find the app {appName}!");
- GeneralTracer.Info($"GeneralUpdate.Core.LinuxStrategy.StartApp: launching main app={mainAppPath}");
- Process.Start(mainAppPath);
- GeneralTracer.Info("GeneralUpdate.Core.LinuxStrategy.StartApp: main app launched successfully.");
+ GeneralTracer.Info($"GeneralUpdate.Core.LinuxStrategy.StartApp: launching app={appPath}");
+ Process.Start(appPath);
+ GeneralTracer.Info("GeneralUpdate.Core.LinuxStrategy.StartApp: app launched successfully.");
}
catch (Exception e)
{
@@ -58,5 +53,4 @@ public override async Task StartAppAsync()
await GracefulExit.CurrentProcessAsync();
}
}
-
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs
index 4373bcd3..08737b34 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/MacStrategy.cs
@@ -21,13 +21,12 @@ public override async Task StartAppAsync()
{
try
{
- var mainApp = Path.Combine(
- _configinfo.InstallPath ?? string.Empty,
- _configinfo.MainAppName ?? string.Empty);
+ var appName = LaunchAppName ?? throw new InvalidOperationException("LaunchAppName must be set before calling StartAppAsync.");
+ var mainApp = ResolveAppPath(appName, UseUpdatePath);
- if (!string.IsNullOrEmpty(_configinfo.MainAppName) && File.Exists(mainApp))
+ if (!string.IsNullOrEmpty(mainApp) && File.Exists(mainApp))
{
- GeneralTracer.Info($"MacStrategy: starting {mainApp}");
+ GeneralTracer.Info($"MacStrategy.StartApp: launching app={mainApp}");
System.Diagnostics.Process.Start(mainApp);
}
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
index 122d1cde..74f12f0a 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
@@ -16,11 +16,11 @@
namespace GeneralUpdate.Core.Strategy;
///
-/// OSS (Object Storage Service) update strategy — client/upgrade split via AppType.
+/// OSS (Object Storage Service) update strategy -- client/upgrade split via AppType.
///
-/// - — downloads version config, checks for updates,
+///
- -- downloads version config, checks for updates,
/// starts the upgrade process, and exits.
-/// - — reads version config, downloads packages from OSS,
+///
- -- reads version config, downloads packages from OSS,
/// decompresses them, starts the main app, and exits.
///
///
@@ -51,7 +51,7 @@ public async Task ExecuteAsync()
if (_configInfo == null)
throw new InvalidOperationException("OSSUpdateStrategy not configured. Call Create() first.");
- // Dispatch by role — no env-var detection needed.
+ // Dispatch by role �?no env-var detection needed.
if (_role == AppType.OSSUpgrade)
{
await ExecuteUpgradeAsync();
@@ -69,8 +69,9 @@ private async Task ExecuteClientAsync()
{
GeneralTracer.Debug("OSSUpdateStrategy (client): checking for updates.");
- var versionFileName = $"{_configInfo!.MainAppName ?? _configInfo.AppName}_versions.json";
- var versionsFilePath = Path.Combine(_appPath, versionFileName);
+ var installPath = _configInfo!.InstallPath;
+ var versionFileName = $"{_configInfo.MainAppName ?? _configInfo.UpdateAppName}_versions.json";
+ var versionsFilePath = Path.Combine(installPath, versionFileName);
if (!string.IsNullOrEmpty(_configInfo.UpdateUrl))
{
@@ -101,13 +102,18 @@ private async Task ExecuteClientAsync()
return;
}
- // Use user-configured AppName or default upgrade exe
- var upgradeAppName = !string.IsNullOrWhiteSpace(_configInfo.AppName) && _configInfo.AppName != "Update.exe"
- ? _configInfo.AppName
+ // Resolve upgrade exe: prefer UpdatePath, fall back to InstallPath
+ var upgradeDir = !string.IsNullOrWhiteSpace(_configInfo.UpdatePath)
+ ? (Path.IsPathRooted(_configInfo.UpdatePath)
+ ? _configInfo.UpdatePath
+ : Path.Combine(installPath, _configInfo.UpdatePath))
+ : installPath;
+ var upgradeAppName = !string.IsNullOrWhiteSpace(_configInfo.UpdateAppName)
+ ? _configInfo.UpdateAppName
: "GeneralUpdate.Upgrade.exe";
- var appPath = Path.Combine(_appPath, upgradeAppName);
+ var appPath = Path.Combine(upgradeDir, upgradeAppName);
if (!File.Exists(appPath))
- throw new FileNotFoundException($"Upgrade application not found: {upgradeAppName}");
+ throw new FileNotFoundException($"Upgrade application not found: {appPath}");
Process.Start(appPath);
await GracefulExit.CurrentProcessAsync().ConfigureAwait(false);
@@ -122,8 +128,10 @@ private async Task ExecuteUpgradeAsync()
var ctx = BuildUpdateContext();
try
{
- var versionFileName = $"{_configInfo!.MainAppName ?? _configInfo.AppName}_versions.json";
- var jsonPath = Path.Combine(_appPath, versionFileName);
+ // Client downloaded the version JSON to InstallPath; Upgrade reads it from there
+ var installPath = _configInfo!.InstallPath;
+ var versionFileName = $"{_configInfo.MainAppName ?? _configInfo.UpdateAppName}_versions.json";
+ var jsonPath = Path.Combine(installPath, versionFileName);
if (!File.Exists(jsonPath) && DownloadSource == null)
throw new FileNotFoundException($"Version config not found: {jsonPath}");
@@ -141,7 +149,8 @@ private async Task ExecuteUpgradeAsync()
List assets;
if (DownloadSource != null)
{
- assets = (await DownloadSource.ListAsync().ConfigureAwait(false)).ToList();
+ var sourceResult = await DownloadSource.ListAsync().ConfigureAwait(false);
+ assets = sourceResult.Assets.ToList();
}
else
{
@@ -158,9 +167,7 @@ private async Task ExecuteUpgradeAsync()
if (string.IsNullOrWhiteSpace(v.Url))
throw new InvalidOperationException(
$"OSS version '{v.PacketName ?? v.Version}' has no download URL.");
- var zipName = $"{v.PacketName ?? v.Version}zip";
- if (!zipName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
- zipName += ".zip";
+ var zipName = $"{v.PacketName ?? v.Version}{Format.Zip.ToExtension()}";
return new DownloadAsset(
Name: zipName, Url: v.Url, Size: 0,
SHA256: v.Hash, Version: v.Version ?? "0.0.0");
@@ -171,10 +178,11 @@ private async Task ExecuteUpgradeAsync()
throw new InvalidOperationException("No assets to download.");
GeneralTracer.Debug($"OSSUpdateStrategy (upgrade): downloading {assets.Count} asset(s).");
- await DownloadAssetsAsync(assets).ConfigureAwait(false);
+ await DownloadAssetsAsync(assets, installPath).ConfigureAwait(false);
GeneralTracer.Debug("OSSUpdateStrategy (upgrade): decompressing.");
- DecompressAssets(assets);
+ var encoding = Encoding.GetEncoding(_configInfo?.Encoding?.CodePage ?? Encoding.UTF8.CodePage);
+ DecompressAssets(assets, installPath, encoding);
await SafeOnDownloadCompletedAsync(ctx).ConfigureAwait(false);
await SafeOnAfterUpdateAsync(ctx).ConfigureAwait(false);
@@ -199,10 +207,11 @@ private async Task ExecuteUpgradeAsync()
public Task StartAppAsync()
{
- var appName = _configInfo?.MainAppName ?? _configInfo?.AppName;
+ var appName = _configInfo?.MainAppName ?? _configInfo?.UpdateAppName;
if (string.IsNullOrEmpty(appName)) return Task.CompletedTask;
- var appPath = Path.Combine(_appPath, appName);
+ var targetDir = _configInfo?.InstallPath ?? _appPath;
+ var appPath = Path.Combine(targetDir, appName);
if (!File.Exists(appPath))
throw new FileNotFoundException($"Application not found: {appPath}");
@@ -234,12 +243,12 @@ private static bool IsOssUpgrade(string clientVersion, string serverVersion)
&& cv < sv;
}
- private async Task DownloadAssetsAsync(List assets)
+ private async Task DownloadAssetsAsync(List assets, string targetPath)
{
var plan = new DownloadPlan(assets, false);
if (DownloadOrchestrator != null)
{
- await DownloadOrchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false);
+ await DownloadOrchestrator.ExecuteAsync(plan, targetPath).ConfigureAwait(false);
}
else
{
@@ -248,17 +257,16 @@ private async Task DownloadAssetsAsync(List assets)
Timeout = TimeSpan.FromSeconds(_configInfo?.DownloadTimeOut > 0 ? _configInfo!.DownloadTimeOut : DefaultTimeOut)
};
var orchestrator = new DefaultDownloadOrchestrator(httpClient);
- await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false);
+ await orchestrator.ExecuteAsync(plan, targetPath).ConfigureAwait(false);
}
}
- private void DecompressAssets(List assets)
+ private static void DecompressAssets(List assets, string targetPath, Encoding encoding)
{
- var encoding = Encoding.GetEncoding(_configInfo?.Encoding?.CodePage ?? Encoding.UTF8.CodePage);
foreach (var asset in assets)
{
- var zipFilePath = Path.Combine(_appPath, $"{asset.Name}{Format.ZIP}");
- CompressProvider.Decompress(Format.ZIP, zipFilePath, _appPath, encoding);
+ var zipFilePath = Path.Combine(targetPath, asset.Name);
+ CompressProvider.Decompress(Format.Zip, zipFilePath, targetPath, encoding);
if (!File.Exists(zipFilePath)) continue;
File.SetAttributes(zipFilePath, FileAttributes.Normal);
@@ -269,7 +277,7 @@ private void DecompressAssets(List assets)
private Hooks.UpdateContext BuildUpdateContext()
{
return new Hooks.UpdateContext(
- _configInfo?.AppName ?? "unknown",
+ _configInfo?.UpdateAppName ?? "unknown",
_configInfo?.InstallPath ?? _appPath,
_configInfo?.ClientVersion ?? "0.0.0",
_configInfo?.LastVersion,
@@ -302,7 +310,7 @@ private async Task SafeOnDownloadCompletedAsync(Hooks.UpdateContext ctx)
try
{
var downloadCtx = new Hooks.DownloadContext(
- _configInfo?.MainAppName ?? _configInfo?.AppName ?? "unknown",
+ _configInfo?.MainAppName ?? _configInfo?.UpdateAppName ?? "unknown",
_configInfo?.LastVersion ?? "", 0, TimeSpan.Zero, _appPath, true);
await Hooks.OnDownloadCompletedAsync(downloadCtx).ConfigureAwait(false);
}
@@ -312,10 +320,7 @@ private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx)
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateStarted, ctx.AppType, DateTimeOffset.UtcNow
- )).ConfigureAwait(false);
+ await Reporter.ReportAsync(new Download.Reporting.UpdateReport(0, (int)Download.Reporting.UpdateStatus.Updating, 1)).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); }
}
@@ -323,10 +328,7 @@ private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx)
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateApplied, ctx.AppType, DateTimeOffset.UtcNow
- )).ConfigureAwait(false);
+ await Reporter.ReportAsync(new Download.Reporting.UpdateReport(0, (int)Download.Reporting.UpdateStatus.Success, 1)).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); }
}
@@ -334,11 +336,7 @@ private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exceptio
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow,
- ErrorMessage: error.Message
- )).ConfigureAwait(false);
+ await Reporter.ReportAsync(new Download.Reporting.UpdateReport(0, (int)Download.Reporting.UpdateStatus.Failure, 1)).ConfigureAwait(false);
}
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); }
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs
index be5218e9..f413288b 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs
@@ -21,7 +21,7 @@ namespace GeneralUpdate.Core.Strategy;
/// Design: Upgrade does NOT validate versions or download packages.
/// The client has already validated versions, downloaded all packages, and
/// passed the results via ProcessInfo. Upgrade only applies updates and
-/// starts the main application — zero network.
+/// starts the main application -- zero network.
///
public class UpgradeUpdateStrategy : IStrategy
{
@@ -30,6 +30,7 @@ public class UpgradeUpdateStrategy : IStrategy
/// Lifecycle hooks injected by the bootstrap.
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
+
/// Update status reporter injected by the bootstrap.
public Download.Reporting.IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter();
@@ -37,8 +38,12 @@ public void Create(GlobalConfigInfo parameter)
{
_configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter));
_osStrategy = ResolveOsStrategy();
- if (_pendingDirtyStrategy != null && _osStrategy is AbstractStrategy abs)
- abs.DirtyStrategy = _pendingDirtyStrategy;
+ if (_osStrategy is AbstractStrategy abs)
+ {
+ if (_pendingDirtyStrategy != null) abs.DirtyStrategy = _pendingDirtyStrategy;
+ if (_pendingBinaryDiffer != null) abs.BinaryDiffer = _pendingBinaryDiffer;
+ if (_pendingDiffPipeline != null) abs.DiffPipeline = _pendingDiffPipeline;
+ }
}
public async Task ExecuteAsync()
@@ -59,10 +64,11 @@ public async Task ExecuteAsync()
_osStrategy!.Create(_configInfo);
- // Apply MainApp updates — Client already applied Upgrade packages, IPC only has MainApp versions
+ // Apply MainApp updates -- Client already applied Upgrade packages, IPC only has MainApp versions
if (_configInfo.UpdateVersions?.Count > 0)
{
- GeneralTracer.Info("UpgradeUpdateStrategy: applying " + _configInfo.UpdateVersions.Count + " MainApp update(s).");
+ GeneralTracer.Info("UpgradeUpdateStrategy: applying " + _configInfo.UpdateVersions.Count +
+ " MainApp update(s).");
await _osStrategy.ExecuteAsync();
}
else
@@ -79,7 +85,22 @@ public async Task ExecuteAsync()
// Hooks: before starting main app (e.g. chmod +x on Linux/macOS)
await SafeOnBeforeStartAppAsync(ctx).ConfigureAwait(false);
- await _osStrategy.StartAppAsync();
+ // Delegate to OS strategy: launch MainAppName + Bowl.
+ // Skip if silent mode requested no-launch (e.g. maintenance windows).
+ if (_configInfo.LaunchClientAfterUpdate)
+ {
+ if (_osStrategy is AbstractStrategy abs2)
+ {
+ abs2.LaunchAppName = _configInfo.MainAppName;
+ abs2.LaunchBowl = true;
+ }
+
+ await _osStrategy.StartAppAsync();
+ }
+ else
+ {
+ GeneralTracer.Info("UpgradeUpdateStrategy: LaunchClientAfterUpdate=false, skipping app launch.");
+ }
}
catch (Exception ex)
{
@@ -91,6 +112,8 @@ public async Task ExecuteAsync()
}
private IDirtyStrategy? _pendingDirtyStrategy;
+ private IBinaryDiffer? _pendingBinaryDiffer;
+ private DiffPipeline? _pendingDiffPipeline;
/// Sets the directory-level dirty strategy on the underlying OS-level strategy for differential patch updates.
/// Safe to call before or after Create(). If called before, the strategy is cached and applied when Create() resolves _osStrategy.
@@ -107,6 +130,8 @@ public void SetBinaryDiffer(IBinaryDiffer? binaryDiffer)
{
if (_osStrategy is AbstractStrategy abs)
abs.BinaryDiffer = binaryDiffer;
+ else
+ _pendingBinaryDiffer = binaryDiffer;
}
/// Sets the DiffPipeline on the underlying OS-level strategy for parallel patch application.
@@ -114,6 +139,8 @@ public void SetDiffPipeline(DiffPipeline? diffPipeline)
{
if (_osStrategy is AbstractStrategy abs)
abs.DiffPipeline = diffPipeline;
+ else
+ _pendingDiffPipeline = diffPipeline;
}
public async Task StartAppAsync()
@@ -142,7 +169,7 @@ private static IStrategy ResolveOsStrategy()
private Hooks.UpdateContext BuildUpdateContext()
{
return new Hooks.UpdateContext(
- _configInfo?.AppName ?? "unknown",
+ _configInfo?.UpdateAppName ?? "unknown",
_configInfo?.InstallPath ?? AppDomain.CurrentDomain.BaseDirectory,
_configInfo?.ClientVersion ?? "0.0.0",
_configInfo?.LastVersion,
@@ -152,52 +179,81 @@ private Hooks.UpdateContext BuildUpdateContext()
private async Task SafeOnBeforeUpdateAsync(Hooks.UpdateContext ctx)
{
- try { return await Hooks.OnBeforeUpdateAsync(ctx).ConfigureAwait(false); }
- catch (Exception ex) { GeneralTracer.Warn($"OnBeforeUpdateAsync hook failed: {ex.Message}"); return true; }
+ try
+ {
+ return await Hooks.OnBeforeUpdateAsync(ctx).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"OnBeforeUpdateAsync hook failed: {ex.Message}");
+ return true;
+ }
}
private async Task SafeOnAfterUpdateAsync(Hooks.UpdateContext ctx)
{
- try { await Hooks.OnAfterUpdateAsync(ctx).ConfigureAwait(false); }
- catch (Exception ex) { GeneralTracer.Warn($"OnAfterUpdateAsync hook failed: {ex.Message}"); }
+ try
+ {
+ await Hooks.OnAfterUpdateAsync(ctx).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"OnAfterUpdateAsync hook failed: {ex.Message}");
+ }
}
private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx)
{
- try { await Hooks.OnBeforeStartAppAsync(ctx).ConfigureAwait(false); }
- catch (Exception ex) { GeneralTracer.Warn($"OnBeforeStartAppAsync hook failed: {ex.Message}"); }
+ try
+ {
+ await Hooks.OnBeforeStartAppAsync(ctx).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"OnBeforeStartAppAsync hook failed: {ex.Message}");
+ }
}
private async Task SafeOnUpdateErrorAsync(Hooks.UpdateContext ctx, Exception error)
{
- try { await Hooks.OnUpdateErrorAsync(ctx, error).ConfigureAwait(false); }
- catch (Exception ex) { GeneralTracer.Warn($"OnUpdateErrorAsync hook failed: {ex.Message}"); }
+ try
+ {
+ await Hooks.OnUpdateErrorAsync(ctx, error).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"OnUpdateErrorAsync hook failed: {ex.Message}");
+ }
}
private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx)
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateApplied, ctx.AppType, DateTimeOffset.UtcNow
- )).ConfigureAwait(false);
+ await Reporter
+ .ReportAsync(new Download.Reporting.UpdateReport(0, (int)Download.Reporting.UpdateStatus.Success,
+ 1)).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}");
}
- catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); }
}
private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error)
{
try
{
- await Reporter.ReportAsync(new Download.Reporting.UpdateReport(
- ctx.AppName, ctx.CurrentVersion, ctx.TargetVersion,
- Download.Reporting.UpdateEvent.UpdateFailed, ctx.AppType, DateTimeOffset.UtcNow,
- ErrorMessage: error.Message
- )).ConfigureAwait(false);
+ await Reporter
+ .ReportAsync(
+ new Download.Reporting.UpdateReport(0, (int)Download.Reporting.UpdateStatus.Failure, 1))
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}");
}
- catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); }
}
#endregion
-}
+}
\ No newline at end of file
diff --git a/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs
index 72d5e613..bcec709c 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/WindowsStrategy.cs
@@ -30,29 +30,27 @@ protected override PipelineBuilder BuildPipeline(PipelineContext context)
return builder;
}
- protected override async Task OnExecuteCompleteAsync()
- {
- GeneralTracer.Info("GeneralUpdate.Core.WindowsStrategy.OnExecuteComplete: all versions processed, starting application.");
- await StartAppAsync();
- }
-
public override async Task StartAppAsync()
{
try
{
- var mainAppPath = CheckPath(_configinfo.InstallPath, _configinfo.MainAppName);
- if (string.IsNullOrEmpty(mainAppPath))
- throw new Exception($"Can't find the app {mainAppPath}!");
+ var appName = LaunchAppName ?? throw new InvalidOperationException("LaunchAppName must be set before calling StartAppAsync.");
+ var appPath = ResolveAppPath(appName, UseUpdatePath);
+ if (string.IsNullOrEmpty(appPath))
+ throw new Exception($"Can't find the app {appName}!");
- GeneralTracer.Info($"GeneralUpdate.Core.WindowsStrategy.StartApp: launching main app={mainAppPath}");
- Process.Start(mainAppPath);
- GeneralTracer.Info("GeneralUpdate.Core.WindowsStrategy.StartApp: main app launched successfully.");
+ GeneralTracer.Info($"GeneralUpdate.Core.WindowsStrategy.StartApp: launching app={appPath}");
+ Process.Start(appPath);
+ GeneralTracer.Info("GeneralUpdate.Core.WindowsStrategy.StartApp: app launched successfully.");
- var bowlAppPath = CheckPath(_configinfo.InstallPath, _configinfo.Bowl);
- if (!string.IsNullOrEmpty(bowlAppPath))
+ if (LaunchBowl)
{
- GeneralTracer.Info($"GeneralUpdate.Core.WindowsStrategy.StartApp: launching Bowl process={bowlAppPath}");
- Process.Start(bowlAppPath);
+ var bowlAppPath = CheckPath(_configinfo.InstallPath, _configinfo.Bowl);
+ if (!string.IsNullOrEmpty(bowlAppPath))
+ {
+ GeneralTracer.Info($"GeneralUpdate.Core.WindowsStrategy.StartApp: launching Bowl process={bowlAppPath}");
+ Process.Start(bowlAppPath);
+ }
}
}
catch (Exception e)
diff --git a/src/c#/GeneralUpdate.slnx b/src/c#/GeneralUpdate.slnx
index 7ec27ed9..f0af67bd 100644
--- a/src/c#/GeneralUpdate.slnx
+++ b/src/c#/GeneralUpdate.slnx
@@ -9,9 +9,11 @@
+
+
diff --git a/tests/ClientTest/ClientTest.csproj b/tests/ClientTest/ClientTest.csproj
new file mode 100644
index 00000000..191c3d61
--- /dev/null
+++ b/tests/ClientTest/ClientTest.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/tests/ClientTest/Program.cs b/tests/ClientTest/Program.cs
new file mode 100644
index 00000000..80c69cdc
--- /dev/null
+++ b/tests/ClientTest/Program.cs
@@ -0,0 +1,134 @@
+using GeneralUpdate.Core;
+using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.Download;
+using GeneralUpdate.Core.Event;
+using GeneralUpdate.Core.Hooks;
+
+try
+{
+ Console.WriteLine("=== GeneralUpdate Client Test ===");
+ Console.WriteLine($"Started at {DateTime.Now}");
+ Console.WriteLine($"Running from: {AppDomain.CurrentDomain.BaseDirectory}");
+
+ var updateUrl = "http://localhost:5000/Upgrade/Verification";
+ var reportUrl = "http://localhost:5000/Upgrade/Report";
+ var appSecretKey = Environment.GetEnvironmentVariable("APP_SECRET_KEY") ?? "dfeb5833-975e-4afb-88f1-6278ee9aeff6";
+ var productId = Environment.GetEnvironmentVariable("PRODUCT_ID") ?? "2d974e2a-31e6-4887-9bb1-b4689e98c77a";
+ var clientVersion = Environment.GetEnvironmentVariable("CLIENT_VERSION") ?? "1.0.0.0";
+
+ Console.WriteLine($"UpdateUrl: {updateUrl}");
+ Console.WriteLine($"ReportUrl: {reportUrl}");
+ Console.WriteLine($"ClientVersion: {clientVersion}");
+ Console.WriteLine($"ProductId: {productId}");
+ Console.WriteLine();
+
+ var config = new Configinfo
+ {
+ UpdateUrl = updateUrl,
+ ReportUrl = reportUrl,
+ UpdatePath = "Upgrade",
+ UpdateAppName = "UpgradeTest.exe",
+ MainAppName = "ClientTest.exe",
+ InstallPath = AppDomain.CurrentDomain.BaseDirectory,
+ ClientVersion = clientVersion,
+ UpgradeClientVersion = "1.0.0.0",
+ ProductId = productId,
+ AppSecretKey = appSecretKey,
+ };
+
+ await new GeneralUpdateBootstrap()
+ .SetConfig(config)
+ .Option(UpdateOptions.AppType, AppType.Client)
+ .Hooks()
+ .AddListenerMultiDownloadStatistics(OnDownloadStatistics)
+ .AddListenerMultiDownloadCompleted(OnDownloadCompleted)
+ .AddListenerMultiAllDownloadCompleted(OnAllDownloadCompleted)
+ .AddListenerMultiDownloadError(OnDownloadError)
+ .AddListenerException(OnException)
+ .AddListenerUpdateInfo(OnUpdateInfo)
+ .LaunchAsync();
+
+ Console.WriteLine("Client test completed.");
+}
+catch (Exception ex)
+{
+ Console.WriteLine($"FATAL: {ex}");
+ Environment.Exit(1);
+}
+
+static void OnDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs e)
+{
+ var v = e.Version as VersionInfo;
+ Console.WriteLine($"[Download] {v?.Version}: {e.ProgressPercentage}% | {e.Speed} | ETA: {e.Remaining}");
+}
+
+static void OnDownloadCompleted(object sender, MultiDownloadCompletedEventArgs e)
+{
+ var v = e.Version as VersionInfo;
+ Console.WriteLine($"[Download] {v?.Version}: {(e.IsCompleted ? "SUCCESS" : "FAILED")}");
+}
+
+static void OnAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs e)
+{
+ Console.WriteLine(e.IsAllDownloadCompleted
+ ? "[Download] All downloads completed."
+ : $"[Download] Downloads finished with {e.FailedVersions.Count} failure(s).");
+}
+
+static void OnDownloadError(object sender, MultiDownloadErrorEventArgs e)
+{
+ var v = e.Version as VersionInfo;
+ Console.WriteLine($"[Download] Error @ {v?.Version}: {e.Exception.Message}");
+}
+
+static void OnException(object sender, ExceptionEventArgs e)
+{
+ Console.WriteLine($"[Error] {e.Exception}");
+}
+
+static void OnUpdateInfo(object sender, UpdateInfoEventArgs e)
+{
+ Console.WriteLine($"[UpdateInfo] Code={e.Info?.Code}, Message={e.Info?.Message}");
+ if (e.Info?.Body is { Count: > 0 })
+ {
+ foreach (var vi in e.Info.Body)
+ Console.WriteLine($" - {vi.Version} ({vi.Name}) [{vi.Size} bytes] {(vi.IsForcibly == true ? "(forced)" : "")}");
+ }
+ else
+ {
+ Console.WriteLine(" No updates available.");
+ }
+}
+
+sealed class ClientTestHooks : IUpdateHooks
+{
+ public async Task OnBeforeUpdateAsync(UpdateContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnBeforeUpdate: {ctx.CurrentVersion} -> {ctx.TargetVersion}");
+ return await Task.FromResult(true);
+ }
+
+ public async Task OnDownloadCompletedAsync(DownloadContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnDownloadCompleted: {ctx.AssetName} v{ctx.Version} ({ctx.TotalBytes} bytes, {ctx.Duration}) {(ctx.Success ? "OK" : "FAIL")}");
+ await Task.CompletedTask;
+ }
+
+ public async Task OnAfterUpdateAsync(UpdateContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnAfterUpdate: {ctx.CurrentVersion} -> {ctx.TargetVersion}");
+ await Task.CompletedTask;
+ }
+
+ public async Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex)
+ {
+ Console.WriteLine($"[Hook] OnUpdateError: {ctx.CurrentVersion} -> {ctx.TargetVersion} | {ex.Message}");
+ await Task.CompletedTask;
+ }
+
+ public async Task OnBeforeStartAppAsync(UpdateContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnBeforeStartApp: {ctx.UpdateAppName} @ {ctx.InstallPath}");
+ await Task.CompletedTask;
+ }
+}
diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
index af50e332..0132fcd8 100644
--- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
+++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
@@ -7,7 +7,9 @@
using System.Threading.Tasks;
using GeneralUpdate.Core;
using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.Download.Reporting;
using GeneralUpdate.Core.FileSystem;
+using GeneralUpdate.Core.Hooks;
using Xunit;
namespace CoreTest.Bootstrap
@@ -53,7 +55,7 @@ public void Dispose()
[Fact] public void DiffMode_Parallel() => Assert.NotNull(B().Option(UpdateOptions.DiffMode, DiffMode.Parallel));
[Fact] public void Encoding_Utf8() => Assert.NotNull(B().Option(UpdateOptions.Encoding, Encoding.UTF8));
[Fact] public void Encoding_Ascii() => Assert.NotNull(B().Option(UpdateOptions.Encoding, Encoding.ASCII));
- [Fact] public void Format_ZIP() => Assert.NotNull(B().Option(UpdateOptions.Format, "ZIP"));
+ [Fact] public void Format_ZIP() => Assert.NotNull(B().Option(UpdateOptions.Format, Format.Zip));
[Theory][InlineData(10)][InlineData(30)][InlineData(60)][InlineData(300)]
public void DownloadTimeout_Various(int t) => Assert.NotNull(B().Option(UpdateOptions.DownloadTimeout, t));
[Fact] public void PatchEnabled_True() => Assert.NotNull(B().Option(UpdateOptions.PatchEnabled, true));
@@ -63,7 +65,6 @@ public void Dispose()
#endregion
#region Silent
- [Fact] public void SilentAutoInstall_True() => Assert.NotNull(B().Option(UpdateOptions.SilentAutoInstall, true));
[Theory][InlineData(15)][InlineData(30)][InlineData(60)]
public void SilentPollInterval_Various(int m) => Assert.NotNull(B().Option(UpdateOptions.SilentPollIntervalMinutes, m));
#endregion
@@ -79,8 +80,6 @@ public void Dispose()
#endregion
#region Blacklist/Misc
- [Fact] public void Hub_Configured() => Assert.NotNull(B().Option(UpdateOptions.Hub,
- new HubConfig { Url = "https://signalr.example.com/hub" }));
#endregion
#region Extension Injection — Hooks / Strategy / Policy / Differ / Pipeline / etc.
@@ -97,6 +96,8 @@ private sealed class StubHooks : GeneralUpdate.Core.Hooks.IUpdateHooks
private sealed class StubStrategy : GeneralUpdate.Core.Strategy.IStrategy
{
public void Create(GlobalConfigInfo parameter) { }
+ public IUpdateHooks Hooks { get; set; }
+ public IUpdateReporter Reporter { get; set; }
public Task ExecuteAsync() => Task.CompletedTask;
public Task StartAppAsync() => Task.CompletedTask;
}
@@ -128,8 +129,11 @@ private sealed class StubDownloadExecutor : GeneralUpdate.Core.Download.Abstract
private sealed class StubDownloadSource : GeneralUpdate.Core.Download.Abstractions.IDownloadSource
{
- public Task> ListAsync(CancellationToken token = default)
- => Task.FromResult>(Array.Empty());
+ public Task ListAsync(CancellationToken token = default)
+ => Task.FromResult(new GeneralUpdate.Core.Download.Models.DownloadSourceResult
+ {
+ Assets = Array.Empty()
+ });
}
private sealed class StubDownloadPipeline : GeneralUpdate.Core.Download.Abstractions.IDownloadPipeline
@@ -207,7 +211,7 @@ [Fact] public void Chain_AllFrameworkOptions()
.Option(UpdateOptions.AppType, AppType.Client)
.Option(UpdateOptions.DiffMode, DiffMode.Parallel)
.Option(UpdateOptions.Encoding, Encoding.UTF8)
- .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.Format, Format.Zip)
.Option(UpdateOptions.DownloadTimeout, 120)
.Option(UpdateOptions.PatchEnabled, true)
.Option(UpdateOptions.BackupEnabled, true)
@@ -217,7 +221,6 @@ [Fact] public void Chain_AllFrameworkOptions()
.Option(UpdateOptions.RetryCount, 5)
.Option(UpdateOptions.VerifyChecksum, true)
.Option(UpdateOptions.RetryInterval, TimeSpan.FromSeconds(2))
- .Option(UpdateOptions.SilentAutoInstall, false)
.Option(UpdateOptions.SilentPollIntervalMinutes, 30)
.SetConfig(new Configinfo
{
@@ -237,7 +240,6 @@ [Fact] public void Chain_SilentClient()
var b = new GeneralUpdateBootstrap()
.Option(UpdateOptions.AppType, AppType.Client)
.Option(UpdateOptions.Silent, true)
- .Option(UpdateOptions.SilentAutoInstall, true)
.Option(UpdateOptions.SilentPollIntervalMinutes, 15)
.SetConfig(new Configinfo { UpdateUrl = "https://api.example.com", MainAppName = "MyApp.exe", ClientVersion = "1.0.0", InstallPath = _testDir, AppSecretKey = "key", Scheme = "https", Token = "token" });
Assert.NotNull(b);
@@ -265,7 +267,7 @@ [Fact] public void Chain_ClientAndUpgrade_BothFullyConfigured()
var sharedConfig = new Configinfo
{
UpdateUrl = "https://update.enterprise.com/api/v2",
- AppName = "Update.exe", MainAppName = "EnterpriseApp.exe",
+ UpdateAppName = "Update.exe", MainAppName = "EnterpriseApp.exe",
ClientVersion = "4.2.1", UpgradeClientVersion = "2.0.0",
InstallPath = _testDir, AppSecretKey = "enterprise-prod-key-2026",
ProductId = "enterprise-app-v4",
@@ -281,7 +283,7 @@ [Fact] public void Chain_ClientAndUpgrade_BothFullyConfigured()
.Option(UpdateOptions.AppType, AppType.Client)
.Option(UpdateOptions.DiffMode, DiffMode.Parallel)
.Option(UpdateOptions.Encoding, Encoding.UTF8)
- .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.Format, Format.Zip)
.Option(UpdateOptions.DownloadTimeout, 120)
.Option(UpdateOptions.PatchEnabled, true)
.Option(UpdateOptions.BackupEnabled, true)
@@ -309,7 +311,7 @@ [Fact] public void Chain_ClientAndUpgrade_BothFullyConfigured()
.Option(UpdateOptions.AppType, AppType.Upgrade)
.Option(UpdateOptions.DiffMode, DiffMode.Parallel)
.Option(UpdateOptions.Encoding, Encoding.UTF8)
- .Option(UpdateOptions.Format, "ZIP")
+ .Option(UpdateOptions.Format, Format.Zip)
.Option(UpdateOptions.DownloadTimeout, 30)
.Option(UpdateOptions.PatchEnabled, true)
.Option(UpdateOptions.BackupEnabled, false)
diff --git a/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs
index 9f447218..a4b34dbf 100644
--- a/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs
+++ b/tests/CoreTest/Bootstrap/BootstrapHooksAndExtensionsTests.cs
@@ -156,29 +156,30 @@ public async Task NoOpUpdateHooks_AllMethods_ReturnDefaults()
[Fact]
public void UpdateReport_StartedEvent()
{
- var report = new UpdateReport("MyApp.exe", "1.0.0", "2.0.0", UpdateEvent.UpdateStarted, AppType.Client, DateTimeOffset.UtcNow);
- Assert.Equal(UpdateEvent.UpdateStarted, report.Event);
- Assert.Equal("1.0.0", report.FromVersion);
+ var report = new UpdateReport(123, (int)UpdateStatus.Updating, 1);
+ Assert.Equal(123, report.RecordId);
+ Assert.Equal((int)UpdateStatus.Updating, report.Status);
+ Assert.Equal(1, report.Type);
}
[Fact]
public void UpdateReport_FailedWithError()
{
- var report = new UpdateReport("App.exe", "1.0.0", "2.0.0", UpdateEvent.UpdateFailed, AppType.Client,
- DateTimeOffset.UtcNow, ErrorMessage: "Disk full", DurationMs: 15000.0);
- Assert.Equal("Disk full", report.ErrorMessage);
- Assert.Equal(15000.0, report.DurationMs);
+ var report = new UpdateReport(456, (int)UpdateStatus.Failure, 1);
+ Assert.Equal(456, report.RecordId);
+ Assert.Equal((int)UpdateStatus.Failure, report.Status);
+ Assert.Equal(1, report.Type);
}
[Fact]
- public void UpdateEvent_AllValues_AreDefined()
+ public void UpdateStatus_AllValues_AreDefined()
{
- var values = Enum.GetValues();
- Assert.Contains(UpdateEvent.UpdateStarted, values);
- Assert.Contains(UpdateEvent.DownloadCompleted, values);
- Assert.Contains(UpdateEvent.UpdateApplied, values);
- Assert.Contains(UpdateEvent.UpdateFailed, values);
- Assert.Contains(UpdateEvent.AppStarted, values);
+ var values = Enum.GetValues();
+ Assert.Contains(UpdateStatus.Updating, values);
+ Assert.Contains(UpdateStatus.Updating, values);
+ Assert.Contains(UpdateStatus.Success, values);
+ Assert.Contains(UpdateStatus.Failure, values);
+ Assert.Contains(UpdateStatus.Success, values);
}
#endregion
@@ -186,7 +187,7 @@ public void UpdateEvent_AllValues_AreDefined()
#region IUpdateEventListener
[Fact]
- public void UpdateEventListener_AllMethods_AreCallable()
+ public void UpdateStatusListener_AllMethods_AreCallable()
{
var listener = new TestEventListener();
var vi = new VersionInfo { Version = "2.0.0", Url = "https://cdn.example.com/pkg.zip", Format = "ZIP" };
@@ -220,7 +221,7 @@ private sealed class TestEventListener : IUpdateEventListener
public void UpdateContext_AllFields_SetCorrectly()
{
var ctx = new UpdateContext("MyApp.exe", "/opt/app", "3.2.1", "4.0.0", AppType.Client);
- Assert.Equal("MyApp.exe", ctx.AppName);
+ Assert.Equal("MyApp.exe", ctx.UpdateAppName);
Assert.Equal("3.2.1", ctx.CurrentVersion);
Assert.Equal("4.0.0", ctx.TargetVersion);
}
diff --git a/tests/CoreTest/Bootstrap/ClientUpgradeIntegrationTests.cs b/tests/CoreTest/Bootstrap/ClientUpgradeIntegrationTests.cs
index ba4b45c6..81fad062 100644
--- a/tests/CoreTest/Bootstrap/ClientUpgradeIntegrationTests.cs
+++ b/tests/CoreTest/Bootstrap/ClientUpgradeIntegrationTests.cs
@@ -48,7 +48,7 @@ public void ClientUpgrade_MutualUpdate_BothNeedUpdates_ConfiguresCorrectly()
var config = new Configinfo
{
UpdateUrl = "https://api.example.com/updates",
- AppName = "Update.exe",
+ UpdateAppName = "Update.exe",
MainAppName = "MyApp.exe",
ClientVersion = "1.0.0",
UpgradeClientVersion = "0.5.0",
@@ -387,7 +387,7 @@ public void ConfigurationMapper_MapToGlobalConfigInfo_MapsAllFields()
var configInfo = new Configinfo
{
UpdateUrl = "https://api.example.com/updates",
- AppName = "Update.exe",
+ UpdateAppName = "Update.exe",
MainAppName = "MyApp.exe",
ClientVersion = "1.0.0",
UpgradeClientVersion = "0.5.0",
@@ -409,7 +409,7 @@ public void ConfigurationMapper_MapToGlobalConfigInfo_MapsAllFields()
Assert.NotNull(globalConfig);
Assert.Equal("https://api.example.com/updates", globalConfig.UpdateUrl);
- Assert.Equal("Update.exe", globalConfig.AppName);
+ Assert.Equal("Update.exe", globalConfig.UpdateAppName);
Assert.Equal("MyApp.exe", globalConfig.MainAppName);
Assert.Equal("1.0.0", globalConfig.ClientVersion);
Assert.Equal("0.5.0", globalConfig.UpgradeClientVersion);
@@ -510,7 +510,7 @@ public void DeveloperScenario_FullProductionSetup_CompleteChain()
.SetConfig(new Configinfo
{
UpdateUrl = "https://update.mycompany.com/api",
- AppName = "Update.exe",
+ UpdateAppName = "Update.exe",
MainAppName = "MyProduct.exe",
ClientVersion = "3.2.1",
UpgradeClientVersion = "1.0.0",
diff --git a/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs b/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs
index 0c15789d..57c409e1 100644
--- a/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs
+++ b/tests/CoreTest/Bootstrap/ParameterMatrixAndEventTests.cs
@@ -492,7 +492,7 @@ public void Configinfo_FullConfiguration_AllFieldsValid()
var config = new Configinfo
{
UpdateUrl = "https://update.mycompany.com/v2/api",
- AppName = "Update.exe",
+ UpdateAppName = "Update.exe",
MainAppName = "EnterpriseApp.exe",
ClientVersion = "4.2.1-beta",
UpgradeClientVersion = "1.5.0",
diff --git a/tests/CoreTest/Compress/CompressProviderTests.cs b/tests/CoreTest/Compress/CompressProviderTests.cs
index ea853494..0ac2d8cb 100644
--- a/tests/CoreTest/Compress/CompressProviderTests.cs
+++ b/tests/CoreTest/Compress/CompressProviderTests.cs
@@ -1,4 +1,5 @@
using GeneralUpdate.Core.Compress;
+using GeneralUpdate.Core.Configuration;
namespace CoreTest.Compress;
@@ -14,7 +15,7 @@ public void Compress_ZipFormat_UsesZipStrategy()
try
{
var ex = Record.Exception(() =>
- CompressProvider.Compress(".zip", tempDir, destZip, false, System.Text.Encoding.UTF8));
+ CompressProvider.Compress(Format.Zip, tempDir, destZip, false, System.Text.Encoding.UTF8));
Assert.Null(ex);
Assert.True(File.Exists(destZip));
}
@@ -29,7 +30,7 @@ public void Compress_ZipFormat_UsesZipStrategy()
public void Compress_UnknownFormat_ThrowsArgumentException()
{
Assert.Throws(() =>
- CompressProvider.Compress("RAR", "source", "dest", false, System.Text.Encoding.UTF8));
+ CompressProvider.Compress((Format)99, "source", "dest", false, System.Text.Encoding.UTF8));
}
[Fact]
@@ -44,7 +45,7 @@ public void Decompress_ZipFormat_UsesZipStrategy()
{
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, zipPath);
var ex = Record.Exception(() =>
- CompressProvider.Decompress(".zip", zipPath, destDir, System.Text.Encoding.UTF8));
+ CompressProvider.Decompress(Format.Zip, zipPath, destDir, System.Text.Encoding.UTF8));
Assert.Null(ex);
Assert.True(File.Exists(Path.Combine(destDir, "test.txt")));
}
@@ -60,6 +61,6 @@ public void Decompress_ZipFormat_UsesZipStrategy()
public void Decompress_UnknownFormat_ThrowsArgumentException()
{
Assert.Throws(() =>
- CompressProvider.Decompress("7z", "source", "dest", System.Text.Encoding.UTF8));
+ CompressProvider.Decompress((Format)99, "source", "dest", System.Text.Encoding.UTF8));
}
}
diff --git a/tests/CoreTest/Configuration/BaseConfigInfoTests.cs b/tests/CoreTest/Configuration/BaseConfigInfoTests.cs
index 201e8fb2..f73746b6 100644
--- a/tests/CoreTest/Configuration/BaseConfigInfoTests.cs
+++ b/tests/CoreTest/Configuration/BaseConfigInfoTests.cs
@@ -14,7 +14,7 @@ private class TestableConfig : BaseConfigInfo { }
public void Ctor_AppName_DefaultsToUpdateExe()
{
var config = new TestableConfig();
- Assert.Equal("Update.exe", config.AppName);
+ Assert.Equal("Update.exe", config.UpdateAppName);
}
[Fact]
@@ -114,7 +114,7 @@ public void AllProperties_CanBeSetAndGet()
{
var config = new TestableConfig
{
- AppName = "MyApp.exe",
+ UpdateAppName = "MyApp.exe",
MainAppName = "MainApp",
InstallPath = "C:\\MyApp",
UpdateLogUrl = "https://logs.example.com",
@@ -130,7 +130,7 @@ public void AllProperties_CanBeSetAndGet()
DriverDirectory = "C:\\Drivers"
};
- Assert.Equal("MyApp.exe", config.AppName);
+ Assert.Equal("MyApp.exe", config.UpdateAppName);
Assert.Equal("MainApp", config.MainAppName);
Assert.Equal("C:\\MyApp", config.InstallPath);
Assert.Equal("https://logs.example.com", config.UpdateLogUrl);
diff --git a/tests/CoreTest/Configuration/ConfiginfoBuilderTests.cs b/tests/CoreTest/Configuration/ConfiginfoBuilderTests.cs
index 077fe7ef..df8ce7d7 100644
--- a/tests/CoreTest/Configuration/ConfiginfoBuilderTests.cs
+++ b/tests/CoreTest/Configuration/ConfiginfoBuilderTests.cs
@@ -51,11 +51,11 @@ public void SetScheme_InvalidValue_ThrowsArgumentException(string value)
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
- public void SetAppName_InvalidValue_ThrowsArgumentException(string value)
+ public void SetUpgradeAppName_InvalidValue_ThrowsArgumentException(string value)
{
var builder = new ConfiginfoBuilder();
- var ex = Assert.Throws(() => builder.SetAppName(value));
- Assert.Contains("AppName", ex.Message);
+ var ex = Assert.Throws(() => builder.SetUpgradeAppName(value));
+ Assert.Contains("UpdateAppName", ex.Message);
}
[Theory]
@@ -189,7 +189,7 @@ public void SetMethods_ReturnsSameBuilder_ForChaining()
.SetUpdateUrl("https://api.example.com")
.SetToken("token")
.SetScheme("https")
- .SetAppName("MyApp")
+ .SetUpgradeAppName("MyApp")
.SetMainAppName("MainApp")
.SetClientVersion("1.0.0");
Assert.Same(builder, result);
@@ -206,7 +206,7 @@ public void Build_WithRequiredFields_ReturnsConfiginfo()
.SetUpdateUrl("https://api.example.com")
.SetToken("token123")
.SetScheme("https")
- .SetAppName("MyApp.exe")
+ .SetUpgradeAppName("MyApp.exe")
.SetMainAppName("MyApp")
.SetClientVersion("1.0.0")
.SetAppSecretKey("secret")
@@ -217,7 +217,7 @@ public void Build_WithRequiredFields_ReturnsConfiginfo()
Assert.Equal("https://api.example.com", config.UpdateUrl);
Assert.Equal("token123", config.Token);
Assert.Equal("https", config.Scheme);
- Assert.Equal("MyApp.exe", config.AppName);
+ Assert.Equal("MyApp.exe", config.UpdateAppName);
Assert.Equal("MyApp", config.MainAppName);
Assert.Equal("1.0.0", config.ClientVersion);
Assert.Equal("secret", config.AppSecretKey);
@@ -231,7 +231,7 @@ public void Build_MissingRequiredFields_ThrowsInvalidOperationException()
.SetUpdateUrl("https://api.example.com")
.SetToken("token")
.SetScheme("https");
- // Missing AppName, MainAppName, AppSecretKey, ClientVersion, InstallPath
+ // Missing UpdateAppName, MainAppName, AppSecretKey, ClientVersion, InstallPath
var ex = Assert.Throws(() => builder.Build());
Assert.Contains("Failed to build valid Configinfo", ex.Message);
Assert.IsType(ex.InnerException);
@@ -244,7 +244,7 @@ public void Build_EmptyBlackLists_ReturnsEmptyListNotNll()
.SetUpdateUrl("https://api.example.com")
.SetToken("token123")
.SetScheme("https")
- .SetAppName("MyApp.exe")
+ .SetUpgradeAppName("MyApp.exe")
.SetMainAppName("MyApp")
.SetClientVersion("1.0.0")
.SetAppSecretKey("secret")
@@ -263,7 +263,7 @@ public void Build_WithOptionalFields_IncludesThem()
.SetUpdateUrl("https://api.example.com")
.SetToken("token123")
.SetScheme("https")
- .SetAppName("MyApp.exe")
+ .SetUpgradeAppName("MyApp.exe")
.SetMainAppName("MyApp")
.SetClientVersion("1.0.0")
.SetAppSecretKey("secret")
diff --git a/tests/CoreTest/Configuration/ConfiginfoTests.cs b/tests/CoreTest/Configuration/ConfiginfoTests.cs
index 6c9cbb08..744c2a92 100644
--- a/tests/CoreTest/Configuration/ConfiginfoTests.cs
+++ b/tests/CoreTest/Configuration/ConfiginfoTests.cs
@@ -15,7 +15,7 @@ public void Validate_UpdateUrlNullOrWhitespace_ThrowsArgumentException(string up
var config = new Configinfo
{
UpdateUrl = updateUrl,
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = "1.0.0",
@@ -31,7 +31,7 @@ public void Validate_UpdateUrlNotWellFormedUri_ThrowsArgumentException()
var config = new Configinfo
{
UpdateUrl = "not_a_valid_uri!!!",
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = "1.0.0",
@@ -48,7 +48,7 @@ public void Validate_UpdateUrlValid_DoesNotThrowForUpdateUrl(string url)
var config = new Configinfo
{
UpdateUrl = url,
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = "1.0.0",
@@ -65,7 +65,7 @@ public void Validate_UpdateLogUrlNull_Allowed()
{
UpdateUrl = "https://api.example.com",
UpdateLogUrl = null,
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = "1.0.0",
@@ -82,7 +82,7 @@ public void Validate_UpdateLogUrlInvalid_ThrowsArgumentException()
{
UpdateUrl = "https://api.example.com",
UpdateLogUrl = "not_a_uri!!!",
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = "1.0.0",
@@ -95,12 +95,12 @@ public void Validate_UpdateLogUrlInvalid_ThrowsArgumentException()
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
- public void Validate_AppNameNullOrWhitespace_ThrowsArgumentException(string appName)
+ public void Validate_UpgradeAppNameNullOrWhitespace_ThrowsArgumentException(string appName)
{
var config = new Configinfo
{
UpdateUrl = "https://api.example.com",
- AppName = appName,
+ UpdateAppName = appName,
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = "1.0.0",
@@ -113,13 +113,13 @@ public void Validate_AppNameNullOrWhitespace_ThrowsArgumentException(string appN
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
- public void Validate_MainAppNameNullOrWhitespace_ThrowsArgumentException(string mainAppName)
+ public void Validate_MainAppNameNullOrWhitespace_ThrowsArgumentException(string mainUpgradeAppName)
{
var config = new Configinfo
{
UpdateUrl = "https://api.example.com",
- AppName = "TestApp",
- MainAppName = mainAppName,
+ UpdateAppName = "TestApp",
+ MainAppName = mainUpgradeAppName,
AppSecretKey = "secret",
ClientVersion = "1.0.0",
InstallPath = "C:\\app"
@@ -136,7 +136,7 @@ public void Validate_AppSecretKeyNullOrWhitespace_ThrowsArgumentException(string
var config = new Configinfo
{
UpdateUrl = "https://api.example.com",
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = secretKey,
ClientVersion = "1.0.0",
@@ -154,7 +154,7 @@ public void Validate_ClientVersionNullOrWhitespace_ThrowsArgumentException(strin
var config = new Configinfo
{
UpdateUrl = "https://api.example.com",
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = clientVersion,
@@ -172,7 +172,7 @@ public void Validate_InstallPathNullOrWhitespace_ThrowsArgumentException(string
var config = new Configinfo
{
UpdateUrl = "https://api.example.com",
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret",
ClientVersion = "1.0.0",
@@ -188,7 +188,7 @@ public void Validate_AllFieldsValid_NoExceptionThrown()
{
UpdateUrl = "https://api.example.com/update",
UpdateLogUrl = "https://api.example.com/log",
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
AppSecretKey = "secret123",
ClientVersion = "1.0.0",
diff --git a/tests/CoreTest/Configuration/ConfigurationMapperExtendedTests.cs b/tests/CoreTest/Configuration/ConfigurationMapperExtendedTests.cs
index de52d9c2..a358c518 100644
--- a/tests/CoreTest/Configuration/ConfigurationMapperExtendedTests.cs
+++ b/tests/CoreTest/Configuration/ConfigurationMapperExtendedTests.cs
@@ -31,7 +31,7 @@ private GlobalConfigInfo CreateValidSource()
ClientVersion = "1.0.0",
LastVersion = "2.0.0",
Encoding = System.Text.Encoding.UTF8,
- Format = ".zip",
+ Format = Format.Zip,
AppSecretKey = "secret",
ReportUrl = "https://report.example.com",
BackupDirectory = Path.Combine(_tempInstallDir, "backup")
@@ -81,14 +81,14 @@ public void MapToGlobalConfigInfo_PreservesExistingTargetFieldsNotInSource()
BackupDirectory = "/custom/backup",
MaxConcurrency = 8
};
- var source = new Configinfo { AppName = "NewApp.exe" };
+ var source = new Configinfo { UpdateAppName = "NewApp.exe" };
var result = ConfigurationMapper.MapToGlobalConfigInfo(source, target);
Assert.Equal("/custom/temp", result.TempPath);
Assert.Equal("/custom/backup", result.BackupDirectory);
Assert.Equal(8, result.MaxConcurrency);
- Assert.Equal("NewApp.exe", result.AppName);
+ Assert.Equal("NewApp.exe", result.UpdateAppName);
}
#endregion
@@ -148,19 +148,19 @@ public void MapToProcessInfo_StandardConfig_CheckAllMappedProperties()
var source = CreateValidSource();
source.UpdateLogUrl = "https://logs.test.com";
source.Encoding = System.Text.Encoding.ASCII;
- source.Format = ".tar";
+ source.Format = Format.Zip;
source.DownloadTimeOut = 120;
var result = ConfigurationMapper.MapToProcessInfo(source,
OneVersion(), new List(), new List(), new List());
- Assert.Equal("MainApp", result.AppName); // MainAppName -> AppName
+ Assert.Equal("MainApp", result.AppName); // MainAppName -> UpdateAppName
Assert.Equal(source.InstallPath, result.InstallPath);
Assert.Equal("1.0.0", result.CurrentVersion);
Assert.Equal("2.0.0", result.LastVersion);
Assert.Equal("secret", result.AppSecretKey);
Assert.Equal("us-ascii", result.CompressEncoding);
- Assert.Equal(".tar", result.CompressFormat);
+ Assert.Equal(".zip", result.CompressFormat);
Assert.Equal(120, result.DownloadTimeOut);
Assert.Equal("https://logs.test.com", result.UpdateLogUrl);
Assert.Equal("https://report.example.com", result.ReportUrl);
@@ -176,7 +176,7 @@ public void CopyBaseFields_ConfiginfoToGlobalConfigInfo_Works()
{
var source = new Configinfo
{
- AppName = "App.exe",
+ UpdateAppName = "App.exe",
MainAppName = "Main",
InstallPath = "C:\\app",
ClientVersion = "v1",
@@ -189,7 +189,7 @@ public void CopyBaseFields_ConfiginfoToGlobalConfigInfo_Works()
ConfigurationMapper.CopyBaseFields(source, target);
- Assert.Equal("App.exe", target.AppName);
+ Assert.Equal("App.exe", target.UpdateAppName);
Assert.Equal("Main", target.MainAppName);
Assert.Equal("C:\\app", target.InstallPath);
Assert.Equal("v1", target.ClientVersion);
@@ -204,7 +204,7 @@ public void CopyBaseFields_GraphCopy_ConfiginfoToNewConfiginfo_Works()
{
var source = new Configinfo
{
- AppName = "Source.exe",
+ UpdateAppName = "Source.exe",
ClientVersion = "5.0.0",
Scheme = "https"
};
@@ -212,7 +212,7 @@ public void CopyBaseFields_GraphCopy_ConfiginfoToNewConfiginfo_Works()
ConfigurationMapper.CopyBaseFields(source, target);
- Assert.Equal("Source.exe", target.AppName);
+ Assert.Equal("Source.exe", target.UpdateAppName);
Assert.Equal("5.0.0", target.ClientVersion);
Assert.Equal("https", target.Scheme);
}
@@ -220,13 +220,13 @@ public void CopyBaseFields_GraphCopy_ConfiginfoToNewConfiginfo_Works()
[Fact]
public void CopyBaseFields_NullSource_DoesNotThrow()
{
- var target = new GlobalConfigInfo { AppName = "keep" };
+ var target = new GlobalConfigInfo { UpdateAppName = "keep" };
var ex = Record.Exception(() =>
ConfigurationMapper.CopyBaseFields(null!, target));
Assert.Null(ex);
- Assert.Equal("keep", target.AppName);
+ Assert.Equal("keep", target.UpdateAppName);
}
#endregion
diff --git a/tests/CoreTest/Configuration/ConfigurationMapperTests.cs b/tests/CoreTest/Configuration/ConfigurationMapperTests.cs
index 57da9121..c06bf462 100644
--- a/tests/CoreTest/Configuration/ConfigurationMapperTests.cs
+++ b/tests/CoreTest/Configuration/ConfigurationMapperTests.cs
@@ -10,7 +10,7 @@ public void MapToGlobalConfigInfo_TargetNull_CreatesNewInstance()
var source = new Configinfo
{
UpdateUrl = "https://api.example.com",
- AppName = "TestApp",
+ UpdateAppName = "TestApp",
MainAppName = "MainApp",
ClientVersion = "1.0.0",
AppSecretKey = "secret",
@@ -23,10 +23,10 @@ public void MapToGlobalConfigInfo_TargetNull_CreatesNewInstance()
[Fact]
public void MapToGlobalConfigInfo_SourceNull_ReturnsEmptyTarget()
{
- var target = new GlobalConfigInfo { AppName = "existing" };
+ var target = new GlobalConfigInfo { UpdateAppName = "existing" };
var result = ConfigurationMapper.MapToGlobalConfigInfo(null, target);
Assert.Same(target, result);
- Assert.Equal("existing", result.AppName); // Unchanged
+ Assert.Equal("existing", result.UpdateAppName); // Unchanged
}
[Fact]
@@ -42,7 +42,7 @@ public void MapToGlobalConfigInfo_MapsAllFields()
var source = new Configinfo
{
UpdateUrl = "https://api.example.com",
- AppName = "App.exe",
+ UpdateAppName = "App.exe",
MainAppName = "MainApp",
ClientVersion = "2.0.0",
AppSecretKey = "key123",
@@ -55,7 +55,7 @@ public void MapToGlobalConfigInfo_MapsAllFields()
};
var result = ConfigurationMapper.MapToGlobalConfigInfo(source);
Assert.Equal("https://api.example.com", result.UpdateUrl);
- Assert.Equal("App.exe", result.AppName);
+ Assert.Equal("App.exe", result.UpdateAppName);
Assert.Equal("MainApp", result.MainAppName);
Assert.Equal("2.0.0", result.ClientVersion);
Assert.Equal("key123", result.AppSecretKey);
@@ -79,7 +79,7 @@ public void MapToProcessInfo_SourceNull_ThrowsArgumentNullException()
}
[Fact]
- public void MapToProcessInfo_MapsAppNameToMainAppName()
+ public void MapToProcessInfo_MapsUpgradeAppNameToMainAppName()
{
var source = new GlobalConfigInfo
{
@@ -89,7 +89,7 @@ public void MapToProcessInfo_MapsAppNameToMainAppName()
LastVersion = "2.0.0",
AppSecretKey = "secret",
Encoding = System.Text.Encoding.UTF8,
- Format = ".zip",
+ Format = Format.Zip,
DownloadTimeOut = 30,
UpdateLogUrl = "https://log.example.com",
ReportUrl = "https://report.example.com",
@@ -127,7 +127,7 @@ public void CopyBaseFields_SourceNull_DoesNotThrow()
[Fact]
public void CopyBaseFields_TargetNull_DoesNotThrow()
{
- var source = new Configinfo { AppName = "source" };
+ var source = new Configinfo { UpdateAppName = "source" };
var exception = Record.Exception(() => ConfigurationMapper.CopyBaseFields(source, null));
Assert.Null(exception);
}
@@ -137,7 +137,7 @@ public void CopyBaseFields_CopiesAllBaseProperties()
{
var source = new Configinfo
{
- AppName = "App.exe",
+ UpdateAppName = "App.exe",
MainAppName = "Main",
InstallPath = "C:\\path",
UpdateLogUrl = "https://log",
@@ -152,7 +152,7 @@ public void CopyBaseFields_CopiesAllBaseProperties()
ConfigurationMapper.CopyBaseFields(source, target);
- Assert.Equal("App.exe", target.AppName);
+ Assert.Equal("App.exe", target.UpdateAppName);
Assert.Equal("Main", target.MainAppName);
Assert.Equal("C:\\path", target.InstallPath);
Assert.Equal("https://log", target.UpdateLogUrl);
diff --git a/tests/CoreTest/Configuration/ConfigurationModelsTests.cs b/tests/CoreTest/Configuration/ConfigurationModelsTests.cs
index 7940fd4a..a6d3f9bd 100644
--- a/tests/CoreTest/Configuration/ConfigurationModelsTests.cs
+++ b/tests/CoreTest/Configuration/ConfigurationModelsTests.cs
@@ -18,7 +18,7 @@ namespace CoreTest.Configuration
/// - DownloadStatus / DownloadPriority enums
/// - AppType / DiffMode / UpdateMode / PlatformType / OssProvider enums
/// - UpdateOption<T> value semantics
- /// - UpdateReport / UpdateEvent types
+ /// - UpdateReport / UpdateStatus types
///
public class ConfigurationModelsTests
{
@@ -68,35 +68,6 @@ public void BlackListConfig_Partial_SingleListOnly()
#endregion
- #region HubConfig
-
- [Fact]
- public void HubConfig_WithUrl_DefaultsReasonable()
- {
- var config = new HubConfig { Url = "https://signalr.example.com/hub" };
-
- Assert.Equal("https://signalr.example.com/hub", config.Url);
- Assert.Equal(TimeSpan.FromSeconds(5), config.ReconnectDelay);
- Assert.Equal(10, config.MaxReconnectAttempts);
- }
-
- [Fact]
- public void HubConfig_AllFields_Customized()
- {
- var config = new HubConfig
- {
- Url = "wss://push.example.com/update-hub",
- ReconnectDelay = TimeSpan.FromSeconds(10),
- MaxReconnectAttempts = 20
- };
-
- Assert.Equal("wss://push.example.com/update-hub", config.Url);
- Assert.Equal(TimeSpan.FromSeconds(10), config.ReconnectDelay);
- Assert.Equal(20, config.MaxReconnectAttempts);
- }
-
- #endregion
-
#region DownloadAsset
[Fact]
@@ -300,14 +271,14 @@ public void DownloadPriority_ThreeValues()
}
[Fact]
- public void UpdateEvent_FiveValues()
+ public void UpdateStatus_FiveValues()
{
- var values = Enum.GetValues();
- Assert.Contains(UpdateEvent.UpdateStarted, values);
- Assert.Contains(UpdateEvent.DownloadCompleted, values);
- Assert.Contains(UpdateEvent.UpdateApplied, values);
- Assert.Contains(UpdateEvent.UpdateFailed, values);
- Assert.Contains(UpdateEvent.AppStarted, values);
+ var values = Enum.GetValues();
+ Assert.Contains(UpdateStatus.Updating, values);
+ Assert.Contains(UpdateStatus.Updating, values);
+ Assert.Contains(UpdateStatus.Success, values);
+ Assert.Contains(UpdateStatus.Failure, values);
+ Assert.Contains(UpdateStatus.Success, values);
}
#endregion
diff --git a/tests/CoreTest/Configuration/GlobalConfigInfoWiringTests.cs b/tests/CoreTest/Configuration/GlobalConfigInfoWiringTests.cs
index 4303913a..dd7981b1 100644
--- a/tests/CoreTest/Configuration/GlobalConfigInfoWiringTests.cs
+++ b/tests/CoreTest/Configuration/GlobalConfigInfoWiringTests.cs
@@ -16,10 +16,10 @@ public class GlobalConfigInfoWiringTests
#region GlobalConfigInfo Default Values
[Fact]
- public void GlobalConfigInfo_MaxConcurrency_DefaultsTo3()
+ public void GlobalConfigInfo_MaxConcurrency_DefaultsTo2()
{
var config = new GlobalConfigInfo();
- Assert.Equal(3, config.MaxConcurrency);
+ Assert.Equal(2, config.MaxConcurrency);
}
[Fact]
diff --git a/tests/CoreTest/Configuration/HubConfigTests.cs b/tests/CoreTest/Configuration/HubConfigTests.cs
deleted file mode 100644
index 41304990..00000000
--- a/tests/CoreTest/Configuration/HubConfigTests.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using GeneralUpdate.Core.Configuration;
-
-namespace CoreTest.Configuration;
-
-///
-/// AAAT unit tests for — default values and property mutation.
-/// Covers: default construction, property get/set, boundary time spans, negative reconnects.
-///
-public class HubConfigTests
-{
- [Fact]
- public void Ctor_Default_UrlIsEmpty()
- {
- var config = new HubConfig();
- Assert.Equal(string.Empty, config.Url);
- }
-
- [Fact]
- public void Ctor_Default_ReconnectDelayIs5Seconds()
- {
- var config = new HubConfig();
- Assert.Equal(TimeSpan.FromSeconds(5), config.ReconnectDelay);
- }
-
- [Fact]
- public void Ctor_Default_MaxReconnectAttemptsIs10()
- {
- var config = new HubConfig();
- Assert.Equal(10, config.MaxReconnectAttempts);
- }
-
- [Fact]
- public void Url_SetAndGet_Works()
- {
- var config = new HubConfig { Url = "https://hub.example.com/update" };
- Assert.Equal("https://hub.example.com/update", config.Url);
- }
-
- [Fact]
- public void ReconnectDelay_SetToZero_Works()
- {
- var config = new HubConfig { ReconnectDelay = TimeSpan.Zero };
- Assert.Equal(TimeSpan.Zero, config.ReconnectDelay);
- }
-
- [Fact]
- public void ReconnectDelay_SetToMaxValue_Works()
- {
- var config = new HubConfig { ReconnectDelay = TimeSpan.MaxValue };
- Assert.Equal(TimeSpan.MaxValue, config.ReconnectDelay);
- }
-
- [Theory]
- [InlineData(0)]
- [InlineData(1)]
- [InlineData(100)]
- [InlineData(-1)]
- public void MaxReconnectAttempts_SetVarious_Works(int attempts)
- {
- var config = new HubConfig { MaxReconnectAttempts = attempts };
- Assert.Equal(attempts, config.MaxReconnectAttempts);
- }
-
- [Fact]
- public void MaxReconnectAttempts_IntMinValue_Works()
- {
- var config = new HubConfig { MaxReconnectAttempts = int.MinValue };
- Assert.Equal(int.MinValue, config.MaxReconnectAttempts);
- }
-}
diff --git a/tests/CoreTest/Configuration/ProcessInfoTests.cs b/tests/CoreTest/Configuration/ProcessInfoTests.cs
index a10b3bd2..01929a90 100644
--- a/tests/CoreTest/Configuration/ProcessInfoTests.cs
+++ b/tests/CoreTest/Configuration/ProcessInfoTests.cs
@@ -14,7 +14,7 @@ public void Ctor_AppNameNull_ThrowsArgumentNullException()
var ex = Assert.Throws(() =>
new ProcessInfo(null, ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null));
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null));
Assert.Contains("appName", ex.Message);
}
@@ -24,7 +24,7 @@ public void Ctor_InstallPathDoesNotExist_ThrowsArgumentException()
var ex = Assert.Throws(() =>
new ProcessInfo("app", "C:\\nonexistent_path_xyz", "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null));
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null));
Assert.Contains("path does not exist", ex.Message);
}
@@ -34,7 +34,7 @@ public void Ctor_CurrentVersionNull_ThrowsArgumentNullException()
var ex = Assert.Throws(() =>
new ProcessInfo("app", ExistingDir, null, "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null));
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null));
Assert.Contains("currentVersion", ex.Message);
}
@@ -44,7 +44,7 @@ public void Ctor_LastVersionNull_ThrowsArgumentNullException()
var ex = Assert.Throws(() =>
new ProcessInfo("app", ExistingDir, "1.0", null, null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null));
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null));
Assert.Contains("lastVersion", ex.Message);
}
@@ -54,7 +54,7 @@ public void Ctor_DownloadTimeOutNegative_ThrowsArgumentException()
var ex = Assert.Throws(() =>
new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", -1, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null));
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null));
Assert.Contains("greater than 0", ex.Message);
}
@@ -63,7 +63,7 @@ public void Ctor_DownloadTimeOutZero_Allowed()
{
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 0, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null);
Assert.Equal(0, info.DownloadTimeOut);
}
@@ -73,7 +73,7 @@ public void Ctor_AppSecretKeyNull_ThrowsArgumentNullException()
var ex = Assert.Throws(() =>
new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, null,
- SingleVersion, "url", "backup", null, null, null, null, null, null, null));
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null));
Assert.Contains("appSecretKey", ex.Message);
}
@@ -86,7 +86,7 @@ public void Ctor_UpdateVersionsNullOrEmpty_ThrowsArgumentException(bool nullList
var ex = Assert.Throws(() =>
new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- versions, "url", "backup", null, null, null, null, null, null, null));
+ versions, "url", "backup", null, null, null, null, null, null, null, null));
Assert.Contains("Collection", ex.Message);
}
@@ -96,7 +96,7 @@ public void Ctor_ReportUrlNull_ThrowsArgumentNullException()
var ex = Assert.Throws(() =>
new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, null, "backup", null, null, null, null, null, null, null));
+ SingleVersion, null, "backup", null, null, null, null, null, null, null, null));
Assert.Contains("reportUrl", ex.Message);
}
@@ -106,7 +106,7 @@ public void Ctor_BackupDirectoryNull_ThrowsArgumentNullException()
var ex = Assert.Throws(() =>
new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", null, null, null, null, null, null, null, null));
+ SingleVersion, "url", null, null, null, null, null, null, null, null, null));
Assert.Contains("backupDirectory", ex.Message);
}
@@ -117,7 +117,7 @@ public void Ctor_AllParametersValid_AllPropertiesSet()
"MyApp", ExistingDir, "1.0.0", "2.0.0", "https://log.example.com",
Encoding.UTF8, ".zip", 60, "secret-key",
SingleVersion, "https://report.example.com", "C:\\backup",
- "BowlProcess", "https", "token-abc", "C:\\drivers",
+ "BowlProcess", "https", "token-abc", "C:\\drivers", "",
new List { ".tmp" }, new List { "skip.dll" }, new List { "logs" });
Assert.Equal("MyApp", info.AppName);
@@ -147,7 +147,7 @@ public void Ctor_EncodingUTF8_CompressEncodingWebNameIsUtf8()
// Arrange & Act
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null);
// Assert
Assert.Equal("utf-8", info.CompressEncoding);
@@ -159,7 +159,7 @@ public void Ctor_EncodingASCII_CompressEncodingWebNameIsAscii()
// Arrange & Act
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.ASCII, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null);
// Assert
Assert.Equal("us-ascii", info.CompressEncoding);
@@ -171,7 +171,7 @@ public void Ctor_EncodingUnicode_CompressEncodingWebNameIsUtf16()
// Arrange & Act
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.Unicode, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null);
// Assert
Assert.Equal("utf-16", info.CompressEncoding);
@@ -184,7 +184,7 @@ public void Ctor_NullableOptionalParams_AllowedAsNull()
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
SingleVersion, "url", "backup",
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
// Assert
Assert.Null(info.Bowl);
@@ -224,7 +224,7 @@ public void Ctor_MultipleVersions_AllStored()
// Act
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- versions, "url", "backup", null, null, null, null, null, null, null);
+ versions, "url", "backup", null, null, null, null, null, null, null, null);
// Assert
Assert.Equal(3, info.UpdateVersions.Count);
@@ -239,7 +239,7 @@ public void Ctor_UpdateLogUrlNull_Allowed()
// Arrange & Act
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null, null, null, null);
+ SingleVersion, "url", "backup", null, null, null, null, null, null, null, null);
// Assert — UpdateLogUrl is explicitly allowed to be null
Assert.Null(info.UpdateLogUrl);
@@ -256,7 +256,7 @@ public void Ctor_AllBlacklistParamsPopulated_PreservedInOrder()
// Act
var info = new ProcessInfo("app", ExistingDir, "1.0", "2.0", null,
Encoding.UTF8, "ZIP", 30, "key",
- SingleVersion, "url", "backup", null, null, null, null,
+ SingleVersion, "url", "backup", null, null, null, null, null,
formats, files, dirs);
// Assert
diff --git a/tests/CoreTest/Configuration/UpdateOptionsStaticTests.cs b/tests/CoreTest/Configuration/UpdateOptionsStaticTests.cs
index c3fd3105..b4dcc9fd 100644
--- a/tests/CoreTest/Configuration/UpdateOptionsStaticTests.cs
+++ b/tests/CoreTest/Configuration/UpdateOptionsStaticTests.cs
@@ -31,7 +31,7 @@ public void Encoding_HasCorrectDefault()
[Fact]
public void Format_HasCorrectDefault()
{
- Assert.Equal("ZIP", UpdateOptions.Format.DefaultValue);
+ Assert.Equal(Format.Zip, UpdateOptions.Format.DefaultValue);
}
[Fact]
@@ -58,12 +58,6 @@ public void Silent_HasCorrectDefault()
Assert.False(UpdateOptions.Silent.DefaultValue);
}
- [Fact]
- public void SilentAutoInstall_HasCorrectDefault()
- {
- Assert.False(UpdateOptions.SilentAutoInstall.DefaultValue);
- }
-
[Fact]
public void SilentPollIntervalMinutes_HasCorrectDefault()
{
@@ -122,14 +116,4 @@ public void AllOptions_RepeatedAccess_ReturnsSameInstance()
}
#endregion
-
- #region Hub option
-
- [Fact]
- public void Hub_NotSet_HasNullDefault()
- {
- Assert.Null(UpdateOptions.Hub.DefaultValue);
- }
-
- #endregion
}
diff --git a/tests/CoreTest/Download/DownloadOrchestratorOptionsTests.cs b/tests/CoreTest/Download/DownloadOrchestratorOptionsTests.cs
index 59a1f962..641c232c 100644
--- a/tests/CoreTest/Download/DownloadOrchestratorOptionsTests.cs
+++ b/tests/CoreTest/Download/DownloadOrchestratorOptionsTests.cs
@@ -20,7 +20,7 @@ public void Defaults_AreAsSpecified()
{
var opts = new DownloadOrchestratorOptions();
- Assert.Equal(3, opts.MaxConcurrency);
+ Assert.Equal(2, opts.MaxConcurrency);
Assert.True(opts.EnableResume);
Assert.Equal(3, opts.RetryCount);
Assert.Equal(TimeSpan.FromSeconds(1), opts.RetryInterval);
@@ -105,7 +105,7 @@ public void From_DefaultsWhenConfigIsMinimal()
var opts = DownloadOrchestratorOptions.From(config);
- Assert.Equal(3, opts.MaxConcurrency);
+ Assert.Equal(2, opts.MaxConcurrency);
Assert.True(opts.EnableResume);
Assert.Equal(3, opts.RetryCount);
}
diff --git a/tests/CoreTest/Download/DownloadPlanBuilderTests.cs b/tests/CoreTest/Download/DownloadPlanBuilderTests.cs
index ac4e3f4e..39bde1ff 100644
--- a/tests/CoreTest/Download/DownloadPlanBuilderTests.cs
+++ b/tests/CoreTest/Download/DownloadPlanBuilderTests.cs
@@ -81,7 +81,7 @@ public void Build_NoAssetIsForcibly_IsForciblyFalse()
}
[Fact]
- public void Build_CrossVersionMatch_ReturnsSingleAssetPlan()
+ public void Build_CrossVersionIncluded_ReturnsAllMatchingAssets()
{
var assets = new[]
{
@@ -90,8 +90,10 @@ public void Build_CrossVersionMatch_ReturnsSingleAssetPlan()
};
var result = DownloadPlanBuilder.Build(assets, "1.0.0");
Assert.True(result.HasAssets);
- Assert.Single(result.Assets);
- Assert.Equal("5.0.0", result.Assets[0].Version);
+ Assert.Equal(3, result.Assets.Count);
+ Assert.Equal("2.0.0", result.Assets[0].Version);
+ Assert.Equal("3.0.0", result.Assets[1].Version);
+ Assert.Equal("5.0.0", result.Assets[2].Version);
}
[Fact]
@@ -142,18 +144,4 @@ public void Build_MixedFrozenAndActive_FiltersFrozen()
Assert.Single(result.Assets);
Assert.Equal("2.0.0", result.Assets[0].Version);
}
-
- [Fact]
- public void MapToAsset_NullFields_HasSaneDefaults()
- {
- var packet = new GeneralUpdate.Core.Download.Abstractions.PacketDTO
- {
- Name = null, Url = null, Version = null, Hash = null
- };
- var asset = DownloadPlanBuilder.MapToAsset(packet);
- Assert.Equal("unknown", asset.Name);
- Assert.Equal("", asset.Url);
- Assert.Equal("0.0.0", asset.Version);
- Assert.Equal(0, asset.Size);
- }
}
diff --git a/tests/CoreTest/Download/PacketDTOTests.cs b/tests/CoreTest/Download/PacketDTOTests.cs
deleted file mode 100644
index 7b1f6781..00000000
--- a/tests/CoreTest/Download/PacketDTOTests.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-namespace CoreTest.Download;
-
-using GeneralUpdate.Core.Download.Abstractions;
-
-///
-/// AAAT unit tests for and related DTO records.
-/// Covers: default values, full assignment, nullable properties, VersionRequest, VersionResponse.
-///
-public class PacketDTOTests
-{
- #region PacketDTO
-
- [Fact]
- public void PacketDTO_Default_AllNullablePropsAreNull()
- {
- var dto = new PacketDTO();
-
- Assert.Null(dto.Name);
- Assert.Null(dto.Hash);
- Assert.Null(dto.ReleaseDate);
- Assert.Null(dto.Url);
- Assert.Null(dto.Version);
- Assert.Null(dto.AppType);
- Assert.Null(dto.Platform);
- Assert.Null(dto.ProductId);
- Assert.Null(dto.IsForcibly);
- Assert.Null(dto.IsFreeze);
- Assert.Null(dto.Format);
- Assert.Null(dto.Size);
- Assert.Null(dto.FromVersion);
- Assert.Null(dto.IsCrossVersion);
- Assert.Null(dto.MinClientVersion);
- Assert.Null(dto.SourceArchiveHash);
- Assert.Null(dto.TargetArchiveHash);
- }
-
- [Fact]
- public void PacketDTO_FullAssignment_AllPropsSet()
- {
- var dto = new PacketDTO
- {
- Name = "UpdatePack",
- Hash = "hash123",
- ReleaseDate = new DateTime(2025, 3, 15),
- Url = "https://cdn.example.com/pack.zip",
- Version = "2.0.0",
- AppType = 1,
- Platform = 0,
- ProductId = "prod-1",
- IsForcibly = true,
- IsFreeze = false,
- Format = ".zip",
- Size = 2048,
- FromVersion = "1.0.0",
- IsCrossVersion = true,
- MinClientVersion = "1.5.0",
- SourceArchiveHash = "srcHash",
- TargetArchiveHash = "tgtHash"
- };
-
- Assert.Equal("UpdatePack", dto.Name);
- Assert.Equal("hash123", dto.Hash);
- Assert.Equal(new DateTime(2025, 3, 15), dto.ReleaseDate);
- Assert.Equal(".zip", dto.Format);
- Assert.Equal(2048, dto.Size);
- Assert.Equal("1.0.0", dto.FromVersion);
- Assert.True(dto.IsCrossVersion);
- Assert.Equal("1.5.0", dto.MinClientVersion);
- Assert.Equal("srcHash", dto.SourceArchiveHash);
- Assert.Equal("tgtHash", dto.TargetArchiveHash);
- }
-
- [Fact]
- public void PacketDTO_IsForcibly_NullableTriState()
- {
- var dto = new PacketDTO();
- Assert.Null(dto.IsForcibly);
-
- dto.IsForcibly = true;
- Assert.True(dto.IsForcibly);
-
- dto.IsForcibly = null;
- Assert.Null(dto.IsForcibly);
- }
-
- [Fact]
- public void PacketDTO_IsFreeze_NullableTriState()
- {
- var dto = new PacketDTO();
- Assert.Null(dto.IsFreeze);
-
- dto.IsFreeze = false;
- Assert.False(dto.IsFreeze);
-
- dto.IsFreeze = null;
- Assert.Null(dto.IsFreeze);
- }
-
- #endregion
-
- #region VersionRequest
-
- [Fact]
- public void VersionRequest_AllFieldsAssigned()
- {
- var req = new VersionRequest("MyApp", "1.0.0", "2.0.0", 1, "prod-001");
-
- Assert.Equal("MyApp", req.AppName);
- Assert.Equal("1.0.0", req.ClientVersion);
- Assert.Equal("2.0.0", req.UpgradeClientVersion);
- Assert.Equal(1, req.Platform);
- Assert.Equal("prod-001", req.ProductId);
- }
-
- [Fact]
- public void VersionRequest_NullableFields_CanBeNull()
- {
- var req = new VersionRequest("App", "1.0", null, null, null);
-
- Assert.Null(req.UpgradeClientVersion);
- Assert.Null(req.Platform);
- Assert.Null(req.ProductId);
- }
-
- #endregion
-
- #region VersionResponse
-
- [Fact]
- public void VersionResponse_NoUpdate_EmptyPackets()
- {
- var resp = new VersionResponse(false, null);
-
- Assert.False(resp.HasUpdate);
- Assert.Null(resp.Packets);
- }
-
- [Fact]
- public void VersionResponse_HasUpdate_WithPackets()
- {
- var packets = new List { new() { Name = "p1" }, new() { Name = "p2" } };
- var resp = new VersionResponse(true, packets);
-
- Assert.True(resp.HasUpdate);
- Assert.Equal(2, resp.Packets!.Count);
- }
-
- [Fact]
- public void VersionResponse_HasUpdateTrue_ButNullPackets_Works()
- {
- var resp = new VersionResponse(true, null);
-
- Assert.True(resp.HasUpdate);
- Assert.Null(resp.Packets);
- }
-
- #endregion
-}
diff --git a/tests/CoreTest/FileSystem/BlackListDefaultsTests.cs b/tests/CoreTest/FileSystem/BlackListDefaultsTests.cs
index 033e866e..01b4d712 100644
--- a/tests/CoreTest/FileSystem/BlackListDefaultsTests.cs
+++ b/tests/CoreTest/FileSystem/BlackListDefaultsTests.cs
@@ -38,7 +38,7 @@ public void DefaultBlackFormats_ContainsPatchPdbRarTarJsonZip()
Assert.Contains(".rar", formats);
Assert.Contains(".tar", formats);
Assert.Contains(".json", formats);
- Assert.Contains(Format.ZIP, formats);
+ Assert.Contains(Format.Zip.ToExtension(), formats);
}
[Fact]
diff --git a/tests/CoreTest/Hooks/HooksIntegrationTests.cs b/tests/CoreTest/Hooks/HooksIntegrationTests.cs
index 9de97b70..d587a2fa 100644
--- a/tests/CoreTest/Hooks/HooksIntegrationTests.cs
+++ b/tests/CoreTest/Hooks/HooksIntegrationTests.cs
@@ -66,7 +66,7 @@ public void UpdateContext_PropertiesSet()
{
var ctx = new UpdateContext("MyApp", "/opt/myapp", "1.0.0", "2.0.0", AppType.Client);
- Assert.Equal("MyApp", ctx.AppName);
+ Assert.Equal("MyApp", ctx.UpdateAppName);
Assert.Equal("/opt/myapp", ctx.InstallPath);
Assert.Equal("1.0.0", ctx.CurrentVersion);
Assert.Equal("2.0.0", ctx.TargetVersion);
diff --git a/tests/CoreTest/Integration/OssIntegrationTests.cs b/tests/CoreTest/Integration/OssIntegrationTests.cs
index 92e2560b..0962b2e8 100644
--- a/tests/CoreTest/Integration/OssIntegrationTests.cs
+++ b/tests/CoreTest/Integration/OssIntegrationTests.cs
@@ -78,7 +78,7 @@ public async Task OSSUpdateStrategy_WithoutConfig_ReturnsWithoutError()
var strategy = new OSSUpdateStrategy();
var config = new GlobalConfigInfo
{
- AppName = "TestOSS",
+ UpdateAppName = "TestOSS",
ClientVersion = "1.0.0",
InstallPath = "/test/oss"
};
diff --git a/tests/CoreTest/Ipc/IpcEncryptionTests.cs b/tests/CoreTest/Ipc/IpcEncryptionTests.cs
index 1b7f51ff..edf4f446 100644
--- a/tests/CoreTest/Ipc/IpcEncryptionTests.cs
+++ b/tests/CoreTest/Ipc/IpcEncryptionTests.cs
@@ -80,7 +80,7 @@ public void SendAndReceive_RoundTrip_ProcessInfoPreserved()
null, System.Text.Encoding.UTF8, ".zip", 30, "secret",
new List { new() { Version = "2.0.0" } },
"https://report.example.com", "C:\\backup",
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
provider.Send(info);
var received = provider.Receive();
diff --git a/tests/CoreTest/Ipc/ProcessInfoProviderTests.cs b/tests/CoreTest/Ipc/ProcessInfoProviderTests.cs
index 09a743e6..9732baea 100644
--- a/tests/CoreTest/Ipc/ProcessInfoProviderTests.cs
+++ b/tests/CoreTest/Ipc/ProcessInfoProviderTests.cs
@@ -35,6 +35,7 @@ public async Task EncryptedFileProvider_SendReceive_RoundTrips()
scheme: "",
token: "",
driverDirectory: "",
+ tempPath: "",
blackFileFormats: new List { ".pdb" },
blackFiles: new List { "test.dll" },
skipDirectories: new List { "logs" }
diff --git a/tests/CoreTest/Pipeline/CompressMiddlewareTests.cs b/tests/CoreTest/Pipeline/CompressMiddlewareTests.cs
index 1c728edf..3a16217a 100644
--- a/tests/CoreTest/Pipeline/CompressMiddlewareTests.cs
+++ b/tests/CoreTest/Pipeline/CompressMiddlewareTests.cs
@@ -14,16 +14,15 @@ public class CompressMiddlewareTests
{
///
/// Tests that InvokeAsync requires necessary context values.
+ /// Missing required context keys cause the pipeline to fail.
///
[Fact]
public async Task InvokeAsync_WithMissingContextValues_ThrowsException()
{
- // Arrange
var middleware = new CompressMiddleware();
var context = new PipelineContext();
-
- // Act & Assert
- await Assert.ThrowsAsync(() => middleware.InvokeAsync(context));
+
+ await Assert.ThrowsAsync(() => middleware.InvokeAsync(context));
}
///
@@ -34,7 +33,7 @@ public void Context_CanStoreAndRetrieveValues()
{
// Arrange
var context = new PipelineContext();
- var format = Format.ZIP;
+ var format = Format.Zip;
var sourcePath = "/test/source.zip";
var patchPath = "/test/patch";
var encoding = Encoding.UTF8;
@@ -50,7 +49,7 @@ public void Context_CanStoreAndRetrieveValues()
context.Add("PatchEnabled", patchEnabled);
// Assert
- Assert.Equal(format, context.Get("Format"));
+ Assert.Equal(format, context.Get("Format"));
Assert.Equal(sourcePath, context.Get("ZipFilePath"));
Assert.Equal(patchPath, context.Get("PatchPath"));
Assert.Equal(encoding, context.Get("Encoding"));
diff --git a/tests/CoreTest/Shared/ConfiginfoBuilderTests.cs b/tests/CoreTest/Shared/ConfiginfoBuilderTests.cs
index bc29ea32..b16a6551 100644
--- a/tests/CoreTest/Shared/ConfiginfoBuilderTests.cs
+++ b/tests/CoreTest/Shared/ConfiginfoBuilderTests.cs
@@ -28,7 +28,7 @@ private void CreateTestConfigFile()
UpdateUrl = TestUpdateUrl,
Token = TestToken,
Scheme = TestScheme,
- AppName = "Update.exe",
+ UpdateAppName = "Update.exe",
MainAppName = "TestApp.exe",
ClientVersion = "1.0.0",
AppSecretKey = "test-secret-key",
@@ -103,7 +103,7 @@ public void Create_ProducesConsistentResults()
Assert.Equal(config1.UpdateUrl, config2.UpdateUrl);
Assert.Equal(config1.Token, config2.Token);
Assert.Equal(config1.Scheme, config2.Scheme);
- Assert.Equal(config1.AppName, config2.AppName);
+ Assert.Equal(config1.UpdateAppName, config2.UpdateAppName);
}
finally
{
@@ -204,7 +204,7 @@ public void Build_WithMinimalParameters_ReturnsValidConfiginfo()
Assert.Equal(TestUpdateUrl, config.UpdateUrl);
Assert.Equal(TestToken, config.Token);
Assert.Equal(TestScheme, config.Scheme);
- Assert.NotNull(config.AppName);
+ Assert.NotNull(config.UpdateAppName);
Assert.NotNull(config.MainAppName);
Assert.NotNull(config.ClientVersion);
Assert.NotNull(config.InstallPath);
@@ -236,8 +236,8 @@ public void Build_GeneratesPlatformSpecificDefaults()
// InstallPath should be the current application's base directory
Assert.Equal(AppDomain.CurrentDomain.BaseDirectory, config.InstallPath);
- // According to requirements, AppName default is "Update.exe" regardless of platform
- Assert.Equal("Update.exe", config.AppName);
+ // According to requirements, UpdateAppName default is "Update.exe" regardless of platform
+ Assert.Equal("Update.exe", config.UpdateAppName);
}
finally
{
@@ -277,50 +277,50 @@ public void Build_InitializesCollectionProperties()
#region Setter Method Tests
///
- /// Tests that SetAppName correctly sets the application name.
+ /// Tests that SetUpgradeAppName correctly sets the application name.
///
[Fact]
- public void SetAppName_WithValidValue_SetsAppName()
+ public void SetUpgradeAppName_WithValidValue_SetsUpgradeAppName()
{
// Arrange
var builder = CreateBuilderWithRequiredFields();
- var customAppName = "CustomApp.exe";
+ var customUpgradeAppName = "CustomApp.exe";
// Act
- var config = builder.SetAppName(customAppName).Build();
+ var config = builder.SetUpgradeAppName(customUpgradeAppName).Build();
// Assert
- Assert.Equal(customAppName, config.AppName);
+ Assert.Equal(customUpgradeAppName, config.UpdateAppName);
}
///
- /// Tests that SetAppName returns the builder for method chaining.
+ /// Tests that SetUpgradeAppName returns the builder for method chaining.
///
[Fact]
- public void SetAppName_ReturnsBuilder_ForMethodChaining()
+ public void SetUpgradeAppName_ReturnsBuilder_ForMethodChaining()
{
// Arrange
var builder = CreateBuilderWithRequiredFields();
// Act
- var result = builder.SetAppName("Test.exe");
+ var result = builder.SetUpgradeAppName("Test.exe");
// Assert
Assert.Same(builder, result);
}
///
- /// Tests that SetAppName throws ArgumentException when value is null.
+ /// Tests that SetUpgradeAppName throws ArgumentException when value is null.
///
[Fact]
- public void SetAppName_WithNullValue_ThrowsArgumentException()
+ public void SetUpgradeAppName_WithNullValue_ThrowsArgumentException()
{
// Arrange
var builder = CreateBuilderWithRequiredFields();
// Act & Assert
- var exception = Assert.Throws(() => builder.SetAppName(null));
- Assert.Contains("AppName", exception.Message);
+ var exception = Assert.Throws(() => builder.SetUpgradeAppName(null));
+ Assert.Contains("UpdateAppName", exception.Message);
}
///
@@ -575,7 +575,7 @@ public void BuilderPattern_SupportsMethodChaining()
// Arrange & Act
CreateTestConfigFile();
var config = ConfiginfoBuilder.Create()
- .SetAppName("CustomApp.exe")
+ .SetUpgradeAppName("CustomApp.exe")
.SetMainAppName("MainCustomApp.exe")
.SetClientVersion("2.0.0")
.SetInstallPath("/custom/path")
@@ -583,7 +583,7 @@ public void BuilderPattern_SupportsMethodChaining()
.Build();
// Assert
- Assert.Equal("CustomApp.exe", config.AppName);
+ Assert.Equal("CustomApp.exe", config.UpdateAppName);
Assert.Equal("MainCustomApp.exe", config.MainAppName);
Assert.Equal("2.0.0", config.ClientVersion);
Assert.Equal("/custom/path", config.InstallPath);
@@ -618,8 +618,8 @@ public void Build_OnWindows_GeneratesWindowsDefaults()
var config = builder.Build();
// Assert
- // According to requirements, AppName default is "Update.exe" regardless of platform
- Assert.Equal("Update.exe", config.AppName);
+ // According to requirements, UpdateAppName default is "Update.exe" regardless of platform
+ Assert.Equal("Update.exe", config.UpdateAppName);
// Should use the current application's base directory
Assert.Equal(AppDomain.CurrentDomain.BaseDirectory, config.InstallPath);
}
@@ -643,8 +643,8 @@ public void Build_OnLinux_GeneratesLinuxDefaults()
var config = builder.Build();
// Assert
- // According to requirements, AppName default is "Update.exe" regardless of platform
- Assert.Equal("Update.exe", config.AppName);
+ // According to requirements, UpdateAppName default is "Update.exe" regardless of platform
+ Assert.Equal("Update.exe", config.UpdateAppName);
// Should use the current application's base directory
Assert.Equal(AppDomain.CurrentDomain.BaseDirectory, config.InstallPath);
}
@@ -668,8 +668,8 @@ public void Build_OnMacOS_GeneratesMacOSDefaults()
var config = builder.Build();
// Assert
- // According to requirements, AppName default is "Update.exe" regardless of platform
- Assert.Equal("Update.exe", config.AppName);
+ // According to requirements, UpdateAppName default is "Update.exe" regardless of platform
+ Assert.Equal("Update.exe", config.UpdateAppName);
// Should use the current application's base directory
Assert.Equal(AppDomain.CurrentDomain.BaseDirectory, config.InstallPath);
}
@@ -700,7 +700,7 @@ public void Build_ReturnsConfiginfoThatPassesValidation()
/// and gracefully falls back to defaults if not found.
///
[Fact]
- public void Build_AttemptsToExtractAppNameFromProject()
+ public void Build_AttemptsToExtractUpgradeAppNameFromProject()
{
// Arrange
var builder = CreateBuilderWithRequiredFields();
@@ -708,16 +708,16 @@ public void Build_AttemptsToExtractAppNameFromProject()
// Act
var config = builder.Build();
- // Assert - AppName should be set (either from project or fallback)
- Assert.NotNull(config.AppName);
- Assert.NotEmpty(config.AppName);
+ // Assert - UpdateAppName should be set (either from project or fallback)
+ Assert.NotNull(config.UpdateAppName);
+ Assert.NotEmpty(config.UpdateAppName);
Assert.NotNull(config.MainAppName);
Assert.NotEmpty(config.MainAppName);
// On Windows, should have .exe extension
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- Assert.EndsWith(".exe", config.AppName);
+ Assert.EndsWith(".exe", config.UpdateAppName);
}
}
@@ -758,7 +758,7 @@ public void CompleteScenario_BuildsValidConfiginfo()
UpdateUrl = "https://api.example.com/updates",
Token = "Bearer abc123xyz",
Scheme = "https",
- AppName = "MyApplication.exe",
+ UpgradeAppName = "MyApplication.exe",
MainAppName = "MyApplication.exe",
ClientVersion = "1.5.2",
UpgradeClientVersion = "1.0.0",
@@ -780,7 +780,7 @@ public void CompleteScenario_BuildsValidConfiginfo()
Assert.Equal("https://api.example.com/updates", config.UpdateUrl);
Assert.Equal("Bearer abc123xyz", config.Token);
Assert.Equal("https", config.Scheme);
- Assert.Equal("MyApplication.exe", config.AppName);
+ Assert.Equal("MyApplication.exe", config.UpdateAppName);
Assert.Equal("1.5.2", config.ClientVersion);
Assert.Equal("/opt/myapp", config.InstallPath);
@@ -810,7 +810,7 @@ public void Create_WithConfigFile_LoadsFromFile()
UpdateUrl = "https://config-file.example.com/updates",
Token = "config-file-token",
Scheme = "https",
- AppName = "ConfigFileApp.exe",
+ UpgradeAppName = "ConfigFileApp.exe",
MainAppName = "ConfigFileMain.exe",
ClientVersion = "9.9.9",
AppSecretKey = "config-file-secret",
@@ -829,7 +829,7 @@ public void Create_WithConfigFile_LoadsFromFile()
Assert.Equal("https://config-file.example.com/updates", config.UpdateUrl);
Assert.Equal("config-file-token", config.Token);
Assert.Equal("https", config.Scheme);
- Assert.Equal("ConfigFileApp.exe", config.AppName);
+ Assert.Equal("ConfigFileApp.exe", config.UpdateAppName);
Assert.Equal("ConfigFileMain.exe", config.MainAppName);
Assert.Equal("9.9.9", config.ClientVersion);
Assert.Equal("/config/file/path", config.InstallPath);
@@ -866,7 +866,7 @@ public void Create_WithoutConfigFile_UsesParameters()
Assert.Equal(TestUpdateUrl, config.UpdateUrl);
Assert.Equal(TestToken, config.Token);
Assert.Equal(TestScheme, config.Scheme);
- Assert.Equal("Update.exe", config.AppName); // Default value
+ Assert.Equal("Update.exe", config.UpdateAppName); // Default value
}
finally
{
diff --git a/tests/CoreTest/Silent/SilentPollOrchestratorTests.cs b/tests/CoreTest/Silent/SilentPollOrchestratorTests.cs
index 4ccd2ca8..3af2fefa 100644
--- a/tests/CoreTest/Silent/SilentPollOrchestratorTests.cs
+++ b/tests/CoreTest/Silent/SilentPollOrchestratorTests.cs
@@ -12,7 +12,7 @@ private GlobalConfigInfo CreateValidConfig()
UpdateUrl = "https://api.example.com/update",
ClientVersion = "1.0.0",
AppSecretKey = "secret",
- AppName = "Update.exe",
+ UpdateAppName = "Update.exe",
MainAppName = "MainApp",
InstallPath = Path.GetTempPath()
};
@@ -97,24 +97,10 @@ public void SilentOptions_DefaultPollInterval_IsOneHour()
Assert.Equal(TimeSpan.FromHours(1), options.PollInterval);
}
- [Fact]
- public void SilentOptions_AutoInstall_DefaultFalse()
- {
- var options = new SilentOptions();
- Assert.False(options.AutoInstall);
- }
-
[Fact]
public void SilentOptions_CustomPollInterval_Stored()
{
var options = new SilentOptions { PollInterval = TimeSpan.FromMinutes(30) };
Assert.Equal(TimeSpan.FromMinutes(30), options.PollInterval);
}
-
- [Fact]
- public void SilentOptions_CustomAutoInstall_Stored()
- {
- var options = new SilentOptions { AutoInstall = true };
- Assert.True(options.AutoInstall);
- }
}
diff --git a/tests/CoreTest/Strategy/StrategyCreationTests.cs b/tests/CoreTest/Strategy/StrategyCreationTests.cs
index 3c0a4b46..74ba0b08 100644
--- a/tests/CoreTest/Strategy/StrategyCreationTests.cs
+++ b/tests/CoreTest/Strategy/StrategyCreationTests.cs
@@ -14,7 +14,7 @@ public void ClientUpdateStrategy_Create_ResolvesOsStrategy()
ClientVersion = "1.0.0",
InstallPath = Path.GetTempPath(),
AppSecretKey = "key",
- AppName = "app",
+ UpdateAppName = "app",
MainAppName = "main"
};
var ex = Record.Exception(() => strategy.Create(config));
@@ -59,7 +59,7 @@ public void UpgradeUpdateStrategy_Create_ResolvesOsStrategy()
ClientVersion = "1.0.0",
InstallPath = Path.GetTempPath(),
AppSecretKey = "key",
- AppName = "Upgrade",
+ UpdateAppName = "Upgrade",
MainAppName = "MainApp"
};
var ex = Record.Exception(() => strategy.Create(config));
@@ -84,7 +84,7 @@ public async Task UpgradeUpdateStrategy_ExecuteAsync_NotConfigured_Throws()
public void OSSUpdateStrategy_Create_ClientRole_Succeeds()
{
var strategy = new OSSUpdateStrategy(AppType.OSSClient);
- var config = new GlobalConfigInfo { ClientVersion = "1.0.0", AppName = "app" };
+ var config = new GlobalConfigInfo { ClientVersion = "1.0.0", UpdateAppName = "app" };
var ex = Record.Exception(() => strategy.Create(config));
Assert.Null(ex);
}
@@ -93,7 +93,7 @@ public void OSSUpdateStrategy_Create_ClientRole_Succeeds()
public void OSSUpdateStrategy_Create_UpgradeRole_Succeeds()
{
var strategy = new OSSUpdateStrategy(AppType.OSSUpgrade);
- var config = new GlobalConfigInfo { ClientVersion = "1.0.0", AppName = "app" };
+ var config = new GlobalConfigInfo { ClientVersion = "1.0.0", UpdateAppName = "app" };
var ex = Record.Exception(() => strategy.Create(config));
Assert.Null(ex);
}
@@ -109,20 +109,20 @@ public void OSSUpdateStrategy_Create_NullConfig_Throws()
public void OSSUpdateStrategy_DefaultConstructor_UsesOSSClientRole()
{
var strategy = new OSSUpdateStrategy();
- var config = new GlobalConfigInfo { ClientVersion = "1.0.0", AppName = "app" };
+ var config = new GlobalConfigInfo { ClientVersion = "1.0.0", UpdateAppName = "app" };
var ex = Record.Exception(() => strategy.Create(config));
Assert.Null(ex);
}
[Fact]
- public async Task OSSUpdateStrategy_StartApp_NullAppName_ReturnsWithoutException()
+ public async Task OSSUpdateStrategy_StartApp_NullUpgradeAppName_ReturnsWithoutException()
{
var strategy = new OSSUpdateStrategy();
strategy.Create(new GeneralUpdate.Core.Configuration.GlobalConfigInfo
{
ClientVersion = "1.0.0",
MainAppName = null,
- AppName = null
+ UpdateAppName = null
});
var ex = await Record.ExceptionAsync(() => strategy.StartAppAsync());
Assert.Null(ex);
diff --git a/tests/UpgradeTest/Program.cs b/tests/UpgradeTest/Program.cs
new file mode 100644
index 00000000..48ff4fff
--- /dev/null
+++ b/tests/UpgradeTest/Program.cs
@@ -0,0 +1,92 @@
+using GeneralUpdate.Core;
+using GeneralUpdate.Core.Configuration;
+using GeneralUpdate.Core.Download;
+using GeneralUpdate.Core.Event;
+using GeneralUpdate.Core.Hooks;
+
+try
+{
+ Console.WriteLine("=== GeneralUpdate Upgrade Test ===");
+ Console.WriteLine($"Started at {DateTime.Now}");
+ Console.WriteLine($"Running from: {AppDomain.CurrentDomain.BaseDirectory}");
+
+ await new GeneralUpdateBootstrap()
+ .Option(UpdateOptions.AppType, AppType.Upgrade)
+ .Hooks()
+ .AddListenerMultiDownloadStatistics(OnDownloadStatistics)
+ .AddListenerMultiDownloadCompleted(OnDownloadCompleted)
+ .AddListenerMultiAllDownloadCompleted(OnAllDownloadCompleted)
+ .AddListenerMultiDownloadError(OnDownloadError)
+ .AddListenerException(OnException)
+ .LaunchAsync();
+
+ Console.WriteLine("Upgrade test completed.");
+}
+catch (Exception ex)
+{
+ Console.WriteLine($"FATAL: {ex}");
+ Environment.Exit(1);
+}
+
+static void OnDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs e)
+{
+ var v = e.Version as VersionInfo;
+ Console.WriteLine($"[Apply] {v?.Version}: {e.ProgressPercentage}%");
+}
+
+static void OnDownloadCompleted(object sender, MultiDownloadCompletedEventArgs e)
+{
+ var v = e.Version as VersionInfo;
+ Console.WriteLine($"[Apply] {v?.Version}: {(e.IsCompleted ? "SUCCESS" : "FAILED")}");
+}
+
+static void OnAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs e)
+{
+ Console.WriteLine(e.IsAllDownloadCompleted
+ ? "[Apply] All patches applied."
+ : $"[Apply] Patches finished with {e.FailedVersions.Count} failure(s).");
+}
+
+static void OnDownloadError(object sender, MultiDownloadErrorEventArgs e)
+{
+ var v = e.Version as VersionInfo;
+ Console.WriteLine($"[Apply] Error @ {v?.Version}: {e.Exception.Message}");
+}
+
+static void OnException(object sender, ExceptionEventArgs e)
+{
+ Console.WriteLine($"[Error] {e.Exception}");
+}
+
+sealed class UpgradeTestHooks : IUpdateHooks
+{
+ public async Task OnBeforeUpdateAsync(UpdateContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnBeforeUpdate: {ctx.CurrentVersion} -> {ctx.TargetVersion}");
+ return await Task.FromResult(true);
+ }
+
+ public async Task OnDownloadCompletedAsync(DownloadContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnDownloadCompleted: {ctx.AssetName} v{ctx.Version} ({ctx.TotalBytes} bytes, {ctx.Duration}) {(ctx.Success ? "OK" : "FAIL")}");
+ await Task.CompletedTask;
+ }
+
+ public async Task OnAfterUpdateAsync(UpdateContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnAfterUpdate: {ctx.CurrentVersion} -> {ctx.TargetVersion}");
+ await Task.CompletedTask;
+ }
+
+ public async Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex)
+ {
+ Console.WriteLine($"[Hook] OnUpdateError: {ctx.CurrentVersion} -> {ctx.TargetVersion} | {ex.Message}");
+ await Task.CompletedTask;
+ }
+
+ public async Task OnBeforeStartAppAsync(UpdateContext ctx)
+ {
+ Console.WriteLine($"[Hook] OnBeforeStartApp: {ctx.UpdateAppName} @ {ctx.InstallPath}");
+ await Task.CompletedTask;
+ }
+}
diff --git a/tests/UpgradeTest/UpgradeTest.csproj b/tests/UpgradeTest/UpgradeTest.csproj
new file mode 100644
index 00000000..191c3d61
--- /dev/null
+++ b/tests/UpgradeTest/UpgradeTest.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+