From c3cfc4e6fc023a28bec673d695a4107b293e52c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:44:12 +0000 Subject: [PATCH 1/3] Initial plan From d3ed677955586e4fb649c2f64eac09ae2f5b7640 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:52:16 +0000 Subject: [PATCH 2/3] feat: add encapsulated silent update mode entry path Agent-Logs-Url: https://github.com/GeneralLibrary/GeneralUpdate/sessions/44d443fb-0e3e-420e-8e14-98bb6a47d8ad Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../GeneralClientBootstrap.cs | 13 +- .../SilentUpdateMode.cs | 185 ++++++++++++++++++ .../Internal/Bootstrap/UpdateOption.cs | 7 +- 3 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs diff --git a/src/c#/GeneralUpdate.ClientCore/GeneralClientBootstrap.cs b/src/c#/GeneralUpdate.ClientCore/GeneralClientBootstrap.cs index 54255570..290cc936 100644 --- a/src/c#/GeneralUpdate.ClientCore/GeneralClientBootstrap.cs +++ b/src/c#/GeneralUpdate.ClientCore/GeneralClientBootstrap.cs @@ -128,6 +128,17 @@ private async Task ExecuteWorkflowAsync() try { Debug.Assert(_configInfo != null); + if (GetOption(UpdateOption.EnableSilentUpdate)) + { + 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; + } //Request the upgrade information needed by the client and upgrade end, and determine if an upgrade is necessary. var mainResp = await VersionService.Validate(_configInfo.UpdateUrl , _configInfo.ClientVersion @@ -423,4 +434,4 @@ private void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadComplete } #endregion Private Methods -} \ No newline at end of file +} diff --git a/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs b/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs new file mode 100644 index 00000000..88006529 --- /dev/null +++ b/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs @@ -0,0 +1,185 @@ +using System; +using System.Diagnostics; +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.ClientCore.Strategys; +using GeneralUpdate.Common.Download; +using GeneralUpdate.Common.FileBasic; +using GeneralUpdate.Common.Internal; +using GeneralUpdate.Common.Internal.Bootstrap; +using GeneralUpdate.Common.Internal.JsonContext; +using GeneralUpdate.Common.Internal.Strategy; +using GeneralUpdate.Common.Shared; +using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Common.Shared.Object.Enum; +using GeneralUpdate.Common.Shared.Service; + +namespace GeneralUpdate.ClientCore; + +internal sealed class SilentUpdateMode +{ + private static readonly TimeSpan PollingInterval = TimeSpan.FromMinutes(20); + private readonly GlobalConfigInfo _configInfo; + private readonly Encoding _encoding; + private readonly string _format; + private readonly int _downloadTimeOut; + private readonly bool _patchEnabled; + private readonly bool _backupEnabled; + private int _prepared; + private int _updaterStarted; + + public SilentUpdateMode(GlobalConfigInfo configInfo, Encoding encoding, string format, int downloadTimeOut, bool patchEnabled, bool backupEnabled) + { + _configInfo = configInfo; + _encoding = encoding; + _format = format; + _downloadTimeOut = downloadTimeOut; + _patchEnabled = patchEnabled; + _backupEnabled = backupEnabled; + } + + public Task StartAsync() + { + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + _ = Task.Run(PollLoopAsync); + return Task.CompletedTask; + } + + private async Task PollLoopAsync() + { + while (Volatile.Read(ref _prepared) == 0) + { + try + { + await PrepareUpdateIfNeededAsync(); + } + catch (Exception exception) + { + GeneralTracer.Error("The PollLoopAsync method in SilentUpdateMode throws an exception.", exception); + } + + if (Volatile.Read(ref _prepared) == 1) + break; + + await Task.Delay(PollingInterval); + } + } + + private async Task PrepareUpdateIfNeededAsync() + { + var mainResp = await VersionService.Validate(_configInfo.UpdateUrl + , _configInfo.ClientVersion + , AppType.ClientApp + , _configInfo.AppSecretKey + , GetPlatform() + , _configInfo.ProductId + , _configInfo.Scheme + , _configInfo.Token); + + if (mainResp?.Code != 200 || mainResp.Body == null || mainResp.Body.Count == 0) + return; + + var versions = mainResp.Body.OrderBy(x => x.ReleaseDate).ToList(); + var lastVersion = versions.Last().Version; + if (CheckFail(lastVersion)) + return; + + BlackListManager.Instance?.AddBlackFiles(_configInfo.BlackFiles); + BlackListManager.Instance?.AddBlackFileFormats(_configInfo.BlackFormats); + BlackListManager.Instance?.AddSkipDirectorys(_configInfo.SkipDirectorys); + + _configInfo.Encoding = _encoding; + _configInfo.Format = _format; + _configInfo.DownloadTimeOut = _downloadTimeOut; + _configInfo.PatchEnabled = _patchEnabled; + _configInfo.IsMainUpdate = true; + _configInfo.LastVersion = lastVersion; + _configInfo.UpdateVersions = versions; + _configInfo.TempPath = StorageManager.GetTempDirectory("main_temp"); + _configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath, $"{StorageManager.DirectoryName}{_configInfo.ClientVersion}"); + + if (_backupEnabled) + { + StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory, BlackListManager.Instance.SkipDirectorys); + } + + var processInfo = ConfigurationMapper.MapToProcessInfo( + _configInfo, + versions, + BlackListManager.Instance.BlackFileFormats.ToList(), + BlackListManager.Instance.BlackFiles.ToList(), + BlackListManager.Instance.SkipDirectorys.ToList()); + _configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo); + + var manager = new DownloadManager(_configInfo.TempPath, _configInfo.Format, _configInfo.DownloadTimeOut); + foreach (var versionInfo in _configInfo.UpdateVersions) + { + manager.Add(new DownloadTask(manager, versionInfo)); + } + await manager.LaunchTasksAsync(); + + var strategy = CreateStrategy(); + strategy.Create(_configInfo); + await strategy.ExecuteAsync(); + + Interlocked.Exchange(ref _prepared, 1); + } + + private void OnProcessExit(object? sender, EventArgs e) + { + if (Volatile.Read(ref _prepared) != 1 || Interlocked.Exchange(ref _updaterStarted, 1) == 1) + return; + + try + { + Environments.SetEnvironmentVariable("ProcessInfo", _configInfo.ProcessInfo); + var appPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); + if (File.Exists(appPath)) + { + Process.Start(new ProcessStartInfo + { + UseShellExecute = true, + FileName = appPath + }); + } + } + catch (Exception exception) + { + GeneralTracer.Error("The OnProcessExit method in SilentUpdateMode throws an exception.", exception); + } + } + + private IStrategy CreateStrategy() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return new WindowsStrategy(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return new LinuxStrategy(); + throw new PlatformNotSupportedException("The current operating system is not supported!"); + } + + private static int GetPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return PlatformType.Windows; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return PlatformType.Linux; + return -1; + } + + private static bool CheckFail(string version) + { + var fail = Environments.GetEnvironmentVariable("UpgradeFail"); + if (string.IsNullOrEmpty(fail) || string.IsNullOrEmpty(version)) + return false; + + var failVersion = new Version(fail); + var lastVersion = new Version(version); + return failVersion >= lastVersion; + } +} diff --git a/src/c#/GeneralUpdate.Common/Internal/Bootstrap/UpdateOption.cs b/src/c#/GeneralUpdate.Common/Internal/Bootstrap/UpdateOption.cs index 35278b2a..9fdb174d 100644 --- a/src/c#/GeneralUpdate.Common/Internal/Bootstrap/UpdateOption.cs +++ b/src/c#/GeneralUpdate.Common/Internal/Bootstrap/UpdateOption.cs @@ -52,6 +52,11 @@ private class UpdateOptionPool : ConstantPool /// Specifies the update execution mode. /// public static readonly UpdateOption Mode = ValueOf("MODE"); + + /// + /// Whether to enable silent update mode. + /// + public static readonly UpdateOption EnableSilentUpdate = ValueOf("ENABLESILENTUPDATE"); internal UpdateOption(int id, string name) : base(id, name) { } @@ -232,4 +237,4 @@ public int CompareTo(T o) throw new System.Exception("failed to compare two different constants"); } } -} \ No newline at end of file +} From 892ae8afb88265ccd55a87b1d31091414e563726 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:55:09 +0000 Subject: [PATCH 3/3] test: cover silent option and refine silent mode internals Agent-Logs-Url: https://github.com/GeneralLibrary/GeneralUpdate/sessions/44d443fb-0e3e-420e-8e14-98bb6a47d8ad Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Bootstrap/GeneralClientBootstrapTests.cs | 18 ++++++++++++ .../SilentUpdateMode.cs | 29 ++++++++++++------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/c#/ClientCoreTest/Bootstrap/GeneralClientBootstrapTests.cs b/src/c#/ClientCoreTest/Bootstrap/GeneralClientBootstrapTests.cs index e6609cc3..5200b149 100644 --- a/src/c#/ClientCoreTest/Bootstrap/GeneralClientBootstrapTests.cs +++ b/src/c#/ClientCoreTest/Bootstrap/GeneralClientBootstrapTests.cs @@ -4,6 +4,7 @@ using GeneralUpdate.ClientCore; using GeneralUpdate.Common.Download; using GeneralUpdate.Common.Internal; +using GeneralUpdate.Common.Internal.Bootstrap; using GeneralUpdate.Common.Shared.Object; using Xunit; @@ -287,6 +288,23 @@ public void FluentInterface_AllowsMethodChaining() Assert.Same(bootstrap, result); } + /// + /// Tests that silent update option can be configured through the fluent option API. + /// + [Fact] + public void Option_EnableSilentUpdate_ReturnsBootstrap() + { + // Arrange + var bootstrap = new GeneralClientBootstrap(); + + // Act + var result = bootstrap.Option(UpdateOption.EnableSilentUpdate, true); + + // Assert + Assert.NotNull(result); + Assert.Same(bootstrap, result); + } + /// /// Tests that Configinfo validates required fields. /// diff --git a/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs b/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs index 88006529..e76a45bb 100644 --- a/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs +++ b/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs @@ -23,6 +23,7 @@ namespace GeneralUpdate.ClientCore; internal sealed class SilentUpdateMode { + private const string ProcessInfoEnvironmentKey = "ProcessInfo"; private static readonly TimeSpan PollingInterval = TimeSpan.FromMinutes(20); private readonly GlobalConfigInfo _configInfo; private readonly Encoding _encoding; @@ -30,6 +31,7 @@ internal sealed class SilentUpdateMode private readonly int _downloadTimeOut; private readonly bool _patchEnabled; private readonly bool _backupEnabled; + private Task? _pollingTask; private int _prepared; private int _updaterStarted; @@ -46,7 +48,14 @@ public SilentUpdateMode(GlobalConfigInfo configInfo, Encoding encoding, string f public Task StartAsync() { AppDomain.CurrentDomain.ProcessExit += OnProcessExit; - _ = Task.Run(PollLoopAsync); + _pollingTask = Task.Run(PollLoopAsync); + _pollingTask.ContinueWith(task => + { + if (task.Exception != null) + { + GeneralTracer.Error("The StartAsync method in SilentUpdateMode captured a polling exception.", task.Exception); + } + }, TaskContinuationOptions.OnlyOnFaulted); return Task.CompletedTask; } @@ -85,8 +94,8 @@ private async Task PrepareUpdateIfNeededAsync() return; var versions = mainResp.Body.OrderBy(x => x.ReleaseDate).ToList(); - var lastVersion = versions.Last().Version; - if (CheckFail(lastVersion)) + var latestVersion = versions.Last().Version; + if (CheckFail(latestVersion)) return; BlackListManager.Instance?.AddBlackFiles(_configInfo.BlackFiles); @@ -98,7 +107,7 @@ private async Task PrepareUpdateIfNeededAsync() _configInfo.DownloadTimeOut = _downloadTimeOut; _configInfo.PatchEnabled = _patchEnabled; _configInfo.IsMainUpdate = true; - _configInfo.LastVersion = lastVersion; + _configInfo.LastVersion = latestVersion; _configInfo.UpdateVersions = versions; _configInfo.TempPath = StorageManager.GetTempDirectory("main_temp"); _configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath, $"{StorageManager.DirectoryName}{_configInfo.ClientVersion}"); @@ -137,14 +146,14 @@ private void OnProcessExit(object? sender, EventArgs e) try { - Environments.SetEnvironmentVariable("ProcessInfo", _configInfo.ProcessInfo); - var appPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); - if (File.Exists(appPath)) + Environments.SetEnvironmentVariable(ProcessInfoEnvironmentKey, _configInfo.ProcessInfo); + var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); + if (File.Exists(updaterPath)) { Process.Start(new ProcessStartInfo { UseShellExecute = true, - FileName = appPath + FileName = updaterPath }); } } @@ -179,7 +188,7 @@ private static bool CheckFail(string version) return false; var failVersion = new Version(fail); - var lastVersion = new Version(version); - return failVersion >= lastVersion; + var latestVersion = new Version(version); + return failVersion >= latestVersion; } }