-
-
Notifications
You must be signed in to change notification settings - Fork 37
/
Copy pathAutoUpdater.cs
384 lines (315 loc) · 13.4 KB
/
AutoUpdater.cs
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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
namespace LauncherBackend.Services;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text.Json;
using DevCenterCommunication.Models;
using Microsoft.Extensions.Logging;
using Models;
using SHA3.Net;
using SharedBase.Utilities;
using Utilities;
public class AutoUpdater : IAutoUpdater
{
private readonly ILogger<AutoUpdater> logger;
private readonly ILauncherPaths launcherPaths;
private readonly INetworkDataRetriever networkDataRetriever;
private readonly ILauncherSettingsManager settingsManager;
private AutoUpdateAttemptInfo? previousAttemptInfo;
public AutoUpdater(ILogger<AutoUpdater> logger, ILauncherPaths launcherPaths,
INetworkDataRetriever networkDataRetriever, ILauncherSettingsManager settingsManager)
{
this.logger = logger;
this.launcherPaths = launcherPaths;
this.networkDataRetriever = networkDataRetriever;
this.settingsManager = settingsManager;
}
public ObservableCollection<FilePrepareProgress> InProgressOperations { get; } = new();
private string BaseTempFolder => !string.IsNullOrEmpty(settingsManager.Settings.TemporaryDownloadsFolder) ?
settingsManager.Settings.TemporaryDownloadsFolder :
launcherPaths.PathToTemporaryFolder;
public async Task<bool> PerformAutoUpdate(DownloadableInfo installerDownload,
LauncherAutoUpdateChannel updateChannel, string currentVersion, CancellationToken cancellationToken)
{
logger.LogInformation("Beginning auto-update from launcher version {CurrentVersion}", currentVersion);
InProgressOperations.Clear();
previousAttemptInfo ??= new AutoUpdateAttemptInfo(currentVersion);
if (previousAttemptInfo.PreviousLauncherVersion != currentVersion)
{
logger.LogInformation("Updating previously attempted auto-update from launcher version");
previousAttemptInfo.PreviousLauncherVersion = currentVersion;
}
if (installerDownload.Mirrors.Count < 1)
{
logger.LogError("Launcher update download has no mirrors");
return false;
}
// TODO: preferred mirrors
var pickedMirror = installerDownload.Mirrors.First();
logger.LogDebug("Picked mirror: {Key}", pickedMirror.Key);
var downloadUrl = pickedMirror.Value;
// TODO: could make it so that the server includes the launcher versions in the update file names, but it's
// maybe not necessary for now
var installerFile = Path.Join(BaseTempFolder, installerDownload.LocalFileName);
// Register the path to be deleted when updating is finished
previousAttemptInfo.UpdateFiles.Add(installerFile);
await WritePreviousAttemptInfo();
logger.LogInformation("Beginning download of installer from {DownloadUrl} to {InstallerFile}", downloadUrl,
installerFile);
var filename = Path.GetFileName(downloadUrl.AbsolutePath);
var operation = new FilePrepareProgress(filename, downloadUrl.WithoutQuery(), pickedMirror.Key);
InProgressOperations.Add(operation);
using var downloadHttpClient = new HttpClient();
downloadHttpClient.Timeout = TimeSpan.FromMinutes(3);
downloadHttpClient.DefaultRequestHeaders.UserAgent.Clear();
downloadHttpClient.DefaultRequestHeaders.UserAgent.Add(networkDataRetriever.UserAgent);
string hash;
try
{
hash = await HashedFileDownloader.DownloadAndHashFile(downloadHttpClient, downloadUrl, installerFile,
Sha3.Sha3256(),
operation, logger, cancellationToken);
}
catch (Exception e)
{
// TODO: do we want to output more in-depth error messages to the user?
// InstallerMessages.Add(new ThrivePlayMessage(ThrivePlayMessage.Type.DownloadingFailed, e.Message));
logger.LogError(e, "Failed to download update installer");
return false;
}
operation.MoveToVerifyStep();
if (hash != installerDownload.FileSha3)
{
try
{
File.Delete(installerFile);
}
catch (Exception e)
{
logger.LogWarning(e, "Failed to remove file with mismatching hash");
}
logger.LogError("Downloaded update installer has wrong hash");
return false;
}
operation.MoveToProcessingStep();
try
{
await StartUpdater(installerFile, updateChannel);
}
catch (Exception e)
{
logger.LogError(e, "Failed to start updater");
return false;
}
logger.LogInformation("Updater should have started");
InProgressOperations.Remove(operation);
return true;
}
public async Task NotifyLatestVersionInstalled()
{
await LoadPreviousAttemptInfo();
await ClearAutoUpdaterFiles();
}
public async Task<bool> CheckFailedAutoUpdate(string currentVersion)
{
await LoadPreviousAttemptInfo();
if (previousAttemptInfo == null)
{
return false;
}
logger.LogInformation("Detected previous update attempt with old launcher version: {PreviousLauncherVersion}",
previousAttemptInfo.PreviousLauncherVersion);
return previousAttemptInfo.PreviousLauncherVersion == currentVersion;
}
public IEnumerable<string> GetPathsToAlreadyDownloadedUpdateFiles()
{
if (previousAttemptInfo == null)
yield break;
foreach (var updateFile in previousAttemptInfo.UpdateFiles)
{
if (!File.Exists(updateFile))
continue;
yield return updateFile;
}
}
public async Task<bool> RetryUpdateApplying(string downloadedUpdateFile,
LauncherAutoUpdateChannel updateChannelType, CancellationToken cancellationToken)
{
try
{
await StartUpdater(downloadedUpdateFile, updateChannelType);
return true;
}
catch (Exception e)
{
logger.LogError(e, "Failed to retry updater running: {DownloadedUpdateFile}", downloadedUpdateFile);
return false;
}
}
public async Task ClearAutoUpdaterFiles()
{
if (previousAttemptInfo == null)
{
logger.LogDebug("Nothing to do about auto-update files");
return;
}
foreach (var updateFile in previousAttemptInfo.UpdateFiles)
{
if (!File.Exists(updateFile))
{
logger.LogInformation("Auto-updater file is already gone: {UpdateFile}", updateFile);
continue;
}
logger.LogInformation("Attempting to delete auto-updater file which is no longer needed: {UpdateFile}",
updateFile);
try
{
File.Delete(updateFile);
}
catch (Exception e)
{
logger.LogError(e, "Failed to delete a file left over from the auto-update process");
return;
}
}
logger.LogInformation("Clearing auto-update file data");
previousAttemptInfo = null;
await WritePreviousAttemptInfo();
}
private async Task LoadPreviousAttemptInfo()
{
var file = launcherPaths.PathToAutoUpdateFile;
previousAttemptInfo = null;
if (!File.Exists(file))
return;
try
{
await using var reader = File.OpenRead(file);
previousAttemptInfo = await JsonSerializer.DeserializeAsync<AutoUpdateAttemptInfo>(reader) ??
throw new NullDecodedJsonException();
logger.LogInformation("Loaded existing auto-update data file");
}
catch (Exception e)
{
logger.LogError(e, "Failed to load auto-update data file");
}
}
private async Task WritePreviousAttemptInfo()
{
var file = launcherPaths.PathToAutoUpdateFile;
try
{
if (previousAttemptInfo == null)
{
if (File.Exists(file))
{
logger.LogInformation(
"Deleting existing auto-update data file ({File}) as it exists and our data is null", file);
File.Delete(file);
}
return;
}
await File.WriteAllTextAsync(file, JsonSerializer.Serialize(previousAttemptInfo));
logger.LogInformation("Wrote auto-update data file at {File}", file);
}
catch (Exception e)
{
logger.LogError(e, "Failed to write auto-update data file");
}
}
private async Task StartUpdater(string installerFile, LauncherAutoUpdateChannel updateChannelType)
{
if (!File.Exists(installerFile))
throw new ArgumentException($"The installer file doesn't exist at: {installerFile}");
switch (updateChannelType)
{
case LauncherAutoUpdateChannel.WindowsInstaller:
{
// For Windows we start running the installer through explorer.exe so that it's not our child process
var explorer = ExecutableFinder.Which("explorer.exe");
Process? process;
if (explorer == null)
{
logger.LogError("Could not find explorer.exe, cannot start process as not our child");
process = Process.Start(installerFile);
}
else
{
var startInfo = new ProcessStartInfo(explorer);
startInfo.ArgumentList.Add(installerFile);
process = Process.Start(startInfo);
}
await Task.Delay(TimeSpan.FromMilliseconds(350));
if (process is { HasExited: true })
{
logger.LogInformation("Updater process already exited with code: {ExitCode}", process.ExitCode);
}
break;
}
case LauncherAutoUpdateChannel.MacDmg:
{
// For mac we just want to get the .dmg file opened and mounted
var startInfo = new ProcessStartInfo("open");
startInfo.ArgumentList.Add(installerFile);
var process = Process.Start(startInfo);
await Task.Delay(TimeSpan.FromMilliseconds(350));
if (process is { HasExited: true })
{
logger.LogInformation("File opener process for updater already exited with code: {ExitCode}",
process.ExitCode);
}
break;
}
case LauncherAutoUpdateChannel.LinuxUnpacked:
// This might get implemented at some point in the future
throw new NotImplementedException();
default:
throw new ArgumentOutOfRangeException(nameof(updateChannelType), updateChannelType, null);
}
}
}
public interface IAutoUpdater
{
/// <summary>
/// Performs an auto update. <see cref="CheckFailedAutoUpdate"/> should be called before calling this to preserve
/// knowledge about existing already downloaded updater files.
/// </summary>
/// <param name="installerDownload">The download to download from</param>
/// <param name="updateChannel">
/// The update channel the download is for, needs to be known for how to handle the update file
/// </param>
/// <param name="currentVersion">
/// The current version of the launcher, this is needed to detect whether the update succeeded or not.
/// </param>
/// <param name="cancellationToken">Cancellation</param>
/// <returns>
/// True when the update is successful and the installer for the new version should have started (and this
/// instance of the launcher should auto close soon)
/// </returns>
public Task<bool> PerformAutoUpdate(DownloadableInfo installerDownload, LauncherAutoUpdateChannel updateChannel,
string currentVersion, CancellationToken cancellationToken);
/// <summary>
/// Should be called when the launcher is detected as the latest version. This clears the auto-update temporary
/// files.
/// </summary>
public Task NotifyLatestVersionInstalled();
/// <summary>
/// Returns true when there's auto-update files that haven't been deleted
/// </summary>
/// <param name="currentVersion">
/// The current version of the running launcher. Used to compare against the version we tried to update from to
/// detect if we are now using a different version or not.
/// </param>
/// <returns>True when updating has failed and the user should be notified</returns>
public Task<bool> CheckFailedAutoUpdate(string currentVersion);
public IEnumerable<string> GetPathsToAlreadyDownloadedUpdateFiles();
public Task<bool> RetryUpdateApplying(string downloadedUpdateFile, LauncherAutoUpdateChannel updateChannelType,
CancellationToken cancellationToken);
/// <summary>
/// Clears the updater files even though there's a failure
/// </summary>
public Task ClearAutoUpdaterFiles();
/// <summary>
/// Progress reporting for the current update process
/// </summary>
public ObservableCollection<FilePrepareProgress> InProgressOperations { get; }
}