From 1aa31e0cb3b312a288aad157af5d986e77b34b4d Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sun, 24 May 2026 15:55:47 +0800 Subject: [PATCH] refactor: merge GeneralClientBootstrap into GeneralUpdateBootstrap via AppType - Added AppType dispatch in LaunchAsync(): ClientApp vs UpgradeApp - Merged client-side features: Bowl process mgmt, custom options, dual version validation, CheckFail, ProcessInfo IPC creation, Silent mode, UpdatePrecheck, StartApp - Added AddListenerUpdatePrecheck(), AddCustomOption() to unified API - Marked GeneralClientBootstrap as [Obsolete] with migration guidance - Full solution builds with 0 errors, 0 warnings Closes #344 --- .../Bootstrap/GeneralClientBootstrap.cs | 3 +- .../Bootstrap/GeneralUpdateBootstrap.cs | 722 +++++++++++------- 2 files changed, 468 insertions(+), 257 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralClientBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralClientBootstrap.cs index 24aca154..efaa354b 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralClientBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralClientBootstrap.cs @@ -21,7 +21,8 @@ namespace GeneralUpdate.Core; /// /// This component is used only for client application bootstrapping classes. /// -public class GeneralClientBootstrap : AbstractBootstrap +[Obsolete("Use GeneralUpdateBootstrap with Option(UpdateOptions.AppType, AppType.ClientApp) instead. This class will be removed in v11. See migration guide at https://github.com/GeneralLibrary/GeneralUpdate/issues/344")] + public class GeneralClientBootstrap : AbstractBootstrap { /// /// All update actions of the core object for automatic upgrades will be related to the packet object. diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index fc26a55e..d8f4a7cf 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -9,348 +9,558 @@ using System.Threading.Tasks; using GeneralUpdate.Core.FileSystem; using GeneralUpdate.Core.Download; -using GeneralUpdate.Core; using GeneralUpdate.Core.Event; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.JsonContext; using GeneralUpdate.Core.Strategy; using GeneralUpdate.Core.Network; -namespace GeneralUpdate.Core +namespace GeneralUpdate.Core; + +/// +/// Unified update bootstrap — single entry point for both Client and Upgrade roles. +/// Use to select the workflow: +/// +/// — validate versions, download, start upgrade process +/// — receive ProcessInfo, apply updates, start main app +/// +/// +/// +/// Migration from GeneralClientBootstrap: +/// Replace new GeneralClientBootstrap().SetConfig(cfg).LaunchAsync() with +/// new GeneralUpdateBootstrap().Option(UpdateOptions.AppType, AppType.ClientApp).SetConfig(cfg).LaunchAsync(). +/// +public class GeneralUpdateBootstrap : AbstractBootstrap { - public class GeneralUpdateBootstrap : AbstractBootstrap - { - private GlobalConfigInfo _configInfo = new(); - private IStrategy? _strategy; - private Func? _customSkipOption; + private GlobalConfigInfo _configInfo = new(); + private IStrategy? _strategy; + private Func? _customSkipOption; + private Func? _updatePrecheck; + private readonly List> _customOptions = new(); - public GeneralUpdateBootstrap() - { - InitializeFromEnvironment(); - } + public GeneralUpdateBootstrap() + { + InitializeFromEnvironment(); + } - #region Launch + // ════════════════════════════════════════════════════════════════ + // Launch — AppType dispatch + // ════════════════════════════════════════════════════════════════ - public override async Task LaunchAsync() + public override async Task LaunchAsync() + { + int appType = GetOption(UpdateOptions.AppType); + return appType switch { - GeneralTracer.Debug("GeneralUpdateBootstrap Launch."); - StrategyFactory(); - - switch (GetOption(UpdateOption.Mode) ?? UpdateMode.Default) - { - case UpdateMode.Default: - GeneralTracer.Info("GeneralUpdateBootstrap.LaunchAsync: Default mode - applying runtime options, creating strategy, downloading and executing."); - ApplyRuntimeOptions(); - _strategy!.Create(_configInfo); - await DownloadAsync(); - await _strategy.ExecuteAsync(); - GeneralTracer.Info("GeneralUpdateBootstrap.LaunchAsync: Default mode execution completed."); - break; + AppType.ClientApp => await LaunchClientAsync(), + AppType.UpgradeApp => await LaunchUpgradeAsync(), + _ => await LaunchClientAsync() // default to Client for backward compatibility + }; + } - case UpdateMode.Scripts: - GeneralTracer.Info("GeneralUpdateBootstrap.LaunchAsync: Scripts mode - executing workflow."); - await ExecuteWorkflowAsync(); - break; + /// Client workflow: validate versions, download, start upgrade process. + private async Task LaunchClientAsync() + { + try + { + GeneralTracer.Debug("GeneralUpdateBootstrap.LaunchClientAsync start."); + CallSmallBowlHome(_configInfo.Bowl); + ExecuteCustomOptions(); + await ExecuteClientWorkflowAsync(); + } + catch (Exception ex) + { + GeneralTracer.Error("LaunchClientAsync threw an exception.", ex); + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); + } + return this; + } - default: - throw new ArgumentOutOfRangeException(); - } + /// Upgrade workflow: receive ProcessInfo, apply updates, start main app. + private async Task LaunchUpgradeAsync() + { + GeneralTracer.Debug("GeneralUpdateBootstrap.LaunchUpgradeAsync start."); + StrategyFactory(); - return this; + switch (GetOption(UpdateOption.Mode) ?? UpdateMode.Default) + { + case UpdateMode.Default: + ApplyRuntimeOptions(); + _strategy!.Create(_configInfo); + await DownloadAsync(); + await _strategy.ExecuteAsync(); + break; + case UpdateMode.Scripts: + await ExecuteUpgradeWorkflowAsync(); + break; + default: + throw new ArgumentOutOfRangeException(); } - #endregion + return this; + } + + // ════════════════════════════════════════════════════════════════ + // Configuration + // ════════════════════════════════════════════════════════════════ - #region Configuration + public GeneralUpdateBootstrap SetConfig(Configinfo configInfo) + { + _configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo); - /// - /// Configure the update bootstrap with user-provided configuration. - /// Uses ConfigurationMapper to ensure consistent field mapping and reduce maintenance burden. - /// - /// User-provided configuration containing update parameters - /// This bootstrap instance for method chaining - public GeneralUpdateBootstrap SetConfig(Configinfo configInfo) + var appType = GetOption(UpdateOptions.AppType); + if (appType != AppType.UpgradeApp) { - // Use ConfigurationMapper instead of manual field mapping - // This ensures all fields are consistently mapped and reduces maintenance burden - _configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo); - - // Set runtime-specific values that are not part of user configuration _configInfo.TempPath = StorageManager.GetTempDirectory("upgrade_temp"); _configInfo.DriveEnabled = GetOption(UpdateOption.Drive) ?? false; _configInfo.PatchEnabled = GetOption(UpdateOption.Patch) ?? true; - InitBlackList(); - return this; - } - - public GeneralUpdateBootstrap SetCustomSkipOption(Func? func) - { - _customSkipOption = func; - return this; } - #endregion + return this; + } + + public GeneralUpdateBootstrap SetCustomSkipOption(Func? func) + { + _customSkipOption = func; + return this; + } + + /// + /// Registers a callback invoked when update information is available. + /// Returns true to skip the update; false to proceed. + /// Forced-update protection still applies — the callback return value is + /// ignored if any version is marked as forcibly required. + /// + public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func func) + { + _updatePrecheck = func ?? throw new ArgumentNullException(nameof(func)); + return this; + } - #region Workflow + /// + /// Add custom operations to execute before the update workflow. + /// Recommended for environment checks to ensure dependencies are available after update. + /// + public GeneralUpdateBootstrap AddCustomOption(List> funcList) + { + Debug.Assert(funcList != null && funcList.Any()); + _customOptions.AddRange(funcList); + return this; + } + + // ════════════════════════════════════════════════════════════════ + // Client Workflow + // ════════════════════════════════════════════════════════════════ - private async Task ExecuteWorkflowAsync() + private async Task ExecuteClientWorkflowAsync() + { + try { - try + Debug.Assert(_configInfo != null); + + // Silent mode + if (GetOption(UpdateOption.EnableSilentUpdate)) { - GeneralTracer.Info($"GeneralUpdateBootstrap.ExecuteWorkflowAsync: validating version against server. UpdateUrl={_configInfo.UpdateUrl}, ClientVersion={_configInfo.ClientVersion}"); - var mainResp = await VersionService.Validate( - _configInfo.UpdateUrl, - _configInfo.ClientVersion, - AppType.ClientApp, - _configInfo.AppSecretKey, - GetPlatform(), - _configInfo.ProductId, - _configInfo.Scheme, - _configInfo.Token); - - _configInfo.IsMainUpdate = CheckUpgrade(mainResp); - GeneralTracer.Info($"GeneralUpdateBootstrap.ExecuteWorkflowAsync: version validation completed. IsMainUpdate={_configInfo.IsMainUpdate}, ResponseCode={mainResp?.Code}"); - - EventManager.Instance.Dispatch(this, new UpdateInfoEventArgs(mainResp)); - - if (CanSkip(CheckForcibly(mainResp.Body))) - { - GeneralTracer.Info("GeneralUpdateBootstrap.ExecuteWorkflowAsync: update skipped by custom skip option."); - return; - } + GeneralTracer.Info("GeneralUpdateBootstrap.ExecuteClientWorkflowAsync: silent mode, delegating to SilentUpdateMode."); + await new SilentUpdateMode( + _configInfo, + GetOption(UpdateOption.Encoding) ?? Encoding.Default, + GetOption(UpdateOption.Format) ?? Format.ZIP, + GetOption(UpdateOption.DownloadTimeOut) ?? 60, + GetOption(UpdateOption.Patch) ?? true, + GetOption(UpdateOption.BackUp) ?? true).StartAsync(); + return; + } - InitBlackList(); - ApplyRuntimeOptions(); + // Dual version validation + GeneralTracer.Info($"GeneralUpdateBootstrap.ExecuteClientWorkflowAsync: validating client={_configInfo.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}"); + var mainResp = await VersionService.Validate(_configInfo.UpdateUrl + , _configInfo.ClientVersion, AppType.ClientApp, _configInfo.AppSecretKey + , GetPlatform(), _configInfo.ProductId, _configInfo.Scheme, _configInfo.Token); - _configInfo.TempPath = StorageManager.GetTempDirectory("main_temp"); - _configInfo.BackupDirectory = Path.Combine( - _configInfo.InstallPath, - $"{StorageManager.DirectoryName}{_configInfo.ClientVersion}"); + var upgradeResp = await VersionService.Validate(_configInfo.UpdateUrl + , _configInfo.UpgradeClientVersion, AppType.UpgradeApp, _configInfo.AppSecretKey + , GetPlatform(), _configInfo.ProductId, _configInfo.Scheme, _configInfo.Token); - _configInfo.UpdateVersions = mainResp.Body! - .OrderBy(x => x.ReleaseDate) - .ToList(); + _configInfo.IsUpgradeUpdate = CheckUpgrade(upgradeResp); + _configInfo.IsMainUpdate = CheckUpgrade(mainResp); + GeneralTracer.Info($"ExecuteClientWorkflowAsync: IsMainUpdate={_configInfo.IsMainUpdate}, IsUpgradeUpdate={_configInfo.IsUpgradeUpdate}"); - GeneralTracer.Info($"GeneralUpdateBootstrap.ExecuteWorkflowAsync: {_configInfo.UpdateVersions.Count} version(s) queued for update."); + var updateInfoArgs = new UpdateInfoEventArgs(mainResp); + EventManager.Instance.Dispatch(this, updateInfoArgs); - if (GetOption(UpdateOption.BackUp) ?? true) - { - GeneralTracer.Info($"GeneralUpdateBootstrap.ExecuteWorkflowAsync: backing up from {_configInfo.InstallPath} to {_configInfo.BackupDirectory}"); - StorageManager.Backup( - _configInfo.InstallPath, - _configInfo.BackupDirectory, - BlackListManager.Instance.SkipDirectorys); - GeneralTracer.Info("GeneralUpdateBootstrap.ExecuteWorkflowAsync: backup completed."); - } + var isForcibly = CheckForcibly(mainResp.Body) || CheckForcibly(upgradeResp.Body); + if (CanSkipClient(isForcibly, updateInfoArgs)) + { + GeneralTracer.Info("ExecuteClientWorkflowAsync: update skipped by precheck callback."); + return; + } - _strategy!.Create(_configInfo); + InitBlackList(); + ApplyRuntimeOptions(); - if (_configInfo.IsMainUpdate) - { - GeneralTracer.Info("GeneralUpdateBootstrap.ExecuteWorkflowAsync: main update required, starting download and execution."); - await DownloadAsync(); - await _strategy.ExecuteAsync(); - GeneralTracer.Info("GeneralUpdateBootstrap.ExecuteWorkflowAsync: main update execution completed."); - } - else + _configInfo.TempPath = StorageManager.GetTempDirectory("main_temp"); + _configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath, + $"{StorageManager.DirectoryName}{_configInfo.ClientVersion}"); + + _configInfo.UpdateVersions = _configInfo.IsUpgradeUpdate + ? upgradeResp.Body.OrderBy(x => x.ReleaseDate).ToList() + : new List(); + + if (_configInfo.IsMainUpdate) + { + _configInfo.LastVersion = mainResp.Body.OrderBy(x => x.ReleaseDate).Last().Version; + GeneralTracer.Info($"ExecuteClientWorkflowAsync: main update, LastVersion={_configInfo.LastVersion}"); + + var failed = CheckFail(_configInfo.LastVersion); + if (failed) { - GeneralTracer.Info("GeneralUpdateBootstrap.ExecuteWorkflowAsync: no main update needed, starting application directly."); - _strategy.StartApp(); + GeneralTracer.Warn($"ExecuteClientWorkflowAsync: version {_configInfo.LastVersion} matches known-failed upgrade, aborting."); + return; } + + var processInfo = ConfigurationMapper.MapToProcessInfo( + _configInfo, mainResp.Body, + BlackListManager.Instance.BlackFormats.ToList(), + BlackListManager.Instance.BlackFiles.ToList(), + BlackListManager.Instance.SkipDirectorys.ToList()); + + _configInfo.ProcessInfo = JsonSerializer.Serialize( + processInfo, ProcessInfoJsonContext.Default.ProcessInfo); } - catch (Exception ex) + + if (GetOption(UpdateOption.BackUp) ?? true) { - GeneralTracer.Error( - "The ExecuteWorkflowAsync method in the GeneralUpdateBootstrap class throws an exception.", - ex); - EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); + GeneralTracer.Info($"ExecuteClientWorkflowAsync: backing up {_configInfo.InstallPath} -> {_configInfo.BackupDirectory}"); + StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory, + BlackListManager.Instance.SkipDirectorys); } - } - #endregion + StrategyFactory(); + GeneralTracer.Info($"ExecuteClientWorkflowAsync: IsUpgradeUpdate={_configInfo.IsUpgradeUpdate}, IsMainUpdate={_configInfo.IsMainUpdate}"); + + switch (_configInfo.IsUpgradeUpdate) + { + case true when _configInfo.IsMainUpdate: + GeneralTracer.Info("ExecuteClientWorkflowAsync: both upgrade+main — downloading and executing."); + await DownloadAsync(); + await _strategy!.ExecuteAsync(); + _strategy.StartApp(); + break; + case true when !_configInfo.IsMainUpdate: + GeneralTracer.Info("ExecuteClientWorkflowAsync: upgrade-only — downloading and executing."); + await DownloadAsync(); + await _strategy!.ExecuteAsync(); + break; + case false when _configInfo.IsMainUpdate: + GeneralTracer.Info("ExecuteClientWorkflowAsync: main-only — starting updater."); + _strategy!.StartApp(); + break; + } + } + catch (Exception ex) + { + GeneralTracer.Error("ExecuteClientWorkflowAsync threw an exception.", ex); + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); + } + } - #region Download + // ════════════════════════════════════════════════════════════════ + // Upgrade Workflow + // ════════════════════════════════════════════════════════════════ - private async Task DownloadAsync() + private async Task ExecuteUpgradeWorkflowAsync() + { + try { - var manager = new DownloadManager( - _configInfo.TempPath, - _configInfo.Format, - _configInfo.DownloadTimeOut); + GeneralTracer.Info($"GeneralUpdateBootstrap.ExecuteUpgradeWorkflowAsync: validating version. UpdateUrl={_configInfo.UpdateUrl}, ClientVersion={_configInfo.ClientVersion}"); + var mainResp = await VersionService.Validate( + _configInfo.UpdateUrl, _configInfo.ClientVersion, + AppType.ClientApp, _configInfo.AppSecretKey, + GetPlatform(), _configInfo.ProductId, + _configInfo.Scheme, _configInfo.Token); - manager.MultiAllDownloadCompleted += OnMultiAllDownloadCompleted; - manager.MultiDownloadCompleted += OnMultiDownloadCompleted; - manager.MultiDownloadError += OnMultiDownloadError; - manager.MultiDownloadStatistics += OnMultiDownloadStatistics; + _configInfo.IsMainUpdate = CheckUpgrade(mainResp); + GeneralTracer.Info($"ExecuteUpgradeWorkflowAsync: IsMainUpdate={_configInfo.IsMainUpdate}"); - foreach (var version in _configInfo.UpdateVersions) - manager.Add(new DownloadTask(manager, version)); + EventManager.Instance.Dispatch(this, new UpdateInfoEventArgs(mainResp)); - await manager.LaunchTasksAsync(); - } + if (CanSkip(CheckForcibly(mainResp.Body))) + { + GeneralTracer.Info("ExecuteUpgradeWorkflowAsync: update skipped."); + return; + } - #endregion + InitBlackList(); + ApplyRuntimeOptions(); - #region Helpers + _configInfo.TempPath = StorageManager.GetTempDirectory("main_temp"); + _configInfo.BackupDirectory = Path.Combine( + _configInfo.InstallPath, + $"{StorageManager.DirectoryName}{_configInfo.ClientVersion}"); - private void InitializeFromEnvironment() - { - var json = Environments.GetEnvironmentVariable("ProcessInfo"); - if (string.IsNullOrWhiteSpace(json)) return; + _configInfo.UpdateVersions = mainResp.Body! + .OrderBy(x => x.ReleaseDate).ToList(); - var processInfo = JsonSerializer.Deserialize( - json, - ProcessInfoJsonContext.Default.ProcessInfo); + GeneralTracer.Info($"ExecuteUpgradeWorkflowAsync: {_configInfo.UpdateVersions.Count} version(s) queued."); - if (processInfo == null) return; + if (GetOption(UpdateOption.BackUp) ?? true) + { + GeneralTracer.Info($"ExecuteUpgradeWorkflowAsync: backing up {_configInfo.InstallPath} -> {_configInfo.BackupDirectory}"); + StorageManager.Backup( + _configInfo.InstallPath, _configInfo.BackupDirectory, + BlackListManager.Instance.SkipDirectorys); + } - BlackListManager.Instance.AddBlackFormats(processInfo.BlackFileFormats); - BlackListManager.Instance.AddBlackFiles(processInfo.BlackFiles); - BlackListManager.Instance.AddSkipDirectorys(processInfo.SkipDirectorys); + _strategy!.Create(_configInfo); - _configInfo = new GlobalConfigInfo + if (_configInfo.IsMainUpdate) { - MainAppName = processInfo.AppName, - InstallPath = processInfo.InstallPath, - ClientVersion = processInfo.CurrentVersion, - LastVersion = processInfo.LastVersion, - UpdateLogUrl = processInfo.UpdateLogUrl, - Encoding = Encoding.GetEncoding(processInfo.CompressEncoding), - Format = processInfo.CompressFormat, - DownloadTimeOut = processInfo.DownloadTimeOut, - AppSecretKey = processInfo.AppSecretKey, - UpdateVersions = processInfo.UpdateVersions, - TempPath = StorageManager.GetTempDirectory("upgrade_temp"), - ReportUrl = processInfo.ReportUrl, - BackupDirectory = processInfo.BackupDirectory, - Scheme = processInfo.Scheme, - Token = processInfo.Token, - DriveEnabled = GetOption(UpdateOption.Drive) ?? false, - PatchEnabled = GetOption(UpdateOption.Patch) ?? true, - Script = processInfo.Script, - DriverDirectory = processInfo.DriverDirectory - }; + GeneralTracer.Info("ExecuteUpgradeWorkflowAsync: main update required, starting download and execution."); + await DownloadAsync(); + await _strategy.ExecuteAsync(); + } + else + { + GeneralTracer.Info("ExecuteUpgradeWorkflowAsync: no update needed, starting application."); + _strategy.StartApp(); + } } - - private void ApplyRuntimeOptions() + catch (Exception ex) { - _configInfo.Encoding = GetOption(UpdateOption.Encoding) ?? Encoding.Default; - _configInfo.Format = GetOption(UpdateOption.Format) ?? Format.ZIP; - _configInfo.DownloadTimeOut = GetOption(UpdateOption.DownloadTimeOut) ?? 60; - _configInfo.DriveEnabled = GetOption(UpdateOption.Drive) ?? false; - _configInfo.PatchEnabled = GetOption(UpdateOption.Patch) ?? true; + GeneralTracer.Error("ExecuteUpgradeWorkflowAsync threw an exception.", ex); + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); } + } - private void InitBlackList() - { - BlackListManager.Instance.AddBlackFiles(_configInfo.BlackFiles); - BlackListManager.Instance.AddBlackFormats(_configInfo.BlackFormats); - BlackListManager.Instance.AddSkipDirectorys(_configInfo.SkipDirectorys); - } + // ════════════════════════════════════════════════════════════════ + // Download + // ════════════════════════════════════════════════════════════════ - private bool CanSkip(bool isForcibly) - { - if (isForcibly) - { - return false; - } + private async Task DownloadAsync() + { + var manager = new DownloadManager( + _configInfo.TempPath, _configInfo.Format, _configInfo.DownloadTimeOut); - // Treat a null custom skip option as "do not skip". - if (_customSkipOption is null) - { - return false; - } + manager.MultiAllDownloadCompleted += OnMultiAllDownloadCompleted; + manager.MultiDownloadCompleted += OnMultiDownloadCompleted; + manager.MultiDownloadError += OnMultiDownloadError; + manager.MultiDownloadStatistics += OnMultiDownloadStatistics; - return _customSkipOption(); - } - private static bool CheckUpgrade(VersionRespDTO? response) - => response?.Code == 200 && response.Body?.Count > 0; + foreach (var version in _configInfo.UpdateVersions) + manager.Add(new DownloadTask(manager, version)); - private static bool CheckForcibly(IEnumerable? versions) - => versions?.Any(v => v.IsForcibly == true) == true; + await manager.LaunchTasksAsync(); + } - private static int GetPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return PlatformType.Windows; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return PlatformType.Linux; - throw new PlatformNotSupportedException("The current operating system is not supported!"); - } + // ════════════════════════════════════════════════════════════════ + // Helpers + // ════════════════════════════════════════════════════════════════ + + private void InitializeFromEnvironment() + { + var json = Environments.GetEnvironmentVariable("ProcessInfo"); + if (string.IsNullOrWhiteSpace(json)) return; - #endregion + var processInfo = JsonSerializer.Deserialize( + json, ProcessInfoJsonContext.Default.ProcessInfo); + if (processInfo == null) return; - #region Strategy & Events + BlackListManager.Instance.AddBlackFormats(processInfo.BlackFileFormats); + BlackListManager.Instance.AddBlackFiles(processInfo.BlackFiles); + BlackListManager.Instance.AddSkipDirectorys(processInfo.SkipDirectorys); - protected override GeneralUpdateBootstrap StrategyFactory() + _configInfo = new GlobalConfigInfo { - _strategy = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? new WindowsStrategy() - : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? new LinuxStrategy() - : throw new PlatformNotSupportedException("The current operating system is not supported!"); + MainAppName = processInfo.AppName, + InstallPath = processInfo.InstallPath, + ClientVersion = processInfo.CurrentVersion, + LastVersion = processInfo.LastVersion, + UpdateLogUrl = processInfo.UpdateLogUrl, + Encoding = Encoding.GetEncoding(processInfo.CompressEncoding), + Format = processInfo.CompressFormat, + DownloadTimeOut = processInfo.DownloadTimeOut, + AppSecretKey = processInfo.AppSecretKey, + UpdateVersions = processInfo.UpdateVersions, + TempPath = StorageManager.GetTempDirectory("upgrade_temp"), + ReportUrl = processInfo.ReportUrl, + BackupDirectory = processInfo.BackupDirectory, + Scheme = processInfo.Scheme, + Token = processInfo.Token, + DriveEnabled = GetOption(UpdateOption.Drive) ?? false, + PatchEnabled = GetOption(UpdateOption.Patch) ?? true, + Script = processInfo.Script, + DriverDirectory = processInfo.DriverDirectory + }; + } - return this; - } + private void ApplyRuntimeOptions() + { + _configInfo.Encoding = GetOption(UpdateOption.Encoding) ?? Encoding.Default; + _configInfo.Format = GetOption(UpdateOption.Format) ?? Format.ZIP; + _configInfo.DownloadTimeOut = GetOption(UpdateOption.DownloadTimeOut) ?? 60; + _configInfo.DriveEnabled = GetOption(UpdateOption.Drive) ?? false; + _configInfo.PatchEnabled = GetOption(UpdateOption.Patch) ?? true; + } + + private void InitBlackList() + { + BlackListManager.Instance.AddBlackFiles(_configInfo.BlackFiles); + BlackListManager.Instance.AddBlackFormats(_configInfo.BlackFormats); + BlackListManager.Instance.AddSkipDirectorys(_configInfo.SkipDirectorys); + } - protected override Task ExecuteStrategyAsync() => throw new NotImplementedException(); - protected override void ExecuteStrategy() => throw new NotImplementedException(); + private bool CanSkip(bool isForcibly) + { + if (isForcibly) return false; + return _customSkipOption?.Invoke() == true; + } - private GeneralUpdateBootstrap AddListener(Action action) - where TArgs : EventArgs - { - if (action is null) throw new ArgumentNullException(nameof(action)); - EventManager.Instance.AddListener(action); - return this; - } + private bool CanSkipClient(bool isForcibly, UpdateInfoEventArgs updateInfo) + { + if (isForcibly) return false; + return _updatePrecheck?.Invoke(updateInfo) == true; + } - public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( - Action cb) => AddListener(cb); + private static bool CheckUpgrade(VersionRespDTO? response) + => response?.Code == 200 && response.Body?.Count > 0; - public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( - Action cb) => AddListener(cb); + private static bool CheckForcibly(IEnumerable? versions) + => versions?.Any(v => v.IsForcibly == true) == true; - public GeneralUpdateBootstrap AddListenerMultiDownloadError( - Action cb) => AddListener(cb); + private static int GetPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return PlatformType.Windows; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return PlatformType.Linux; + return -1; + } - public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( - Action cb) => AddListener(cb); + /// Check if the target version matches a known-failed upgrade. + private bool CheckFail(string version) + { + var fail = Environments.GetEnvironmentVariable("UpgradeFail"); + if (string.IsNullOrEmpty(fail) || string.IsNullOrEmpty(version)) + return false; - public GeneralUpdateBootstrap AddListenerException( - Action cb) => AddListener(cb); + var failVersion = new Version(fail); + var lastVersion = new Version(version); + return failVersion >= lastVersion; + } - public GeneralUpdateBootstrap AddListenerUpdateInfo( - Action cb) => AddListener(cb); + /// Kill existing Bowl watchdog processes before update. + private void CallSmallBowlHome(string processName) + { + if (string.IsNullOrWhiteSpace(processName)) return; - private void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs e) + try { - GeneralTracer.Info( - $"Multi download statistics, {ObjectTranslator.GetPacketHash(e.Version)} " + - $"[BytesReceived]:{e.BytesReceived} [ProgressPercentage]:{e.ProgressPercentage} " + - $"[Remaining]:{e.Remaining} [TotalBytesToReceive]:{e.TotalBytesToReceive} [Speed]:{e.Speed}"); - EventManager.Instance.Dispatch(sender, e); - } + var processes = Process.GetProcessesByName(processName); + if (processes.Length == 0) + { + GeneralTracer.Info($"No process named {processName} found."); + return; + } - private void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs e) - { - GeneralTracer.Info( - $"Multi download completed, {ObjectTranslator.GetPacketHash(e.Version)} [IsCompleted]:{e.IsComplated}"); - EventManager.Instance.Dispatch(sender, e); + foreach (var process in processes) + { + GeneralTracer.Info($"Killing process {process.ProcessName} (ID: {process.Id})"); + process.Kill(); + } } - - private void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs e) + catch (Exception ex) { - GeneralTracer.Error( - $"Multi download error {ObjectTranslator.GetPacketHash(e.Version)}.", - e.Exception); - EventManager.Instance.Dispatch(sender, e); + GeneralTracer.Error("CallSmallBowlHome threw an exception.", ex); + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message)); } + } - private void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs e) + /// Execute all registered custom pre-update operations. + private void ExecuteCustomOptions() + { + if (!_customOptions.Any()) return; + + foreach (var option in _customOptions) { - GeneralTracer.Info($"Multi all download completed {e.IsAllDownloadCompleted}."); - EventManager.Instance.Dispatch(sender, e); + if (!option.Invoke()) + { + var exception = new Exception($"{nameof(option)} execution failure!"); + GeneralTracer.Error("ExecuteCustomOptions failed.", exception); + EventManager.Instance.Dispatch(this, + new ExceptionEventArgs(exception, exception.Message)); + } } + } + + // ════════════════════════════════════════════════════════════════ + // Strategy & Events + // ════════════════════════════════════════════════════════════════ + + protected override GeneralUpdateBootstrap StrategyFactory() + { + _strategy = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? new WindowsStrategy() + : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? new LinuxStrategy() + : throw new PlatformNotSupportedException("The current operating system is not supported!"); + + return this; + } - #endregion + protected override Task ExecuteStrategyAsync() => throw new NotImplementedException(); + protected override void ExecuteStrategy() => throw new NotImplementedException(); + + private GeneralUpdateBootstrap AddListener(Action action) where TArgs : EventArgs + { + if (action is null) throw new ArgumentNullException(nameof(action)); + EventManager.Instance.AddListener(action); + return this; + } + + public GeneralUpdateBootstrap AddListenerMultiAllDownloadCompleted( + Action cb) => AddListener(cb); + + public GeneralUpdateBootstrap AddListenerMultiDownloadCompleted( + Action cb) => AddListener(cb); + + public GeneralUpdateBootstrap AddListenerMultiDownloadError( + Action cb) => AddListener(cb); + + public GeneralUpdateBootstrap AddListenerMultiDownloadStatistics( + Action cb) => AddListener(cb); + + public GeneralUpdateBootstrap AddListenerException( + Action cb) => AddListener(cb); + + public GeneralUpdateBootstrap AddListenerUpdateInfo( + Action cb) => AddListener(cb); + + private void OnMultiDownloadStatistics(object sender, MultiDownloadStatisticsEventArgs e) + { + GeneralTracer.Info( + $"Multi download statistics, {ObjectTranslator.GetPacketHash(e.Version)} " + + $"[BytesReceived]:{e.BytesReceived} [ProgressPercentage]:{e.ProgressPercentage} " + + $"[Remaining]:{e.Remaining} [TotalBytesToReceive]:{e.TotalBytesToReceive} [Speed]:{e.Speed}"); + EventManager.Instance.Dispatch(sender, e); + } + + private void OnMultiDownloadCompleted(object sender, MultiDownloadCompletedEventArgs e) + { + GeneralTracer.Info( + $"Multi download completed, {ObjectTranslator.GetPacketHash(e.Version)} [IsCompleted]:{e.IsComplated}"); + EventManager.Instance.Dispatch(sender, e); + } + + private void OnMultiDownloadError(object sender, MultiDownloadErrorEventArgs e) + { + GeneralTracer.Error( + $"Multi download error {ObjectTranslator.GetPacketHash(e.Version)}.", e.Exception); + EventManager.Instance.Dispatch(sender, e); + } + + private void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadCompletedEventArgs e) + { + GeneralTracer.Info($"Multi all download completed {e.IsAllDownloadCompleted}."); + EventManager.Instance.Dispatch(sender, e); } -} \ No newline at end of file +}