diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index a9bdc513..02a1936a 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -27,7 +27,8 @@ namespace GeneralUpdate.Core; /// /// — validate versions, download, start upgrade process /// — receive ProcessInfo, apply updates, start main app -/// — OSS-based cloud storage update +/// — OSS client: download version config, start upgrade process +/// — OSS upgrade: download packages from cloud, start main app /// /// /// @@ -71,7 +72,8 @@ public override async Task LaunchAsync() { AppType.Client => await LaunchWithStrategy(new ClientUpdateStrategy()), AppType.Upgrade => await LaunchWithStrategy(new UpgradeUpdateStrategy()), - AppType.OSS => await LaunchOssAsync(), + AppType.OSSClient => await LaunchWithStrategy(new OSSUpdateStrategy(AppType.OSSClient)), + AppType.OSSUpgrade => await LaunchWithStrategy(new OSSUpdateStrategy(AppType.OSSUpgrade)), _ => await LaunchWithStrategy(new ClientUpdateStrategy()) }; } @@ -153,73 +155,6 @@ private async Task LaunchWithStrategy(IStrategy roleStra return this; } - /// OSS workflow: download packages from cloud storage, apply updates. - private async Task LaunchOssAsync() - { - try - { - GeneralTracer.Debug("LaunchOssAsync start."); - - var json = Environments.GetEnvironmentVariable("GlobalConfigInfoOSS"); - if (!string.IsNullOrWhiteSpace(json)) - { - var strategy = new OSSUpdateStrategy(); - strategy.Create(_configInfo); - await strategy.ExecuteAsync(); - return this; - } - - // Client-side OSS - var basePath = AppDomain.CurrentDomain.BaseDirectory; - var versionFileName = $"{_configInfo.MainAppName ?? _configInfo.AppName}_versions.json"; - var versionsFilePath = Path.Combine(basePath, versionFileName); - - DownloadOssFile(_configInfo.UpdateUrl, versionsFilePath); - if (!File.Exists(versionsFilePath)) return this; - - var versions = StorageManager.GetJson>(versionsFilePath, - VersionOSSJsonContext.Default.ListVersionOSS); - if (versions == null || versions.Count == 0) return this; - - versions = versions.OrderByDescending(x => x.PubTime).ToList(); - var newVersion = versions.First(); - - if (!IsOssUpgrade(_configInfo.ClientVersion, newVersion.Version)) - { - GeneralTracer.Info("LaunchOssAsync: no upgrade needed."); - return this; - } - - // Use user-configured AppName, fall back to default updater name - var upgradeAppName = !string.IsNullOrWhiteSpace(_configInfo.AppName) && _configInfo.AppName != "Update.exe" - ? _configInfo.AppName - : "GeneralUpdate.Upgrade.exe"; - var appPath = Path.Combine(basePath, upgradeAppName); - if (!File.Exists(appPath)) - throw new Exception($"Upgrade application not found: {upgradeAppName}"); - - var ossConfig = new GlobalConfigInfoOSS - { - AppName = _configInfo.MainAppName ?? _configInfo.AppName, - CurrentVersion = _configInfo.ClientVersion, - VersionFileName = versionFileName, - Encoding = (_configInfo.Encoding?.CodePage ?? Encoding.UTF8.CodePage).ToString(), - Url = _configInfo.UpdateUrl - }; - - var serialized = JsonSerializer.Serialize(ossConfig, - GlobalConfigInfoOSSJsonContext.Default.GlobalConfigInfoOSS); - Environments.SetEnvironmentVariable("GlobalConfigInfoOSS", serialized); - Process.Start(appPath); - await GracefulExit.CurrentProcessAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - GeneralTracer.Error("LaunchOssAsync failed.", ex); - EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); - } - return this; - } // ════════════════════════════════════════════════════════════════ // Configuration @@ -373,26 +308,6 @@ private async Task CallSmallBowlHomeAsync(string processName) } } - private static void DownloadOssFile(string url, string path) - { - if (File.Exists(path)) - { - File.SetAttributes(path, FileAttributes.Normal); - File.Delete(path); - } - using var webClient = new System.Net.WebClient(); - webClient.DownloadFile(new Uri(url), path); - } - - private static bool IsOssUpgrade(string clientVersion, string serverVersion) - { - if (string.IsNullOrWhiteSpace(clientVersion) || string.IsNullOrWhiteSpace(serverVersion)) - return false; - return Version.TryParse(clientVersion, out var cv) - && Version.TryParse(serverVersion, out var sv) - && cv < sv; - } - // ════════════════════════════════════════════════════════════════ // Strategy & Events // ════════════════════════════════════════════════════════════════ diff --git a/src/c#/GeneralUpdate.Core/Configuration/AppType.cs b/src/c#/GeneralUpdate.Core/Configuration/AppType.cs index 216c5e7c..3d15602b 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/AppType.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/AppType.cs @@ -11,6 +11,9 @@ public enum AppType /// Upgrade application — applies downloaded update packages, starts main app. Upgrade = 2, - /// OSS (Object Storage Service) update mode — downloads from cloud storage. - OSS = 3 + /// OSS client mode — checks version config, starts upgrade process. + OSSClient = 3, + + /// OSS upgrade mode — downloads packages from OSS, deploys to client. + OSSUpgrade = 4 } diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs index 61739c04..e7233392 100644 --- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs +++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs @@ -5,39 +5,40 @@ using System.Linq; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using GeneralUpdate.Core.Compress; -using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Download.Abstractions; using GeneralUpdate.Core.Download.Models; using GeneralUpdate.Core.Download.Orchestrators; -using GeneralUpdate.Core.Download.Sources; -using GeneralUpdate.Core.Configuration; namespace GeneralUpdate.Core.Strategy; /// -/// OSS (Object Storage Service) update strategy. -/// Downloads version configuration, fetches update packages from OSS, -/// decompresses them, and launches the main application. +/// OSS (Object Storage Service) update strategy — client/upgrade split via AppType. +/// +/// — downloads version config, checks for updates, +/// starts the upgrade process, and exits. +/// — reads version config, downloads packages from OSS, +/// decompresses them, starts the main app, and exits. +/// /// -/// -/// This replaces the legacy OSSStrategy and GeneralUpdateOSS classes. -/// The OSS workflow is OS-agnostic — no platform-specific pipeline is required. -/// public class OSSUpdateStrategy : IStrategy { + private readonly AppType _role; private GlobalConfigInfo? _configInfo; private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory; private const int DefaultTimeOut = 60; - /// Lifecycle hooks injected by the bootstrap. + public OSSUpdateStrategy(AppType role = AppType.OSSClient) + { + _role = role; + } + 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(); - /// Download source for OSS version listing. Override via .DownloadSource<OssDownloadSource>(). public IDownloadSource? DownloadSource { get; set; } - /// Download orchestrator. Override via .DownloadOrchestrator<T>(). public IDownloadOrchestrator? DownloadOrchestrator { get; set; } public void Create(GlobalConfigInfo parameter) @@ -50,91 +51,149 @@ public async Task ExecuteAsync() if (_configInfo == null) throw new InvalidOperationException("OSSUpdateStrategy not configured. Call Create() first."); + // Dispatch by role — no env-var detection needed. + if (_role == AppType.OSSUpgrade) + { + await ExecuteUpgradeAsync(); + return; + } + + await ExecuteClientAsync(); + } + + // ════════════════════════════════════════════════════════════════ + // Client side: check version, start upgrade process + // ════════════════════════════════════════════════════════════════ + + 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); + + if (!string.IsNullOrEmpty(_configInfo.UpdateUrl)) + { + DownloadVersionConfig(_configInfo.UpdateUrl, versionsFilePath); + } + + if (!File.Exists(versionsFilePath)) + { + GeneralTracer.Info("OSSUpdateStrategy: version config download failed, aborting."); + return; + } + + var versions = JsonSerializer.Deserialize( + File.ReadAllText(versionsFilePath), + JsonContext.VersionOSSJsonContext.Default.ListVersionOSS); + if (versions == null || versions.Count == 0) + { + GeneralTracer.Info("OSSUpdateStrategy: no versions found, aborting."); + return; + } + + versions = versions.OrderByDescending(x => x.PubTime).ToList(); + var latest = versions.First(); + + if (!IsOssUpgrade(_configInfo.ClientVersion, latest.Version)) + { + GeneralTracer.Info("OSSUpdateStrategy: no upgrade needed."); + return; + } + + // Use user-configured AppName or default upgrade exe + var upgradeAppName = !string.IsNullOrWhiteSpace(_configInfo.AppName) && _configInfo.AppName != "Update.exe" + ? _configInfo.AppName + : "GeneralUpdate.Upgrade.exe"; + var appPath = Path.Combine(_appPath, upgradeAppName); + if (!File.Exists(appPath)) + throw new FileNotFoundException($"Upgrade application not found: {upgradeAppName}"); + + Process.Start(appPath); + await GracefulExit.CurrentProcessAsync().ConfigureAwait(false); + } + + // ════════════════════════════════════════════════════════════════ + // Upgrade side: download packages, decompress, start main app + // ════════════════════════════════════════════════════════════════ + + private async Task ExecuteUpgradeAsync() + { var ctx = BuildUpdateContext(); try { - var versionFileName = $"{_configInfo.MainAppName ?? _configInfo.AppName}_versions.json"; - - GeneralTracer.Debug("OSSUpdateStrategy: 1. Reading version configuration."); + var versionFileName = $"{_configInfo!.MainAppName ?? _configInfo.AppName}_versions.json"; var jsonPath = Path.Combine(_appPath, versionFileName); + if (!File.Exists(jsonPath) && DownloadSource == null) throw new FileNotFoundException($"Version config not found: {jsonPath}"); // Hooks: allow cancellation before download if (!await SafeOnBeforeUpdateAsync(ctx).ConfigureAwait(false)) { - GeneralTracer.Info("OSSUpdateStrategy: update cancelled by OnBeforeUpdateAsync hook."); + GeneralTracer.Info("OSSUpdateStrategy (upgrade): cancelled by hook."); return; } - // Report: update started await SafeReportUpdateStartedAsync(ctx).ConfigureAwait(false); + // Build download assets from version config or injected source List assets; if (DownloadSource != null) { - GeneralTracer.Debug("OSSUpdateStrategy: 2. Using injected IDownloadSource."); - var sourceAssets = await DownloadSource.ListAsync().ConfigureAwait(false); - assets = sourceAssets.ToList(); + assets = (await DownloadSource.ListAsync().ConfigureAwait(false)).ToList(); } else { - GeneralTracer.Debug("OSSUpdateStrategy: 2. Parsing version configuration from local JSON."); - var versions = System.Text.Json.JsonSerializer.Deserialize( + var versions = JsonSerializer.Deserialize( File.ReadAllText(jsonPath), JsonContext.VersionOSSJsonContext.Default.ListVersionOSS); if (versions == null || versions.Count == 0) throw new InvalidOperationException("No versions found in OSS configuration."); - assets = versions.OrderBy(v => v.PubTime).Select(v => - { - 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"; - return new Download.Models.DownloadAsset( - Name: zipName, Url: v.Url, Size: 0, - SHA256: v.Hash, Version: v.Version ?? "0.0.0"); - }).ToList(); + assets = versions.OrderBy(v => v.PubTime) + .Where(v => new Version(v.Version ?? "0.0.0") > new Version(_configInfo.ClientVersion)) + .Select(v => + { + 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"; + return new DownloadAsset( + Name: zipName, Url: v.Url, Size: 0, + SHA256: v.Hash, Version: v.Version ?? "0.0.0"); + }).ToList(); } if (assets.Count == 0) throw new InvalidOperationException("No assets to download."); - GeneralTracer.Debug($"OSSUpdateStrategy: 3. Downloading {assets.Count} asset(s)."); - await DownloadAssetsAsync(assets); + GeneralTracer.Debug($"OSSUpdateStrategy (upgrade): downloading {assets.Count} asset(s)."); + await DownloadAssetsAsync(assets).ConfigureAwait(false); - GeneralTracer.Debug("OSSUpdateStrategy: 4. Decompressing packages."); + GeneralTracer.Debug("OSSUpdateStrategy (upgrade): decompressing."); DecompressAssets(assets); - // Hooks: download + decompress completed await SafeOnDownloadCompletedAsync(ctx).ConfigureAwait(false); await SafeOnAfterUpdateAsync(ctx).ConfigureAwait(false); - - // Report: update applied await SafeReportUpdateAppliedAsync(ctx).ConfigureAwait(false); - - // Hooks: before starting main app await SafeOnBeforeStartAppAsync(ctx).ConfigureAwait(false); - GeneralTracer.Debug("OSSUpdateStrategy: 5. Launching main application."); + GeneralTracer.Debug("OSSUpdateStrategy (upgrade): launching main app."); StartApp(); } catch (Exception ex) { await SafeOnUpdateErrorAsync(ctx, ex).ConfigureAwait(false); await SafeReportUpdateFailedAsync(ctx, ex).ConfigureAwait(false); - GeneralTracer.Error("OSSUpdateStrategy.ExecuteAsync failed.", ex); + GeneralTracer.Error("OSSUpdateStrategy.ExecuteUpgradeAsync failed.", ex); throw; } } - public void Execute() - { - ExecuteAsync().GetAwaiter().GetResult(); - } + public void Execute() => ExecuteAsync().GetAwaiter().GetResult(); public void StartApp() { @@ -146,22 +205,45 @@ public void StartApp() throw new FileNotFoundException($"Application not found: {appPath}"); Process.Start(appPath); - GeneralTracer.Debug("OSSUpdateStrategy: application started."); + GeneralTracer.Debug("OSSUpdateStrategy: main application started."); } #region Helpers + private static void DownloadVersionConfig(string url, string path) + { + if (File.Exists(path)) + { + File.SetAttributes(path, FileAttributes.Normal); + File.Delete(path); + } + using var httpClient = new HttpClient(); + var bytes = httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult(); + File.WriteAllBytes(path, bytes); + } + + private static bool IsOssUpgrade(string clientVersion, string serverVersion) + { + if (string.IsNullOrWhiteSpace(clientVersion) || string.IsNullOrWhiteSpace(serverVersion)) + return false; + return Version.TryParse(clientVersion, out var cv) + && Version.TryParse(serverVersion, out var sv) + && cv < sv; + } + private async Task DownloadAssetsAsync(List assets) { var plan = new DownloadPlan(assets, false); - if (DownloadOrchestrator != null) { await DownloadOrchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false); } else { - using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(_configInfo.DownloadTimeOut > 0 ? _configInfo.DownloadTimeOut : DefaultTimeOut) }; + using var httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(_configInfo?.DownloadTimeOut > 0 ? _configInfo!.DownloadTimeOut : DefaultTimeOut) + }; var orchestrator = new DefaultDownloadOrchestrator(httpClient); await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false); } @@ -181,10 +263,6 @@ private void DecompressAssets(List assets) } } - // ════════════════════════════════════════════════════════════════ - // Hooks & Reporter safe wrappers - // ════════════════════════════════════════════════════════════════ - private Hooks.UpdateContext BuildUpdateContext() { return new Hooks.UpdateContext( @@ -192,7 +270,7 @@ private Hooks.UpdateContext BuildUpdateContext() _configInfo?.InstallPath ?? _appPath, _configInfo?.ClientVersion ?? "0.0.0", _configInfo?.LastVersion, - AppType.OSS + AppType.OSSUpgrade ); } @@ -201,38 +279,32 @@ 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; } } - private async Task SafeOnBeforeStartAppAsync(Hooks.UpdateContext ctx) { 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}"); } } - private async Task SafeOnAfterUpdateAsync(Hooks.UpdateContext ctx) { try { await Hooks.OnAfterUpdateAsync(ctx).ConfigureAwait(false); } catch (Exception ex) { GeneralTracer.Warn($"OnAfterUpdateAsync hook failed: {ex.Message}"); } } - private async Task SafeOnDownloadCompletedAsync(Hooks.UpdateContext ctx) { try { var downloadCtx = new Hooks.DownloadContext( _configInfo?.MainAppName ?? _configInfo?.AppName ?? "unknown", - _configInfo?.LastVersion ?? "", - 0, TimeSpan.Zero, _appPath, true); + _configInfo?.LastVersion ?? "", 0, TimeSpan.Zero, _appPath, true); await Hooks.OnDownloadCompletedAsync(downloadCtx).ConfigureAwait(false); } catch (Exception ex) { GeneralTracer.Warn($"OnDownloadCompletedAsync hook failed: {ex.Message}"); } } - private async Task SafeReportUpdateStartedAsync(Hooks.UpdateContext ctx) { try @@ -244,7 +316,6 @@ await Reporter.ReportAsync(new Download.Reporting.UpdateReport( } catch (Exception ex) { GeneralTracer.Warn($"Report UpdateStarted failed: {ex.Message}"); } } - private async Task SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx) { try @@ -256,7 +327,6 @@ await Reporter.ReportAsync(new Download.Reporting.UpdateReport( } catch (Exception ex) { GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}"); } } - private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error) { try diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs index 8054aebe..fdc7c245 100644 --- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs +++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs @@ -47,7 +47,8 @@ public void Dispose() #region Core [Fact] public void AppType_Client() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.Client)); [Fact] public void AppType_Upgrade() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.Upgrade)); - [Fact] public void AppType_OSS() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.OSS)); + [Fact] public void AppType_OSSClient() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.OSSClient)); + [Fact] public void AppType_OSSUpgrade() => Assert.NotNull(B().Option(UpdateOptions.AppType, AppType.OSSUpgrade)); [Fact] public void DiffMode_Serial() => Assert.NotNull(B().Option(UpdateOptions.DiffMode, DiffMode.Serial)); [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)); diff --git a/tests/CoreTest/Configuration/ConfigurationModelsTests.cs b/tests/CoreTest/Configuration/ConfigurationModelsTests.cs index 1aef0fd1..97bb3356 100644 --- a/tests/CoreTest/Configuration/ConfigurationModelsTests.cs +++ b/tests/CoreTest/Configuration/ConfigurationModelsTests.cs @@ -258,7 +258,9 @@ public void DownloadResult_FailureWithRetries() [Fact] public void AppType_UpgradeIs2() => Assert.Equal(2, (int)AppType.Upgrade); [Fact] - public void AppType_OSSIs3() => Assert.Equal(3, (int)AppType.OSS); + public void AppType_OSSClientIs3() => Assert.Equal(3, (int)AppType.OSSClient); + [Fact] + public void AppType_OSSUpgradeIs4() => Assert.Equal(4, (int)AppType.OSSUpgrade); [Fact] public void DiffMode_SerialAndParallel_AreDefined()