-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathUpdateStrategy.cs
More file actions
299 lines (266 loc) · 13.1 KB
/
Copy pathUpdateStrategy.cs
File metadata and controls
299 lines (266 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Event;
using GeneralUpdate.Core.Pipeline;
namespace GeneralUpdate.Core.Strategy;
/// <summary>
/// Upgrade-side update strategy. Receives process information passed from the client via encrypted IPC,
/// applies updates, and launches the main application.
/// </summary>
/// <remarks>
/// <para>
/// This strategy serves the <c>AppType.Upgrade</c> role and uses a two-layer strategy design:
/// the upper role strategy (this class) handles workflow orchestration,
/// while the lower OS-level strategy (<see cref="WindowsStrategy"/>, <see cref="LinuxStrategy"/>, <see cref="MacStrategy"/>)
/// handles platform-specific operations.
/// </para>
/// <para>
/// <b>Execution Flow:</b>
/// <list type="number">
/// <item><description>Receives the <see cref="GlobalConfigInfo"/> passed from the client via the <see cref="Create"/> method,
/// which contains already-downloaded update package paths, hash values, and other metadata.</description></item>
/// <item><description>Calls the <see cref="Hooks.IUpdateHooks.OnBeforeUpdateAsync"/> lifecycle hook,
/// allowing the caller to execute custom logic or cancel the operation before applying updates.</description></item>
/// <item><description>Delegates to the OS strategy to execute the update pipeline: processes each version through the
/// <c>Hash</c> (hash verification) → <c>Decompress</c> (extraction) → <c>Patch</c> (incremental patch) middleware chain.</description></item>
/// <item><description>Calls the <see cref="Hooks.IUpdateHooks.OnAfterUpdateAsync"/> hook to notify the caller that all updates have been applied.</description></item>
/// <item><description>Calls the <see cref="Hooks.IUpdateHooks.OnBeforeStartAppAsync"/> hook,
/// allowing the caller to perform additional operations before launching the main application
/// (such as setting executable permissions or preparing resource files).</description></item>
/// <item><description>Launches the main application (<c>MainAppName</c>) and the Bowl helper process through the OS strategy.</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Design Note:</b> The upgrade side does not perform version validation or download operations.
/// The client has already completed all network requests and downloads, passing results through process information.
/// The upgrade side is responsible only for applying updates and launching the application -- zero network overhead.
/// </para>
/// </remarks>
public class UpdateStrategy : IStrategy
{
private GlobalConfigInfo? _configInfo;
private IStrategy? _osStrategy;
private IStrategy? _customOsStrategy;
private int _reportType = 1; // 1=Upgrade(active poll), 2=Push(SignalR push)
/// <summary>
/// Gets or sets the lifecycle hooks. Injected by the bootstrap to execute custom logic at key points in the update flow.
/// </summary>
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
/// <summary>
/// Gets or sets the update status reporter. Injected by the bootstrap to report update progress and results to the server or caller.
/// </summary>
public Download.Reporting.IUpdateReporter Reporter { get; set; } = new Download.Reporting.HttpUpdateReporter();
/// <summary>
/// Sets a custom OS-level strategy (injected via <c>.Strategy<T>()</c>).
/// When set, replaces the automatic platform detection logic in <see cref="ResolveOsStrategy"/>.
/// </summary>
public void SetOsStrategy(IStrategy? strategy) => _customOsStrategy = strategy;
/// <summary>
/// Sets the report type for status reporting. The caller (ClientStrategy) should pass the same
/// report type it used, so the server can distinguish push-triggered updates from active polls.
/// </summary>
/// <param name="reportType">1 = Upgrade (active poll), 2 = Push (SignalR push). Default is 1.</param>
public void SetReportType(int reportType) => _reportType = reportType;
/// <summary>
/// Initializes the upgrade-side strategy. Receives the global configuration information passed from the client
/// and resolves the strategy instance for the current operating system.
/// </summary>
/// <param name="parameter">Global configuration information containing update package paths, hash values, version information, etc.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="parameter"/> is null.</exception>
public void Create(GlobalConfigInfo parameter)
{
_configInfo = parameter ?? throw new ArgumentNullException(nameof(parameter));
_osStrategy = ResolveOsStrategy();
if (_osStrategy is AbstractStrategy abs)
{
if (_pendingDiffPipeline != null) abs.DiffPipeline = _pendingDiffPipeline;
abs.Reporter = this.Reporter;
}
}
/// <summary>
/// Executes the upgrade-side update flow. Follows the lifecycle order: pre-update hook, OS update pipeline,
/// post-update hook, pre-start-app hook, and main application launch.
/// </summary>
public async Task ExecuteAsync()
{
if (_configInfo == null) throw new InvalidOperationException("UpdateStrategy not configured.");
var ctx = BuildUpdateContext();
try
{
GeneralTracer.Debug("UpdateStrategy.ExecuteAsync start.");
// Hooks: allow cancellation before applying updates
if (!await SafeOnBeforeUpdateAsync(ctx).ConfigureAwait(false))
{
GeneralTracer.Info("UpdateStrategy: update cancelled by OnBeforeUpdateAsync hook.");
return;
}
_osStrategy!.Create(_configInfo);
// Apply MainApp updates -- Client already applied Upgrade packages, IPC only has MainApp versions
if (_configInfo.UpdateVersions?.Count > 0)
{
GeneralTracer.Info("UpdateStrategy: applying " + _configInfo.UpdateVersions.Count +
" MainApp update(s).");
await _osStrategy.ExecuteAsync();
// Only advance the manifest version when every package was applied
// successfully. AbstractStrategy catches per-package failures and
// continues the loop, so ExecuteAsync() completing is not a
// reliable success signal on its own.
if ((_osStrategy as AbstractStrategy)?.AllPackagesSucceeded == true)
WriteBackClientVersion();
}
else
{
GeneralTracer.Info("UpdateStrategy: no updates to apply, starting application directly.");
}
// Hooks: after all updates applied
await SafeOnAfterUpdateAsync(ctx).ConfigureAwait(false);
// Report: update applied successfully — uses the first Client package's RecordId
await SafeReportUpdateAppliedAsync(ctx).ConfigureAwait(false);
// Hooks: before starting main app (e.g. chmod +x on Linux/macOS)
await SafeOnBeforeStartAppAsync(ctx).ConfigureAwait(false);
// Delegate to OS strategy: launch MainAppName + Bowl.
// Skip if silent mode requested no-launch (e.g. maintenance windows).
if (_configInfo.LaunchClientAfterUpdate)
{
if (_osStrategy is AbstractStrategy abs2)
{
abs2.LaunchAppName = _configInfo.MainAppName;
abs2.LaunchBowl = true;
}
await _osStrategy.StartAppAsync();
}
else
{
GeneralTracer.Info("UpdateStrategy: LaunchClientAfterUpdate=false, skipping app launch.");
}
}
catch (Exception ex)
{
await SafeOnUpdateErrorAsync(ctx, ex).ConfigureAwait(false);
await SafeReportUpdateFailedAsync(ctx, ex).ConfigureAwait(false);
GeneralTracer.Error("UpdateStrategy.ExecuteAsync failed.", ex);
EventManager.Instance.Dispatch(this, new ExceptionEventArgs(ex, ex.Message));
}
}
private DiffPipeline? _pendingDiffPipeline;
/// <summary>
/// Sets the differential patch pipeline on the underlying OS-level strategy for parallel patch application.
/// </summary>
/// <param name="diffPipeline">The differential pipeline instance. If <c>null</c>, clears the pending pipeline.</param>
public void SetDiffPipeline(DiffPipeline? diffPipeline)
{
if (_osStrategy is AbstractStrategy abs)
abs.DiffPipeline = diffPipeline;
else
_pendingDiffPipeline = diffPipeline;
}
/// <summary>
/// Starts the main application. Delegates to the underlying OS strategy for platform-specific application launch logic.
/// </summary>
public async Task StartAppAsync()
{
if (_osStrategy != null)
await _osStrategy.StartAppAsync();
}
#region Helpers
private IStrategy ResolveOsStrategy()
{
if (_customOsStrategy != null)
return _customOsStrategy;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return new WindowsStrategy();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return new LinuxStrategy();
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return new MacStrategy();
throw new PlatformNotSupportedException("The current operating system is not supported!");
}
private Hooks.UpdateContext BuildUpdateContext()
{
return new Hooks.UpdateContext(
_configInfo?.UpdateAppName ?? "unknown",
_configInfo?.InstallPath ?? AppDomain.CurrentDomain.BaseDirectory,
_configInfo?.ClientVersion ?? "0.0.0",
_configInfo?.LastVersion,
AppType.Upgrade
);
}
private async Task<bool> 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 SafeOnAfterUpdateAsync(Hooks.UpdateContext ctx)
{
try { await Hooks.OnAfterUpdateAsync(ctx).ConfigureAwait(false); }
catch (Exception ex) { GeneralTracer.Warn($"OnAfterUpdateAsync hook failed: {ex.Message}"); }
}
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 SafeReportUpdateAppliedAsync(Hooks.UpdateContext ctx)
{
try
{
var recordId = _configInfo?.UpdateVersions?.FirstOrDefault()?.RecordId ?? 0;
await Reporter
.ReportAsync(new Download.Reporting.UpdateReport(recordId,
(int)Download.Reporting.UpdateStatus.Success, _reportType)).ConfigureAwait(false);
}
catch (Exception ex)
{
GeneralTracer.Warn($"Report UpdateApplied failed: {ex.Message}");
}
}
private async Task SafeReportUpdateFailedAsync(Hooks.UpdateContext ctx, Exception error)
{
try
{
var recordId = _configInfo?.UpdateVersions?.FirstOrDefault()?.RecordId ?? 0;
await Reporter
.ReportAsync(new Download.Reporting.UpdateReport(recordId,
(int)Download.Reporting.UpdateStatus.Failure, _reportType)).ConfigureAwait(false);
}
catch (Exception ex)
{
GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}");
}
}
/// <summary>
/// After the main-app update pipeline completes, writes the new <c>ClientVersion</c>
/// back to <c>generalupdate.manifest.json</c> in the client's install directory so the
/// next poll cycle starts from the correct version.
/// </summary>
private void WriteBackClientVersion()
{
// Use the latest version from the applied update list; fall back to LastVersion
// (which covers the single-package / full-update case).
var latestVersion = _configInfo?.UpdateVersions?.LastOrDefault()?.Version
?? _configInfo?.LastVersion;
if (string.IsNullOrEmpty(latestVersion)) return;
try
{
ManifestInfo.TryUpdateVersion(
_configInfo!.InstallPath,
clientVersion: latestVersion);
GeneralTracer.Info(
$"UpdateStrategy: ClientVersion updated to {latestVersion} in manifest.");
}
catch (Exception ex)
{
GeneralTracer.Warn(
$"UpdateStrategy: failed to write back ClientVersion: {ex.Message}");
}
}
#endregion
}