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/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..e76a45bb --- /dev/null +++ b/src/c#/GeneralUpdate.ClientCore/SilentUpdateMode.cs @@ -0,0 +1,194 @@ +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 const string ProcessInfoEnvironmentKey = "ProcessInfo"; + 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 Task? _pollingTask; + 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; + _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; + } + + 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 latestVersion = versions.Last().Version; + if (CheckFail(latestVersion)) + 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 = latestVersion; + _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(ProcessInfoEnvironmentKey, _configInfo.ProcessInfo); + var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName); + if (File.Exists(updaterPath)) + { + Process.Start(new ProcessStartInfo + { + UseShellExecute = true, + FileName = updaterPath + }); + } + } + 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 latestVersion = new Version(version); + return failVersion >= latestVersion; + } +} 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 +}