Skip to content

Commit 4e78e20

Browse files
CopilotJusterZhu
andauthored
Add opt-in silent update mode in ClientCore with encapsulated polling/pipeline execution (#164)
* Initial plan * 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> * 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> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com>
1 parent ba27ec5 commit 4e78e20

4 files changed

Lines changed: 230 additions & 2 deletions

File tree

src/c#/ClientCoreTest/Bootstrap/GeneralClientBootstrapTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using GeneralUpdate.ClientCore;
55
using GeneralUpdate.Common.Download;
66
using GeneralUpdate.Common.Internal;
7+
using GeneralUpdate.Common.Internal.Bootstrap;
78
using GeneralUpdate.Common.Shared.Object;
89
using Xunit;
910

@@ -287,6 +288,23 @@ public void FluentInterface_AllowsMethodChaining()
287288
Assert.Same(bootstrap, result);
288289
}
289290

291+
/// <summary>
292+
/// Tests that silent update option can be configured through the fluent option API.
293+
/// </summary>
294+
[Fact]
295+
public void Option_EnableSilentUpdate_ReturnsBootstrap()
296+
{
297+
// Arrange
298+
var bootstrap = new GeneralClientBootstrap();
299+
300+
// Act
301+
var result = bootstrap.Option(UpdateOption.EnableSilentUpdate, true);
302+
303+
// Assert
304+
Assert.NotNull(result);
305+
Assert.Same(bootstrap, result);
306+
}
307+
290308
/// <summary>
291309
/// Tests that Configinfo validates required fields.
292310
/// </summary>

src/c#/GeneralUpdate.ClientCore/GeneralClientBootstrap.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ private async Task ExecuteWorkflowAsync()
128128
try
129129
{
130130
Debug.Assert(_configInfo != null);
131+
if (GetOption(UpdateOption.EnableSilentUpdate))
132+
{
133+
await new SilentUpdateMode(
134+
_configInfo,
135+
GetOption(UpdateOption.Encoding) ?? Encoding.Default,
136+
GetOption(UpdateOption.Format) ?? Format.ZIP,
137+
GetOption(UpdateOption.DownloadTimeOut) ?? 60,
138+
GetOption(UpdateOption.Patch) ?? true,
139+
GetOption(UpdateOption.BackUp) ?? true).StartAsync();
140+
return;
141+
}
131142
//Request the upgrade information needed by the client and upgrade end, and determine if an upgrade is necessary.
132143
var mainResp = await VersionService.Validate(_configInfo.UpdateUrl
133144
, _configInfo.ClientVersion
@@ -423,4 +434,4 @@ private void OnMultiAllDownloadCompleted(object sender, MultiAllDownloadComplete
423434
}
424435

425436
#endregion Private Methods
426-
}
437+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
using System.Text;
7+
using System.Text.Json;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using GeneralUpdate.ClientCore.Strategys;
11+
using GeneralUpdate.Common.Download;
12+
using GeneralUpdate.Common.FileBasic;
13+
using GeneralUpdate.Common.Internal;
14+
using GeneralUpdate.Common.Internal.Bootstrap;
15+
using GeneralUpdate.Common.Internal.JsonContext;
16+
using GeneralUpdate.Common.Internal.Strategy;
17+
using GeneralUpdate.Common.Shared;
18+
using GeneralUpdate.Common.Shared.Object;
19+
using GeneralUpdate.Common.Shared.Object.Enum;
20+
using GeneralUpdate.Common.Shared.Service;
21+
22+
namespace GeneralUpdate.ClientCore;
23+
24+
internal sealed class SilentUpdateMode
25+
{
26+
private const string ProcessInfoEnvironmentKey = "ProcessInfo";
27+
private static readonly TimeSpan PollingInterval = TimeSpan.FromMinutes(20);
28+
private readonly GlobalConfigInfo _configInfo;
29+
private readonly Encoding _encoding;
30+
private readonly string _format;
31+
private readonly int _downloadTimeOut;
32+
private readonly bool _patchEnabled;
33+
private readonly bool _backupEnabled;
34+
private Task? _pollingTask;
35+
private int _prepared;
36+
private int _updaterStarted;
37+
38+
public SilentUpdateMode(GlobalConfigInfo configInfo, Encoding encoding, string format, int downloadTimeOut, bool patchEnabled, bool backupEnabled)
39+
{
40+
_configInfo = configInfo;
41+
_encoding = encoding;
42+
_format = format;
43+
_downloadTimeOut = downloadTimeOut;
44+
_patchEnabled = patchEnabled;
45+
_backupEnabled = backupEnabled;
46+
}
47+
48+
public Task StartAsync()
49+
{
50+
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
51+
_pollingTask = Task.Run(PollLoopAsync);
52+
_pollingTask.ContinueWith(task =>
53+
{
54+
if (task.Exception != null)
55+
{
56+
GeneralTracer.Error("The StartAsync method in SilentUpdateMode captured a polling exception.", task.Exception);
57+
}
58+
}, TaskContinuationOptions.OnlyOnFaulted);
59+
return Task.CompletedTask;
60+
}
61+
62+
private async Task PollLoopAsync()
63+
{
64+
while (Volatile.Read(ref _prepared) == 0)
65+
{
66+
try
67+
{
68+
await PrepareUpdateIfNeededAsync();
69+
}
70+
catch (Exception exception)
71+
{
72+
GeneralTracer.Error("The PollLoopAsync method in SilentUpdateMode throws an exception.", exception);
73+
}
74+
75+
if (Volatile.Read(ref _prepared) == 1)
76+
break;
77+
78+
await Task.Delay(PollingInterval);
79+
}
80+
}
81+
82+
private async Task PrepareUpdateIfNeededAsync()
83+
{
84+
var mainResp = await VersionService.Validate(_configInfo.UpdateUrl
85+
, _configInfo.ClientVersion
86+
, AppType.ClientApp
87+
, _configInfo.AppSecretKey
88+
, GetPlatform()
89+
, _configInfo.ProductId
90+
, _configInfo.Scheme
91+
, _configInfo.Token);
92+
93+
if (mainResp?.Code != 200 || mainResp.Body == null || mainResp.Body.Count == 0)
94+
return;
95+
96+
var versions = mainResp.Body.OrderBy(x => x.ReleaseDate).ToList();
97+
var latestVersion = versions.Last().Version;
98+
if (CheckFail(latestVersion))
99+
return;
100+
101+
BlackListManager.Instance?.AddBlackFiles(_configInfo.BlackFiles);
102+
BlackListManager.Instance?.AddBlackFileFormats(_configInfo.BlackFormats);
103+
BlackListManager.Instance?.AddSkipDirectorys(_configInfo.SkipDirectorys);
104+
105+
_configInfo.Encoding = _encoding;
106+
_configInfo.Format = _format;
107+
_configInfo.DownloadTimeOut = _downloadTimeOut;
108+
_configInfo.PatchEnabled = _patchEnabled;
109+
_configInfo.IsMainUpdate = true;
110+
_configInfo.LastVersion = latestVersion;
111+
_configInfo.UpdateVersions = versions;
112+
_configInfo.TempPath = StorageManager.GetTempDirectory("main_temp");
113+
_configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath, $"{StorageManager.DirectoryName}{_configInfo.ClientVersion}");
114+
115+
if (_backupEnabled)
116+
{
117+
StorageManager.Backup(_configInfo.InstallPath, _configInfo.BackupDirectory, BlackListManager.Instance.SkipDirectorys);
118+
}
119+
120+
var processInfo = ConfigurationMapper.MapToProcessInfo(
121+
_configInfo,
122+
versions,
123+
BlackListManager.Instance.BlackFileFormats.ToList(),
124+
BlackListManager.Instance.BlackFiles.ToList(),
125+
BlackListManager.Instance.SkipDirectorys.ToList());
126+
_configInfo.ProcessInfo = JsonSerializer.Serialize(processInfo, ProcessInfoJsonContext.Default.ProcessInfo);
127+
128+
var manager = new DownloadManager(_configInfo.TempPath, _configInfo.Format, _configInfo.DownloadTimeOut);
129+
foreach (var versionInfo in _configInfo.UpdateVersions)
130+
{
131+
manager.Add(new DownloadTask(manager, versionInfo));
132+
}
133+
await manager.LaunchTasksAsync();
134+
135+
var strategy = CreateStrategy();
136+
strategy.Create(_configInfo);
137+
await strategy.ExecuteAsync();
138+
139+
Interlocked.Exchange(ref _prepared, 1);
140+
}
141+
142+
private void OnProcessExit(object? sender, EventArgs e)
143+
{
144+
if (Volatile.Read(ref _prepared) != 1 || Interlocked.Exchange(ref _updaterStarted, 1) == 1)
145+
return;
146+
147+
try
148+
{
149+
Environments.SetEnvironmentVariable(ProcessInfoEnvironmentKey, _configInfo.ProcessInfo);
150+
var updaterPath = Path.Combine(_configInfo.InstallPath, _configInfo.AppName);
151+
if (File.Exists(updaterPath))
152+
{
153+
Process.Start(new ProcessStartInfo
154+
{
155+
UseShellExecute = true,
156+
FileName = updaterPath
157+
});
158+
}
159+
}
160+
catch (Exception exception)
161+
{
162+
GeneralTracer.Error("The OnProcessExit method in SilentUpdateMode throws an exception.", exception);
163+
}
164+
}
165+
166+
private IStrategy CreateStrategy()
167+
{
168+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
169+
return new WindowsStrategy();
170+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
171+
return new LinuxStrategy();
172+
throw new PlatformNotSupportedException("The current operating system is not supported!");
173+
}
174+
175+
private static int GetPlatform()
176+
{
177+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
178+
return PlatformType.Windows;
179+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
180+
return PlatformType.Linux;
181+
return -1;
182+
}
183+
184+
private static bool CheckFail(string version)
185+
{
186+
var fail = Environments.GetEnvironmentVariable("UpgradeFail");
187+
if (string.IsNullOrEmpty(fail) || string.IsNullOrEmpty(version))
188+
return false;
189+
190+
var failVersion = new Version(fail);
191+
var latestVersion = new Version(version);
192+
return failVersion >= latestVersion;
193+
}
194+
}

src/c#/GeneralUpdate.Common/Internal/Bootstrap/UpdateOption.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ private class UpdateOptionPool : ConstantPool
5252
/// Specifies the update execution mode.
5353
/// </summary>
5454
public static readonly UpdateOption<UpdateMode?> Mode = ValueOf<UpdateMode?>("MODE");
55+
56+
/// <summary>
57+
/// Whether to enable silent update mode.
58+
/// </summary>
59+
public static readonly UpdateOption<bool> EnableSilentUpdate = ValueOf<bool>("ENABLESILENTUPDATE");
5560

5661
internal UpdateOption(int id, string name)
5762
: base(id, name) { }
@@ -232,4 +237,4 @@ public int CompareTo(T o)
232237
throw new System.Exception("failed to compare two different constants");
233238
}
234239
}
235-
}
240+
}

0 commit comments

Comments
 (0)