markus / MarkusAutoUpdate / src / NetSparkle / SparkleUpdater.cs @ d8f5045e
이력 | 보기 | 이력해설 | 다운로드 (75.5 KB)
1 |
using System; |
---|---|
2 |
using System.ComponentModel; |
3 |
using System.Net; |
4 |
using System.Net.Security; |
5 |
using System.Security.Cryptography.X509Certificates; |
6 |
using System.Threading; |
7 |
using NetSparkleUpdater.Interfaces; |
8 |
using System.IO; |
9 |
using System.Diagnostics; |
10 |
using System.Threading.Tasks; |
11 |
using NetSparkleUpdater.Enums; |
12 |
using System.Net.Http; |
13 |
using NetSparkleUpdater.Events; |
14 |
using System.Collections.Generic; |
15 |
using NetSparkleUpdater.Downloaders; |
16 |
using NetSparkleUpdater.Configurations; |
17 |
using NetSparkleUpdater.SignatureVerifiers; |
18 |
using NetSparkleUpdater.AppCastHandlers; |
19 |
using NetSparkleUpdater.AssemblyAccessors; |
20 |
using System.Text; |
21 |
using System.Globalization; |
22 |
#if NETSTANDARD |
23 |
using System.Runtime.InteropServices; |
24 |
#endif |
25 |
|
26 |
namespace NetSparkleUpdater |
27 |
{ |
28 |
/// <summary> |
29 |
/// Class to communicate with a sparkle-based appcast to download |
30 |
/// and install updates to an application |
31 |
/// </summary> |
32 |
public partial class SparkleUpdater : IDisposable |
33 |
{ |
34 |
#region Protected/Private Members |
35 |
|
36 |
/// <summary> |
37 |
/// The <see cref="Process"/> responsible for launching the downloaded update. |
38 |
/// Only valid once the application is about to quit and the update is going to |
39 |
/// be launched. |
40 |
/// </summary> |
41 |
protected Process _installerProcess; |
42 |
|
43 |
private ILogger _logWriter; |
44 |
private readonly Task _taskWorker; |
45 |
private CancellationToken _cancelToken; |
46 |
private readonly CancellationTokenSource _cancelTokenSource; |
47 |
private readonly SynchronizationContext _syncContext; |
48 |
private readonly string _appReferenceAssembly; |
49 |
|
50 |
private bool _doInitialCheck; |
51 |
private bool _forceInitialCheck; |
52 |
|
53 |
private readonly EventWaitHandle _exitHandle; |
54 |
private readonly EventWaitHandle _loopingHandle; |
55 |
private TimeSpan _checkFrequency; |
56 |
private string _tmpDownloadFilePath; |
57 |
private string _downloadTempFileName; |
58 |
private AppCastItem _itemBeingDownloaded; |
59 |
private bool _hasAttemptedFileRedownload; |
60 |
private UpdateInfo _latestDownloadedUpdateInfo; |
61 |
private IUIFactory _uiFactory; |
62 |
private bool _disposed; |
63 |
private Configuration _configuration; |
64 |
|
65 |
#endregion |
66 |
|
67 |
#region Constructors |
68 |
|
69 |
/// <summary> |
70 |
/// ctor which needs the appcast url |
71 |
/// </summary> |
72 |
/// <param name="appcastUrl">the URL of the appcast file</param> |
73 |
/// <param name="signatureVerifier">the object that will verify your appcast signatures.</param> |
74 |
public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier) |
75 |
: this(appcastUrl, signatureVerifier, null) |
76 |
{ } |
77 |
|
78 |
/// <summary> |
79 |
/// ctor which needs the appcast url and a referenceassembly |
80 |
/// </summary> |
81 |
/// <param name="appcastUrl">the URL of the appcast file</param> |
82 |
/// <param name="signatureVerifier">the object that will verify your appcast signatures.</param> |
83 |
/// <param name="referenceAssembly">the name of the assembly to use for comparison when checking update versions</param> |
84 |
public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly) |
85 |
: this(appcastUrl, signatureVerifier, referenceAssembly, null) |
86 |
{ } |
87 |
|
88 |
/// <summary> |
89 |
/// ctor which needs the appcast url and a referenceassembly |
90 |
/// </summary> |
91 |
/// <param name="appcastUrl">the URL of the appcast file</param> |
92 |
/// <param name="signatureVerifier">the object that will verify your appcast signatures.</param> |
93 |
/// <param name="referenceAssembly">the name of the assembly to use for comparison when checking update versions</param> |
94 |
/// <param name="factory">a UI factory to use in place of the default UI</param> |
95 |
public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly, IUIFactory factory) |
96 |
{ |
97 |
_latestDownloadedUpdateInfo = null; |
98 |
_hasAttemptedFileRedownload = false; |
99 |
UIFactory = factory; |
100 |
SignatureVerifier = signatureVerifier; |
101 |
// Syncronization Context |
102 |
_syncContext = SynchronizationContext.Current; |
103 |
if (_syncContext == null) |
104 |
{ |
105 |
_syncContext = new SynchronizationContext(); |
106 |
} |
107 |
// init UI |
108 |
UIFactory?.Init(); |
109 |
_appReferenceAssembly = null; |
110 |
// set the reference assembly |
111 |
if (referenceAssembly != null) |
112 |
{ |
113 |
_appReferenceAssembly = referenceAssembly; |
114 |
LogWriter.PrintMessage("Checking the following file: " + _appReferenceAssembly); |
115 |
} |
116 |
|
117 |
// adjust the delegates |
118 |
_taskWorker = new Task(() => |
119 |
{ |
120 |
OnWorkerDoWork(null, null); |
121 |
}); |
122 |
_cancelTokenSource = new CancellationTokenSource(); |
123 |
_cancelToken = _cancelTokenSource.Token; |
124 |
|
125 |
// build the wait handle |
126 |
_exitHandle = new EventWaitHandle(false, EventResetMode.AutoReset); |
127 |
_loopingHandle = new EventWaitHandle(false, EventResetMode.ManualReset); |
128 |
|
129 |
// set the url |
130 |
AppCastUrl = appcastUrl; |
131 |
LogWriter.PrintMessage("Using the following url: {0}", AppCastUrl); |
132 |
UserInteractionMode = UserInteractionMode.NotSilent; |
133 |
TmpDownloadFilePath = ""; |
134 |
} |
135 |
|
136 |
#endregion |
137 |
|
138 |
#region Properties |
139 |
|
140 |
/// <summary> |
141 |
/// The security protocol used by NetSparkle. Setting this property will also set this |
142 |
/// for the current AppDomain of the caller. Needs to be set to |
143 |
/// SecurityProtocolType.Tls12 for some cases (such as when downloading from GitHub). |
144 |
/// </summary> |
145 |
public SecurityProtocolType SecurityProtocolType |
146 |
{ |
147 |
get |
148 |
{ |
149 |
return ServicePointManager.SecurityProtocol; |
150 |
} |
151 |
set |
152 |
{ |
153 |
ServicePointManager.SecurityProtocol = value; |
154 |
} |
155 |
} |
156 |
|
157 |
/// <summary> |
158 |
/// Set the user interaction mode for Sparkle to use when there is a valid update for the software |
159 |
/// </summary> |
160 |
public UserInteractionMode UserInteractionMode { get; set; } |
161 |
|
162 |
/// <summary> |
163 |
/// If set, downloads files to this path. If the folder doesn't already exist, creates |
164 |
/// the folder at download time (and not before). |
165 |
/// Note that this variable is a path, not a full file name. |
166 |
/// </summary> |
167 |
public string TmpDownloadFilePath |
168 |
{ |
169 |
get { return _tmpDownloadFilePath; } |
170 |
set { _tmpDownloadFilePath = value?.Trim(); } |
171 |
} |
172 |
|
173 |
/// <summary> |
174 |
/// Defines if the application needs to be relaunched after executing the downloaded installer |
175 |
/// </summary> |
176 |
public bool RelaunchAfterUpdate { get; set; } |
177 |
|
178 |
/// <summary> |
179 |
/// Run the downloaded installer with these arguments |
180 |
/// </summary> |
181 |
public string CustomInstallerArguments { get; set; } |
182 |
|
183 |
/// <summary> |
184 |
/// Function that is called asynchronously to clean up old installers that have been |
185 |
/// downloaded with SilentModeTypes.DownloadNoInstall or SilentModeTypes.DownloadAndInstall. |
186 |
/// </summary> |
187 |
public Action ClearOldInstallers { get; set; } |
188 |
|
189 |
/// <summary> |
190 |
/// Whether or not the update loop is running |
191 |
/// </summary> |
192 |
public bool IsUpdateLoopRunning |
193 |
{ |
194 |
get |
195 |
{ |
196 |
return _loopingHandle.WaitOne(0); |
197 |
} |
198 |
} |
199 |
|
200 |
/// <summary> |
201 |
/// Factory for creating UI elements like progress window, etc. |
202 |
/// </summary> |
203 |
public IUIFactory UIFactory |
204 |
{ |
205 |
get { return _uiFactory; } |
206 |
set { _uiFactory = value; _uiFactory?.Init(); } |
207 |
} |
208 |
|
209 |
/// <summary> |
210 |
/// The user interface window that shows the release notes and |
211 |
/// asks the user to skip, remind me later, or update |
212 |
/// </summary> |
213 |
private IUpdateAvailable UpdateAvailableWindow { get; set; } |
214 |
|
215 |
/// <summary> |
216 |
/// The user interface window that shows a download progress bar, |
217 |
/// and then asks to install and relaunch the application |
218 |
/// </summary> |
219 |
private IDownloadProgress ProgressWindow { get; set; } |
220 |
|
221 |
/// <summary> |
222 |
/// The user interface window that shows the 'Checking for Updates...' |
223 |
/// form. |
224 |
/// </summary> |
225 |
private ICheckingForUpdates CheckingForUpdatesWindow { get; set; } |
226 |
|
227 |
/// <summary> |
228 |
/// The NetSparkle configuration object for the current assembly. |
229 |
/// </summary> |
230 |
public Configuration Configuration |
231 |
{ |
232 |
get |
233 |
{ |
234 |
if (_configuration == null) |
235 |
{ |
236 |
var assembly = new AssemblyReflectionAccessor(_appReferenceAssembly); |
237 |
#if NETSTANDARD |
238 |
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
239 |
{ |
240 |
_configuration = new RegistryConfiguration(assembly); |
241 |
} |
242 |
else |
243 |
{ |
244 |
_configuration = new JSONConfiguration(assembly); |
245 |
} |
246 |
#else |
247 |
_configuration = new RegistryConfiguration(assembly); |
248 |
#endif |
249 |
|
250 |
assembly = null; |
251 |
} |
252 |
return _configuration; |
253 |
} |
254 |
set { _configuration = value; } |
255 |
} |
256 |
|
257 |
/// <summary> |
258 |
/// The object that verifies signatures (DSA or otherwise) of downloaded items |
259 |
/// </summary> |
260 |
public ISignatureVerifier SignatureVerifier { get; set; } |
261 |
|
262 |
/// <summary> |
263 |
/// Gets or sets the appcast URL |
264 |
/// </summary> |
265 |
public string AppCastUrl { get; set; } |
266 |
|
267 |
/// <summary> |
268 |
/// Specifies if you want to use the notification toast |
269 |
/// </summary> |
270 |
public bool UseNotificationToast { get; set; } |
271 |
|
272 |
/// <summary> |
273 |
/// WinForms/WPF only. |
274 |
/// If true, tries to run UI code on the main thread using <see cref="SynchronizationContext"/>. |
275 |
/// Must be set to true if using NetSparkleUpdater from Avalonia. |
276 |
/// </summary> |
277 |
public bool ShowsUIOnMainThread { get; set; } |
278 |
|
279 |
/// <summary> |
280 |
/// Object that handles any diagnostic messages for NetSparkle. |
281 |
/// If you want to use your own class for this, you should just |
282 |
/// need to override <see cref="LogWriter.PrintMessage"/> in your own class. |
283 |
/// Make sure to set this object before calling <see cref="StartLoop(bool)"/> to guarantee |
284 |
/// that all messages will get sent to the right place! |
285 |
/// </summary> |
286 |
public ILogger LogWriter |
287 |
{ |
288 |
get |
289 |
{ |
290 |
if (_logWriter == null) |
291 |
{ |
292 |
_logWriter = new LogWriter(); |
293 |
} |
294 |
return _logWriter; |
295 |
} |
296 |
set |
297 |
{ |
298 |
_logWriter = value; |
299 |
} |
300 |
} |
301 |
|
302 |
/// <summary> |
303 |
/// Whether or not to check with the online server to verify download |
304 |
/// file names. |
305 |
/// </summary> |
306 |
public bool CheckServerFileName { get; set; } = true; |
307 |
|
308 |
/// <summary> |
309 |
/// Returns the latest appcast items to the caller. Might be null. |
310 |
/// </summary> |
311 |
public List<AppCastItem> LatestAppCastItems |
312 |
{ |
313 |
get |
314 |
{ |
315 |
return _latestDownloadedUpdateInfo?.Updates; |
316 |
} |
317 |
} |
318 |
|
319 |
/// <summary> |
320 |
/// Loops through all of the most recently grabbed app cast items |
321 |
/// and checks if any of them are marked as critical |
322 |
/// </summary> |
323 |
public bool UpdateMarkedCritical |
324 |
{ |
325 |
get |
326 |
{ |
327 |
if (LatestAppCastItems != null) |
328 |
{ |
329 |
foreach (AppCastItem item in LatestAppCastItems) |
330 |
{ |
331 |
if (item.IsCriticalUpdate) |
332 |
{ |
333 |
return true; |
334 |
} |
335 |
} |
336 |
} |
337 |
return false; |
338 |
} |
339 |
} |
340 |
|
341 |
/// <summary> |
342 |
/// The object responsable for downloading update files for your application |
343 |
/// </summary> |
344 |
public IUpdateDownloader UpdateDownloader { get; set; } |
345 |
|
346 |
/// <summary> |
347 |
/// The object responsible for downloading app cast and app cast signature |
348 |
/// information for your application |
349 |
/// </summary> |
350 |
public IAppCastDataDownloader AppCastDataDownloader { get; set; } |
351 |
|
352 |
/// <summary> |
353 |
/// The object responsible for parsing app cast information and checking to |
354 |
/// see if any updates are available in a given app cast |
355 |
/// </summary> |
356 |
public IAppCastHandler AppCastHandler { get; set; } |
357 |
|
358 |
#endregion |
359 |
|
360 |
/// <summary> |
361 |
/// Starts a NetSparkle background loop to check for updates every 24 hours. |
362 |
/// <para>You should only call this function when your app is initialized and shows its main window.</para> |
363 |
/// </summary> |
364 |
/// <param name="doInitialCheck">whether the first check should happen before or after the first interval</param> |
365 |
public void StartLoop(bool doInitialCheck) |
366 |
{ |
367 |
StartLoop(doInitialCheck, false); |
368 |
} |
369 |
|
370 |
/// <summary> |
371 |
/// Starts a NetSparkle background loop to check for updates on a given interval. |
372 |
/// <para>You should only call this function when your app is initialized and shows its main window.</para> |
373 |
/// </summary> |
374 |
/// <param name="doInitialCheck">whether the first check should happen before or after the first interval</param> |
375 |
/// <param name="checkFrequency">the interval to wait between update checks</param> |
376 |
public void StartLoop(bool doInitialCheck, TimeSpan checkFrequency) |
377 |
{ |
378 |
StartLoop(doInitialCheck, false, checkFrequency); |
379 |
} |
380 |
|
381 |
/// <summary> |
382 |
/// Starts a NetSparkle background loop to check for updates every 24 hours. |
383 |
/// <para>You should only call this function when your app is initialized and shows its main window.</para> |
384 |
/// </summary> |
385 |
/// <param name="doInitialCheck">whether the first check should happen before or after the first interval</param> |
386 |
/// <param name="forceInitialCheck">if <paramref name="doInitialCheck"/> is true, whether the first check |
387 |
/// should happen even if the last check was less than 24 hours ago</param> |
388 |
public void StartLoop(bool doInitialCheck, bool forceInitialCheck) |
389 |
{ |
390 |
StartLoop(doInitialCheck, forceInitialCheck, TimeSpan.FromHours(24)); |
391 |
} |
392 |
|
393 |
/// <summary> |
394 |
/// Starts a NetSparkle background loop to check for updates on a given interval. |
395 |
/// <para>You should only call this function when your app is initialized and shows its main window.</para> |
396 |
/// </summary> |
397 |
/// <param name="doInitialCheck">whether the first check should happen before or after the first period</param> |
398 |
/// <param name="forceInitialCheck">if <paramref name="doInitialCheck"/> is true, whether the first check |
399 |
/// should happen even if the last check was within the last <paramref name="checkFrequency"/> interval</param> |
400 |
/// <param name="checkFrequency">the interval to wait between update checks</param> |
401 |
public async void StartLoop(bool doInitialCheck, bool forceInitialCheck, TimeSpan checkFrequency) |
402 |
{ |
403 |
if (ClearOldInstallers != null) |
404 |
{ |
405 |
try |
406 |
{ |
407 |
await Task.Run(ClearOldInstallers); |
408 |
} |
409 |
catch (Exception e) |
410 |
{ |
411 |
LogWriter.PrintMessage("ClearOldInstallers threw an exception: {0}", e.Message); |
412 |
} |
413 |
} |
414 |
// first set the event handle |
415 |
_loopingHandle.Set(); |
416 |
|
417 |
// Start the helper thread as a background worker |
418 |
|
419 |
// store info |
420 |
_doInitialCheck = doInitialCheck; |
421 |
_forceInitialCheck = forceInitialCheck; |
422 |
_checkFrequency = checkFrequency; |
423 |
|
424 |
LogWriter.PrintMessage("Starting background worker"); |
425 |
|
426 |
// start the work |
427 |
//var scheduler = TaskScheduler.FromCurrentSynchronizationContext(); |
428 |
//_taskWorker.Start(scheduler); |
429 |
// don't allow starting the task 2x |
430 |
if (_taskWorker.IsCompleted == false && _taskWorker.Status != TaskStatus.Running && |
431 |
_taskWorker.Status != TaskStatus.WaitingToRun && _taskWorker.Status != TaskStatus.WaitingForActivation) |
432 |
{ |
433 |
_taskWorker.Start(); |
434 |
} |
435 |
} |
436 |
|
437 |
/// <summary> |
438 |
/// Stops the Sparkle background loop. Called automatically by <see cref="Dispose()"/>. |
439 |
/// </summary> |
440 |
public void StopLoop() |
441 |
{ |
442 |
// ensure the work will finished |
443 |
_exitHandle.Set(); |
444 |
} |
445 |
|
446 |
/// <summary> |
447 |
/// Finalizer |
448 |
/// </summary> |
449 |
~SparkleUpdater() |
450 |
{ |
451 |
Dispose(false); |
452 |
} |
453 |
|
454 |
#region IDisposable |
455 |
|
456 |
/// <summary> |
457 |
/// Inherited from IDisposable. Stops all background activities. |
458 |
/// </summary> |
459 |
public void Dispose() |
460 |
{ |
461 |
Dispose(true); |
462 |
GC.SuppressFinalize(this); |
463 |
} |
464 |
|
465 |
/// <summary> |
466 |
/// Dispose of managed and unmanaged resources |
467 |
/// </summary> |
468 |
/// <param name="disposing"></param> |
469 |
protected virtual void Dispose(bool disposing) |
470 |
{ |
471 |
if (!_disposed) |
472 |
{ |
473 |
if (disposing) |
474 |
{ |
475 |
// Dispose managed resources. |
476 |
StopLoop(); |
477 |
UnregisterEvents(); |
478 |
_cancelTokenSource?.Dispose(); |
479 |
_exitHandle?.Dispose(); |
480 |
_loopingHandle?.Dispose(); |
481 |
UpdateDownloader?.Dispose(); |
482 |
_installerProcess?.Dispose(); |
483 |
} |
484 |
// There are no unmanaged resources to release, but |
485 |
// if we add them, they need to be released here. |
486 |
} |
487 |
_disposed = true; |
488 |
} |
489 |
|
490 |
/// <summary> |
491 |
/// Unregisters events so that we don't have multiple items updating |
492 |
/// </summary> |
493 |
private void UnregisterEvents() |
494 |
{ |
495 |
_cancelTokenSource.Cancel(); |
496 |
|
497 |
CleanUpUpdateDownloader(); |
498 |
|
499 |
if (UpdateAvailableWindow != null) |
500 |
{ |
501 |
UpdateAvailableWindow.UserResponded -= OnUserWindowUserResponded; |
502 |
UpdateAvailableWindow = null; |
503 |
} |
504 |
|
505 |
if (ProgressWindow != null) |
506 |
{ |
507 |
ProgressWindow.DownloadProcessCompleted -= ProgressWindowCompleted; |
508 |
ProgressWindow = null; |
509 |
} |
510 |
} |
511 |
|
512 |
#endregion |
513 |
|
514 |
/// <summary> |
515 |
/// This method checks if an update is required. During this process the appcast |
516 |
/// will be downloaded and checked against the reference assembly. Ensure that |
517 |
/// the calling process has read access to the reference assembly. |
518 |
/// This method is also called from the background loops. |
519 |
/// </summary> |
520 |
/// <param name="config">the NetSparkle configuration for the reference assembly</param> |
521 |
/// <returns><see cref="UpdateInfo"/> with information on whether there is an update available or not.</returns> |
522 |
protected async Task<UpdateInfo> GetUpdateStatus(Configuration config) |
523 |
{ |
524 |
List<AppCastItem> updates = null; |
525 |
// report |
526 |
LogWriter.PrintMessage("Downloading and checking appcast"); |
527 |
|
528 |
// init the appcast |
529 |
if (AppCastDataDownloader == null) |
530 |
{ |
531 |
AppCastDataDownloader = new WebRequestAppCastDataDownloader(); |
532 |
} |
533 |
if (AppCastHandler == null) |
534 |
{ |
535 |
AppCastHandler = new XMLAppCast(); |
536 |
} |
537 |
AppCastHandler.SetupAppCastHandler(AppCastDataDownloader, AppCastUrl, config, SignatureVerifier, LogWriter); |
538 |
// check if any updates are available |
539 |
try |
540 |
{ |
541 |
await Task.Factory.StartNew(() => |
542 |
{ |
543 |
LogWriter.PrintMessage("About to start downloading the app cast..."); |
544 |
if (AppCastHandler.DownloadAndParse()) |
545 |
{ |
546 |
LogWriter.PrintMessage("App cast successfully downloaded and parsed. Getting available updates..."); |
547 |
updates = AppCastHandler.GetAvailableUpdates(); |
548 |
} |
549 |
}); |
550 |
} |
551 |
catch (Exception e) |
552 |
{ |
553 |
LogWriter.PrintMessage("Couldn't read/parse the app cast: {0}", e.Message); |
554 |
updates = null; |
555 |
} |
556 |
|
557 |
if (updates == null) |
558 |
{ |
559 |
LogWriter.PrintMessage("No version information in app cast found"); |
560 |
return new UpdateInfo(UpdateStatus.CouldNotDetermine); |
561 |
} |
562 |
|
563 |
// set the last check time |
564 |
LogWriter.PrintMessage("Touch the last check timestamp"); |
565 |
config.TouchCheckTime(); |
566 |
|
567 |
// check if the version will be the same then the installed version |
568 |
if (updates.Count == 0) |
569 |
{ |
570 |
LogWriter.PrintMessage("Installed version is valid, no update needed ({0})", config.InstalledVersion); |
571 |
return new UpdateInfo(UpdateStatus.UpdateNotAvailable); |
572 |
} |
573 |
LogWriter.PrintMessage("Latest version on the server is {0}", updates[0].Version); |
574 |
|
575 |
// check if the available update has to be skipped |
576 |
if (updates[0].Version.Equals(config.LastVersionSkipped)) |
577 |
{ |
578 |
LogWriter.PrintMessage("Latest update has to be skipped (user decided to skip version {0})", config.LastVersionSkipped); |
579 |
return new UpdateInfo(UpdateStatus.UserSkipped); |
580 |
} |
581 |
|
582 |
// ok we need an update |
583 |
return new UpdateInfo(UpdateStatus.UpdateAvailable, updates); |
584 |
} |
585 |
|
586 |
/// <summary> |
587 |
/// Shows the update needed UI with the given set of updates. |
588 |
/// </summary> |
589 |
/// <param name="updates">updates to show UI for</param> |
590 |
/// <param name="isUpdateAlreadyDownloaded">If true, make sure UI text shows that the user is about to install the file instead of download it.</param> |
591 |
public void ShowUpdateNeededUI(List<AppCastItem> updates, bool isUpdateAlreadyDownloaded = false) |
592 |
{ |
593 |
if (updates != null) |
594 |
{ |
595 |
if (UseNotificationToast && (bool)UIFactory?.CanShowToastMessages()) |
596 |
{ |
597 |
UIFactory?.ShowToast(updates, OnToastClick); |
598 |
} |
599 |
else |
600 |
{ |
601 |
ShowUpdateAvailableWindow(updates, isUpdateAlreadyDownloaded); |
602 |
} |
603 |
} |
604 |
} |
605 |
|
606 |
/// <summary> |
607 |
/// Shows the update UI with the latest downloaded update information. |
608 |
/// </summary> |
609 |
/// <param name="isUpdateAlreadyDownloaded">If true, make sure UI text shows that the user is about to install the file instead of download it.</param> |
610 |
public void ShowUpdateNeededUI(bool isUpdateAlreadyDownloaded = false) |
611 |
{ |
612 |
ShowUpdateNeededUI(_latestDownloadedUpdateInfo?.Updates, isUpdateAlreadyDownloaded); |
613 |
} |
614 |
|
615 |
private void OnToastClick(List<AppCastItem> updates) |
616 |
{ |
617 |
ShowUpdateAvailableWindow(updates); |
618 |
} |
619 |
|
620 |
private void ShowUpdateAvailableWindow(List<AppCastItem> updates, bool isUpdateAlreadyDownloaded = false) |
621 |
{ |
622 |
if (UpdateAvailableWindow != null) |
623 |
{ |
624 |
// close old window |
625 |
if (ShowsUIOnMainThread) |
626 |
{ |
627 |
_syncContext.Post((state) => |
628 |
{ |
629 |
UpdateAvailableWindow.Close(); |
630 |
UpdateAvailableWindow = null; |
631 |
}, null); |
632 |
} |
633 |
else |
634 |
{ |
635 |
UpdateAvailableWindow.Close(); |
636 |
UpdateAvailableWindow = null; |
637 |
} |
638 |
} |
639 |
|
640 |
// create the form |
641 |
Thread thread = new Thread(() => |
642 |
{ |
643 |
try |
644 |
{ |
645 |
// define action |
646 |
Action<object> showSparkleUI = (state) => |
647 |
{ |
648 |
UpdateAvailableWindow = UIFactory?.CreateUpdateAvailableWindow(this, updates, isUpdateAlreadyDownloaded); |
649 |
|
650 |
if (UpdateAvailableWindow != null) |
651 |
{ |
652 |
|
653 |
// clear if already set. |
654 |
UpdateAvailableWindow.UserResponded += OnUserWindowUserResponded; |
655 |
UpdateAvailableWindow.Show(ShowsUIOnMainThread); |
656 |
} |
657 |
}; |
658 |
|
659 |
// call action |
660 |
if (ShowsUIOnMainThread) |
661 |
{ |
662 |
_syncContext.Post((state) => showSparkleUI(state), null); |
663 |
} |
664 |
else |
665 |
{ |
666 |
showSparkleUI(null); |
667 |
} |
668 |
} |
669 |
catch (Exception e) |
670 |
{ |
671 |
LogWriter.PrintMessage("Error showing sparkle form: {0}", e.Message); |
672 |
} |
673 |
}); |
674 |
#if NETFRAMEWORK |
675 |
thread.SetApartmentState(ApartmentState.STA); |
676 |
#else |
677 |
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
678 |
{ |
679 |
thread.SetApartmentState(ApartmentState.STA); // only supported on Windows |
680 |
} |
681 |
#endif |
682 |
thread.Start(); |
683 |
} |
684 |
|
685 |
/// <summary> |
686 |
/// Get the download path for a given app cast item. |
687 |
/// If any directories need to be created, this function |
688 |
/// will create those directories. |
689 |
/// </summary> |
690 |
/// <param name="item">The item that you want to generate a download path for</param> |
691 |
/// <returns>The download path for an app cast item if item is not null and has valid download link |
692 |
/// Otherwise returns null.</returns> |
693 |
public async Task<string> GetDownloadPathForAppCastItem(AppCastItem item) |
694 |
{ |
695 |
if (item != null && item.DownloadLink != null) |
696 |
{ |
697 |
string filename = string.Empty; |
698 |
|
699 |
// default to using the server's file name as the download file name |
700 |
if (CheckServerFileName && UpdateDownloader != null) |
701 |
{ |
702 |
try |
703 |
{ |
704 |
filename = await UpdateDownloader.RetrieveDestinationFileNameAsync(item); |
705 |
} |
706 |
catch (Exception) |
707 |
{ |
708 |
// ignore |
709 |
} |
710 |
} |
711 |
|
712 |
if (string.IsNullOrEmpty(filename)) |
713 |
{ |
714 |
// attempt to get download file name based on download link |
715 |
try |
716 |
{ |
717 |
filename = Path.GetFileName(new Uri(item.DownloadLink).LocalPath); |
718 |
} |
719 |
catch (UriFormatException) |
720 |
{ |
721 |
// ignore |
722 |
} |
723 |
} |
724 |
|
725 |
if (!string.IsNullOrEmpty(filename)) |
726 |
{ |
727 |
string tmpPath = string.IsNullOrEmpty(TmpDownloadFilePath) ? Path.GetTempPath() : TmpDownloadFilePath; |
728 |
|
729 |
// Creates all directories and subdirectories in the specific path unless they already exist. |
730 |
Directory.CreateDirectory(tmpPath); |
731 |
|
732 |
return Path.Combine(tmpPath, filename); |
733 |
} |
734 |
} |
735 |
return null; |
736 |
} |
737 |
|
738 |
/// <summary> |
739 |
/// Starts the download process by grabbing the download path for |
740 |
/// the app cast item (asynchronous so that it can get the server's |
741 |
/// download name in case there is a redirect; cancel this by setting |
742 |
/// CheckServerFileName to false), then beginning the download |
743 |
/// process if the download file doesn't already exist |
744 |
/// </summary> |
745 |
/// <param name="item">the appcast item to download</param> |
746 |
public async Task InitAndBeginDownload(AppCastItem item) |
747 |
{ |
748 |
if (UpdateDownloader != null && UpdateDownloader.IsDownloading) |
749 |
{ |
750 |
return; // file is already downloading, don't do anything! |
751 |
} |
752 |
LogWriter.PrintMessage("Preparing to download {0}", item.DownloadLink); |
753 |
_itemBeingDownloaded = item; |
754 |
CreateUpdateDownloaderIfNeeded(); |
755 |
_downloadTempFileName = await GetDownloadPathForAppCastItem(item); |
756 |
// Make sure the file doesn't already exist on disk. If it's already downloaded and the |
757 |
// DSA signature checks out, don't redownload the file! |
758 |
bool needsToDownload = true; |
759 |
if (File.Exists(_downloadTempFileName)) |
760 |
{ |
761 |
ValidationResult result = SignatureVerifier.VerifySignatureOfFile(item.DownloadSignature, _downloadTempFileName); |
762 |
if (result == ValidationResult.Valid) |
763 |
{ |
764 |
LogWriter.PrintMessage("File is already downloaded"); |
765 |
// We already have the file! Don't redownload it! |
766 |
needsToDownload = false; |
767 |
// Still need to set up the ProgressWindow for non-silent downloads, though, |
768 |
// so that the user can actually perform the install |
769 |
CreateAndShowProgressWindow(item, true); |
770 |
CallFuncConsideringUIThreads(() => { DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName); }); |
771 |
bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall; |
772 |
if (shouldInstallAndRelaunch) |
773 |
{ |
774 |
CallFuncConsideringUIThreads(() => { ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); }); |
775 |
} |
776 |
} |
777 |
else if (!_hasAttemptedFileRedownload) |
778 |
{ |
779 |
// The file exists but it either has a bad DSA signature or SecurityMode is set to Unsafe. |
780 |
// Redownload it! |
781 |
_hasAttemptedFileRedownload = true; |
782 |
LogWriter.PrintMessage("File is corrupt or DSA signature is Unchecked; deleting file and redownloading..."); |
783 |
try |
784 |
{ |
785 |
File.Delete(_downloadTempFileName); |
786 |
} |
787 |
catch (Exception e) |
788 |
{ |
789 |
LogWriter.PrintMessage("Hm, seems as though we couldn't delete the temporary file even though it is apparently corrupt. {0}", |
790 |
e.Message); |
791 |
// we won't be able to download anyway since we couldn't delete the file :( we'll try next time the |
792 |
// update loop goes around. |
793 |
needsToDownload = false; |
794 |
CallFuncConsideringUIThreads(() => |
795 |
{ |
796 |
DownloadHadError?.Invoke(item, _downloadTempFileName, |
797 |
new Exception(string.Format("Unable to delete old download at {0}", _downloadTempFileName))); |
798 |
}); |
799 |
} |
800 |
} |
801 |
else |
802 |
{ |
803 |
CallFuncConsideringUIThreads(() => { DownloadedFileIsCorrupt?.Invoke(item, _downloadTempFileName); }); |
804 |
} |
805 |
} |
806 |
if (needsToDownload) |
807 |
{ |
808 |
// remove any old event handlers so we don't fire 2x |
809 |
UpdateDownloader.DownloadProgressChanged -= OnDownloadProgressChanged; |
810 |
UpdateDownloader.DownloadFileCompleted -= OnDownloadFinished; |
811 |
|
812 |
CreateAndShowProgressWindow(item, false); |
813 |
UpdateDownloader.DownloadProgressChanged += OnDownloadProgressChanged; |
814 |
UpdateDownloader.DownloadFileCompleted += OnDownloadFinished; |
815 |
|
816 |
Uri url = Utilities.GetAbsoluteURL(item.DownloadLink, AppCastUrl); |
817 |
LogWriter.PrintMessage("Starting to download {0} to {1}", item.DownloadLink, _downloadTempFileName); |
818 |
UpdateDownloader.StartFileDownload(url, _downloadTempFileName); |
819 |
CallFuncConsideringUIThreads(() => { DownloadStarted?.Invoke(item, _downloadTempFileName); }); |
820 |
} |
821 |
} |
822 |
|
823 |
private void OnDownloadProgressChanged(object sender, ItemDownloadProgressEventArgs args) |
824 |
{ |
825 |
CallFuncConsideringUIThreads(() => |
826 |
{ |
827 |
DownloadMadeProgress?.Invoke(sender, _itemBeingDownloaded, args); |
828 |
}); |
829 |
} |
830 |
|
831 |
private void CleanUpUpdateDownloader() |
832 |
{ |
833 |
if (UpdateDownloader != null) |
834 |
{ |
835 |
if (ProgressWindow != null) |
836 |
{ |
837 |
UpdateDownloader.DownloadProgressChanged -= ProgressWindow.OnDownloadProgressChanged; |
838 |
} |
839 |
UpdateDownloader.DownloadProgressChanged -= OnDownloadProgressChanged; |
840 |
UpdateDownloader.DownloadFileCompleted -= OnDownloadFinished; |
841 |
UpdateDownloader?.Dispose(); |
842 |
UpdateDownloader = null; |
843 |
} |
844 |
} |
845 |
|
846 |
private void CreateUpdateDownloaderIfNeeded() |
847 |
{ |
848 |
if (UpdateDownloader == null) |
849 |
{ |
850 |
UpdateDownloader = new WebClientFileDownloader(); |
851 |
} |
852 |
} |
853 |
|
854 |
private void CreateAndShowProgressWindow(AppCastItem castItem, bool shouldShowAsDownloadedAlready) |
855 |
{ |
856 |
if (ProgressWindow != null) |
857 |
{ |
858 |
ProgressWindow.DownloadProcessCompleted -= ProgressWindowCompleted; |
859 |
UpdateDownloader.DownloadProgressChanged -= ProgressWindow.OnDownloadProgressChanged; |
860 |
ProgressWindow = null; |
861 |
} |
862 |
if (ProgressWindow == null && UIFactory != null && !IsDownloadingSilently()) |
863 |
{ |
864 |
if (!IsDownloadingSilently() && ProgressWindow == null) |
865 |
{ |
866 |
// create the form |
867 |
Action<object> showSparkleDownloadUI = (state) => |
868 |
{ |
869 |
ProgressWindow = UIFactory?.CreateProgressWindow(castItem); |
870 |
if (ProgressWindow != null) |
871 |
{ |
872 |
ProgressWindow.DownloadProcessCompleted += ProgressWindowCompleted; |
873 |
UpdateDownloader.DownloadProgressChanged += ProgressWindow.OnDownloadProgressChanged; |
874 |
if (shouldShowAsDownloadedAlready) |
875 |
{ |
876 |
ProgressWindow?.FinishedDownloadingFile(true); |
877 |
_syncContext.Post((state2) => OnDownloadFinished(null, new AsyncCompletedEventArgs(null, false, null)), null); |
878 |
} |
879 |
} |
880 |
}; |
881 |
Thread thread = new Thread(() => |
882 |
{ |
883 |
// call action |
884 |
if (ShowsUIOnMainThread) |
885 |
{ |
886 |
_syncContext.Post((state) => |
887 |
{ |
888 |
showSparkleDownloadUI(null); |
889 |
ProgressWindow?.Show(ShowsUIOnMainThread); |
890 |
}, null); |
891 |
} |
892 |
else |
893 |
{ |
894 |
showSparkleDownloadUI(null); |
895 |
ProgressWindow?.Show(ShowsUIOnMainThread); |
896 |
} |
897 |
}); |
898 |
#if NETFRAMEWORK |
899 |
thread.SetApartmentState(ApartmentState.STA); |
900 |
#else |
901 |
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
902 |
{ |
903 |
thread.SetApartmentState(ApartmentState.STA); // only supported on Windows |
904 |
} |
905 |
#endif |
906 |
thread.Start(); |
907 |
} |
908 |
} |
909 |
} |
910 |
|
911 |
private async void ProgressWindowCompleted(object sender, DownloadInstallEventArgs args) |
912 |
{ |
913 |
if (args.ShouldInstall) |
914 |
{ |
915 |
ProgressWindow?.SetDownloadAndInstallButtonEnabled(false); // disable while we ask if we can close up the software |
916 |
if (await AskApplicationToSafelyCloseUp()) |
917 |
{ |
918 |
ProgressWindow?.Close(); |
919 |
await RunDownloadedInstaller(_downloadTempFileName); |
920 |
} |
921 |
else |
922 |
{ |
923 |
ProgressWindow?.SetDownloadAndInstallButtonEnabled(true); |
924 |
} |
925 |
} |
926 |
else |
927 |
{ |
928 |
CancelFileDownload(); |
929 |
ProgressWindow?.Close(); |
930 |
} |
931 |
} |
932 |
|
933 |
/// <summary> |
934 |
/// Called when the installer is downloaded |
935 |
/// </summary> |
936 |
/// <param name="sender">not used.</param> |
937 |
/// <param name="e">used to determine if the download was successful.</param> |
938 |
private void OnDownloadFinished(object sender, AsyncCompletedEventArgs e) |
939 |
{ |
940 |
bool shouldShowUIItems = !IsDownloadingSilently(); |
941 |
|
942 |
if (e.Cancelled) |
943 |
{ |
944 |
DownloadCanceled?.Invoke(_itemBeingDownloaded, _downloadTempFileName); |
945 |
_hasAttemptedFileRedownload = false; |
946 |
if (File.Exists(_downloadTempFileName)) |
947 |
{ |
948 |
File.Delete(_downloadTempFileName); |
949 |
} |
950 |
LogWriter.PrintMessage("Download was canceled"); |
951 |
string errorMessage = "Download canceled"; |
952 |
if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage)) |
953 |
{ |
954 |
UIFactory?.ShowDownloadErrorMessage(errorMessage, AppCastUrl); |
955 |
} |
956 |
DownloadCanceled?.Invoke(_itemBeingDownloaded, _downloadTempFileName); |
957 |
return; |
958 |
} |
959 |
if (e.Error != null) |
960 |
{ |
961 |
DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, e.Error); |
962 |
// Clean temp files on error too |
963 |
if (File.Exists(_downloadTempFileName)) |
964 |
{ |
965 |
File.Delete(_downloadTempFileName); |
966 |
} |
967 |
LogWriter.PrintMessage("Error on download finished: {0}", e.Error.Message); |
968 |
if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(e.Error.Message)) |
969 |
{ |
970 |
UIFactory?.ShowDownloadErrorMessage(e.Error.Message, AppCastUrl); |
971 |
} |
972 |
DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(e.Error.Message)); |
973 |
return; |
974 |
} |
975 |
// test the item for signature |
976 |
var validationRes = ValidationResult.Invalid; |
977 |
if (!e.Cancelled && e.Error == null) |
978 |
{ |
979 |
LogWriter.PrintMessage("Fully downloaded file exists at {0}", _downloadTempFileName); |
980 |
|
981 |
LogWriter.PrintMessage("Performing signature check"); |
982 |
|
983 |
// get the assembly |
984 |
if (File.Exists(_downloadTempFileName)) |
985 |
{ |
986 |
// check if the file was downloaded successfully |
987 |
string absolutePath = Path.GetFullPath(_downloadTempFileName); |
988 |
if (!File.Exists(absolutePath)) |
989 |
{ |
990 |
var message = "File not found even though it was reported as downloading successfully!"; |
991 |
LogWriter.PrintMessage(message); |
992 |
DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(message)); |
993 |
} |
994 |
|
995 |
// check the signature |
996 |
validationRes = SignatureVerifier.VerifySignatureOfFile(_itemBeingDownloaded?.DownloadSignature, _downloadTempFileName); |
997 |
} |
998 |
} |
999 |
|
1000 |
bool isSignatureInvalid = validationRes == ValidationResult.Invalid; // if Unchecked, we accept download as valid |
1001 |
if (shouldShowUIItems) |
1002 |
{ |
1003 |
CallFuncConsideringUIThreads(() => { ProgressWindow?.FinishedDownloadingFile(!isSignatureInvalid); }); |
1004 |
} |
1005 |
// signature of file isn't valid so exit with error |
1006 |
if (isSignatureInvalid) |
1007 |
{ |
1008 |
LogWriter.PrintMessage("Invalid signature for downloaded file for app cast: {0}", _downloadTempFileName); |
1009 |
string errorMessage = "Downloaded file has invalid signature!"; |
1010 |
DownloadedFileIsCorrupt?.Invoke(_itemBeingDownloaded, _downloadTempFileName); |
1011 |
// Default to showing errors in the progress window. Only go to the UIFactory to show errors if necessary. |
1012 |
CallFuncConsideringUIThreads(() => |
1013 |
{ |
1014 |
if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage)) |
1015 |
{ |
1016 |
UIFactory?.ShowDownloadErrorMessage(errorMessage, AppCastUrl); |
1017 |
} |
1018 |
DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(errorMessage)); |
1019 |
}); |
1020 |
} |
1021 |
else |
1022 |
{ |
1023 |
LogWriter.PrintMessage("DSA Signature is valid. File successfully downloaded!"); |
1024 |
DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName); |
1025 |
bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall; |
1026 |
if (shouldInstallAndRelaunch) |
1027 |
{ |
1028 |
CallFuncConsideringUIThreads(() => { ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); }); |
1029 |
} |
1030 |
} |
1031 |
_itemBeingDownloaded = null; |
1032 |
} |
1033 |
|
1034 |
/// <summary> |
1035 |
/// Run the provided app cast item update regardless of what else is going on. |
1036 |
/// Note that a more up to date download may be taking place, so if you don't |
1037 |
/// want to run a potentially out-of-date installer, don't use this. This should |
1038 |
/// only be used if your user wants to update before another update has been |
1039 |
/// installed AND the file is already downloaded. |
1040 |
/// This function will verify that the file exists and that the DSA |
1041 |
/// signature is valid before running. It will also utilize the |
1042 |
/// PreparingToExit event to ensure that the application can close. |
1043 |
/// </summary> |
1044 |
/// <param name="item">AppCastItem to install</param> |
1045 |
/// <param name="installPath">Install path to the executable. If not provided, will ask the server for the download path.</param> |
1046 |
public async void InstallUpdate(AppCastItem item, string installPath = null) |
1047 |
{ |
1048 |
ProgressWindow?.SetDownloadAndInstallButtonEnabled(false); // disable while we ask if we can close up the software |
1049 |
if (await AskApplicationToSafelyCloseUp()) |
1050 |
{ |
1051 |
var path = installPath != null && File.Exists(installPath) ? installPath : await GetDownloadPathForAppCastItem(item); |
1052 |
if (File.Exists(path)) |
1053 |
{ |
1054 |
var result = SignatureVerifier.VerifySignatureOfFile(item.DownloadSignature, path); |
1055 |
if (result == ValidationResult.Valid || result == ValidationResult.Unchecked) |
1056 |
{ |
1057 |
await RunDownloadedInstaller(path); |
1058 |
} |
1059 |
} |
1060 |
} |
1061 |
ProgressWindow?.SetDownloadAndInstallButtonEnabled(true); |
1062 |
} |
1063 |
|
1064 |
/// <summary> |
1065 |
/// Checks to see |
1066 |
/// </summary> |
1067 |
/// <param name="item"></param> |
1068 |
/// <returns></returns> |
1069 |
public bool IsDownloadingItem(AppCastItem item) |
1070 |
{ |
1071 |
return _itemBeingDownloaded?.DownloadSignature == item.DownloadSignature; |
1072 |
} |
1073 |
|
1074 |
/// <summary> |
1075 |
/// True if the user has silent updates enabled; false otherwise. |
1076 |
/// </summary> |
1077 |
private bool IsDownloadingSilently() |
1078 |
{ |
1079 |
return UserInteractionMode != UserInteractionMode.NotSilent; |
1080 |
} |
1081 |
|
1082 |
/// <summary> |
1083 |
/// Checks to see if two extensions match (this is basically just a |
1084 |
/// convenient string comparison). Both extensions should include the |
1085 |
/// initial . (full-stop/period) in the extension. |
1086 |
/// </summary> |
1087 |
/// <param name="extension">first extension to check</param> |
1088 |
/// <param name="otherExtension">other extension to check</param> |
1089 |
/// <returns>true if the extensions match; false otherwise</returns> |
1090 |
protected bool DoExtensionsMatch(string extension, string otherExtension) |
1091 |
{ |
1092 |
return extension.Equals(otherExtension, StringComparison.CurrentCultureIgnoreCase); |
1093 |
} |
1094 |
|
1095 |
/// <summary> |
1096 |
/// Get the install command for the file at the given path. Figures out which |
1097 |
/// command to use based on the download file path's file extension. |
1098 |
/// Currently supports .exe, .msi, and .msp. |
1099 |
/// </summary> |
1100 |
/// <param name="downloadFilePath">Path to the downloaded update file</param> |
1101 |
/// <returns>the installer command if the file has one of the given |
1102 |
/// extensions; the initial downloadFilePath if not.</returns> |
1103 |
protected virtual string GetWindowsInstallerCommand(string downloadFilePath) |
1104 |
{ |
1105 |
string installerExt = Path.GetExtension(downloadFilePath); |
1106 |
if (DoExtensionsMatch(installerExt, ".exe")) |
1107 |
{ |
1108 |
return "\"" + downloadFilePath + "\""; |
1109 |
} |
1110 |
if (DoExtensionsMatch(installerExt, ".msi")) |
1111 |
{ |
1112 |
return "msiexec /i \"" + downloadFilePath + "\""; |
1113 |
} |
1114 |
if (DoExtensionsMatch(installerExt, ".msp")) |
1115 |
{ |
1116 |
return "msiexec /p \"" + downloadFilePath + "\""; |
1117 |
} |
1118 |
return downloadFilePath; |
1119 |
} |
1120 |
|
1121 |
/// <summary> |
1122 |
/// Get the install command for the file at the given path. Figures out which |
1123 |
/// command to use based on the download file path's file extension. |
1124 |
/// <para>Windows: currently supports .exe, .msi, and .msp.</para> |
1125 |
/// <para>macOS: currently supports .pkg, .dmg, and .zip.</para> |
1126 |
/// <para>Linux: currently supports .tar.gz, .deb, and .rpm.</para> |
1127 |
/// </summary> |
1128 |
/// <param name="downloadFilePath">Path to the downloaded update file</param> |
1129 |
/// <returns>the installer command if the file has one of the given |
1130 |
/// extensions; the initial downloadFilePath if not.</returns> |
1131 |
protected virtual string GetInstallerCommand(string downloadFilePath) |
1132 |
{ |
1133 |
// get the file type |
1134 |
#if NETFRAMEWORK |
1135 |
return GetWindowsInstallerCommand(downloadFilePath); |
1136 |
#else |
1137 |
string installerExt = Path.GetExtension(downloadFilePath); |
1138 |
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
1139 |
{ |
1140 |
return GetWindowsInstallerCommand(downloadFilePath); |
1141 |
} |
1142 |
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
1143 |
{ |
1144 |
if (DoExtensionsMatch(installerExt, ".pkg") || |
1145 |
DoExtensionsMatch(installerExt, ".dmg")) |
1146 |
{ |
1147 |
return "open \"" + downloadFilePath + "\""; |
1148 |
} |
1149 |
} |
1150 |
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
1151 |
{ |
1152 |
if (DoExtensionsMatch(installerExt, ".deb")) |
1153 |
{ |
1154 |
return "sudo dpkg -i \"" + downloadFilePath + "\""; |
1155 |
} |
1156 |
if (DoExtensionsMatch(installerExt, ".rpm")) |
1157 |
{ |
1158 |
return "sudo rpm -i \"" + downloadFilePath + "\""; |
1159 |
} |
1160 |
} |
1161 |
return downloadFilePath; |
1162 |
#endif |
1163 |
} |
1164 |
|
1165 |
private bool IsZipDownload(string downloadFilePath) |
1166 |
{ |
1167 |
#if NETCORE |
1168 |
string installerExt = Path.GetExtension(downloadFilePath); |
1169 |
bool isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); |
1170 |
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); |
1171 |
if ((isMacOS && DoExtensionsMatch(installerExt, ".zip")) || |
1172 |
(isLinux && downloadFilePath.EndsWith(".tar.gz"))) |
1173 |
{ |
1174 |
return true; |
1175 |
} |
1176 |
#endif |
1177 |
return false; |
1178 |
} |
1179 |
|
1180 |
/// <summary> |
1181 |
/// Updates the application via the file at the given path. Figures out which command needs |
1182 |
/// to be run, sets up the application so that it will start the downloaded file once the |
1183 |
/// main application stops, and then waits to start the downloaded update. |
1184 |
/// </summary> |
1185 |
/// <param name="downloadFilePath">path to the downloaded installer/updater</param> |
1186 |
/// <returns>the awaitable <see cref="Task"/> for the application quitting</returns> |
1187 |
protected virtual async Task RunDownloadedInstaller(string downloadFilePath) |
1188 |
{ |
1189 |
LogWriter.PrintMessage("Running downloaded installer"); |
1190 |
// get the commandline |
1191 |
string cmdLine = Environment.CommandLine; |
1192 |
string workingDir = Utilities.GetFullBaseDirectory(); |
1193 |
|
1194 |
// generate the batch file path |
1195 |
#if NETFRAMEWORK |
1196 |
bool isWindows = true; |
1197 |
bool isMacOS = false; |
1198 |
#else |
1199 |
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); |
1200 |
bool isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); |
1201 |
#endif |
1202 |
var extension = isWindows ? ".cmd" : ".sh"; |
1203 |
string batchFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + extension); |
1204 |
string installerCmd; |
1205 |
try |
1206 |
{ |
1207 |
installerCmd = GetInstallerCommand(downloadFilePath); |
1208 |
if (!string.IsNullOrEmpty(CustomInstallerArguments)) |
1209 |
{ |
1210 |
installerCmd += " " + CustomInstallerArguments; |
1211 |
} |
1212 |
} |
1213 |
catch (InvalidDataException) |
1214 |
{ |
1215 |
UIFactory?.ShowUnknownInstallerFormatMessage(downloadFilePath); |
1216 |
return; |
1217 |
} |
1218 |
|
1219 |
// generate the batch file |
1220 |
LogWriter.PrintMessage("Generating batch in {0}", Path.GetFullPath(batchFilePath)); |
1221 |
|
1222 |
string processID = Process.GetCurrentProcess().Id.ToString(); |
1223 |
|
1224 |
using (StreamWriter write = new StreamWriter(batchFilePath, false, new UTF8Encoding(false))) |
1225 |
{ |
1226 |
if (isWindows) |
1227 |
{ |
1228 |
write.WriteLine("@echo off"); |
1229 |
// We should wait until the host process has died before starting the installer. |
1230 |
// This way, any DLLs or other items can be replaced properly. |
1231 |
// Code from: http://stackoverflow.com/a/22559462/3938401 |
1232 |
string relaunchAfterUpdate = ""; |
1233 |
if (RelaunchAfterUpdate) |
1234 |
{ |
1235 |
relaunchAfterUpdate = $@" |
1236 |
cd {workingDir} |
1237 |
{cmdLine}"; |
1238 |
} |
1239 |
|
1240 |
string output = $@" |
1241 |
set /A counter=0 |
1242 |
setlocal ENABLEDELAYEDEXPANSION |
1243 |
:loop |
1244 |
set /A counter=!counter!+1 |
1245 |
if !counter! == 90 ( |
1246 |
goto :afterinstall |
1247 |
) |
1248 |
tasklist | findstr ""\<{processID}\>"" > nul |
1249 |
if not errorlevel 1 ( |
1250 |
timeout /t 1 > nul |
1251 |
goto :loop |
1252 |
) |
1253 |
:install |
1254 |
{installerCmd} |
1255 |
{relaunchAfterUpdate} |
1256 |
:afterinstall |
1257 |
endlocal"; |
1258 |
write.Write(output); |
1259 |
write.Close(); |
1260 |
} |
1261 |
else |
1262 |
{ |
1263 |
// We should wait until the host process has died before starting the installer. |
1264 |
var waitForFinish = $@" |
1265 |
COUNTER=0; |
1266 |
while ps -p {processID} > /dev/null; |
1267 |
do sleep 1; |
1268 |
COUNTER=$((++COUNTER)); |
1269 |
if [ $COUNTER -eq 90 ] |
1270 |
then |
1271 |
exit -1; |
1272 |
fi; |
1273 |
done; |
1274 |
"; |
1275 |
string relaunchAfterUpdate = ""; |
1276 |
if (RelaunchAfterUpdate) |
1277 |
{ |
1278 |
relaunchAfterUpdate = $@"{Process.GetCurrentProcess().MainModule.FileName}"; |
1279 |
} |
1280 |
if (IsZipDownload(downloadFilePath)) // .zip on macOS or .tar.gz on Linux |
1281 |
{ |
1282 |
// waiting for finish based on http://blog.joncairns.com/2013/03/wait-for-a-unix-process-to-finish/ |
1283 |
// use tar to extract |
1284 |
var tarCommand = isMacOS ? $"tar -x -f {downloadFilePath} -C \"{workingDir}\"" |
1285 |
: $"tar -xf {downloadFilePath} -C \"{workingDir}\" --overwrite "; |
1286 |
var output = $@" |
1287 |
{waitForFinish} |
1288 |
{tarCommand} |
1289 |
{relaunchAfterUpdate}"; |
1290 |
write.Write(output); |
1291 |
} |
1292 |
else |
1293 |
{ |
1294 |
string installerExt = Path.GetExtension(downloadFilePath); |
1295 |
if (DoExtensionsMatch(installerExt, ".pkg") || |
1296 |
DoExtensionsMatch(installerExt, ".dmg")) |
1297 |
{ |
1298 |
relaunchAfterUpdate = ""; // relaunching not supported for pkg or dmg downloads |
1299 |
} |
1300 |
var output = $@" |
1301 |
{waitForFinish} |
1302 |
{installerCmd} |
1303 |
{relaunchAfterUpdate}"; |
1304 |
write.Write(output); |
1305 |
} |
1306 |
write.Close(); |
1307 |
} |
1308 |
} |
1309 |
|
1310 |
// report |
1311 |
LogWriter.PrintMessage("Going to execute script at path: {0}", batchFilePath); |
1312 |
|
1313 |
// init the installer helper |
1314 |
_installerProcess = new Process |
1315 |
{ |
1316 |
StartInfo = |
1317 |
{ |
1318 |
FileName = batchFilePath, |
1319 |
WindowStyle = ProcessWindowStyle.Hidden, |
1320 |
UseShellExecute = false, |
1321 |
CreateNoWindow = true |
1322 |
} |
1323 |
}; |
1324 |
// start the installer process. the batch file will wait for the host app to close before starting. |
1325 |
_installerProcess.Start(); |
1326 |
await QuitApplication(); |
1327 |
} |
1328 |
|
1329 |
/// <summary> |
1330 |
/// Quits the application (host application) |
1331 |
/// </summary> |
1332 |
/// <returns>Runs asynchrously, so returns a Task</returns> |
1333 |
public async Task QuitApplication() |
1334 |
{ |
1335 |
// quit the app |
1336 |
_exitHandle?.Set(); // make SURE the loop exits! |
1337 |
// In case the user has shut the window that started this Sparkle window/instance, don't crash and burn. |
1338 |
// If you have better ideas on how to figure out if they've shut all other windows, let me know... |
1339 |
try |
1340 |
{ |
1341 |
await CallFuncConsideringUIThreadsAsync(new Func<Task>(async () => |
1342 |
{ |
1343 |
if (CloseApplicationAsync != null) |
1344 |
{ |
1345 |
await CloseApplicationAsync.Invoke(); |
1346 |
} |
1347 |
else if (CloseApplication != null) |
1348 |
{ |
1349 |
CloseApplication.Invoke(); |
1350 |
|
1351 |
} |
1352 |
else |
1353 |
{ |
1354 |
// Because the download/install window is usually on a separate thread, |
1355 |
// send dual shutdown messages via both the sync context (kills "main" app) |
1356 |
// and the current thread (kills current thread) |
1357 |
UIFactory?.Shutdown(); |
1358 |
} |
1359 |
})); |
1360 |
} |
1361 |
catch (Exception e) |
1362 |
{ |
1363 |
LogWriter.PrintMessage(e.Message); |
1364 |
} |
1365 |
} |
1366 |
|
1367 |
/// <summary> |
1368 |
/// Apps may need, for example, to let user save their work |
1369 |
/// </summary> |
1370 |
/// <returns>true if it's OK to run the installer</returns> |
1371 |
private async Task<bool> AskApplicationToSafelyCloseUp() |
1372 |
{ |
1373 |
try |
1374 |
{ |
1375 |
// In case the user has shut the window that started this Sparkle window/instance, don't crash and burn. |
1376 |
// If you have better ideas on how to figure out if they've shut all other windows, let me know... |
1377 |
if (PreparingToExitAsync != null) |
1378 |
{ |
1379 |
var args = new CancelEventArgs(); |
1380 |
await PreparingToExitAsync(this, args); |
1381 |
return !args.Cancel; |
1382 |
} |
1383 |
else if (PreparingToExit != null) |
1384 |
{ |
1385 |
var args = new CancelEventArgs(); |
1386 |
PreparingToExit(this, args); |
1387 |
return !args.Cancel; |
1388 |
} |
1389 |
} |
1390 |
catch (Exception e) |
1391 |
{ |
1392 |
LogWriter.PrintMessage(e.Message); |
1393 |
} |
1394 |
return true; |
1395 |
} |
1396 |
|
1397 |
|
1398 |
/// <summary> |
1399 |
/// Check for updates, using UI interaction appropriate for if the user initiated the update request |
1400 |
/// </summary> |
1401 |
public async Task<UpdateInfo> CheckForUpdatesAtUserRequest() |
1402 |
{ |
1403 |
CheckingForUpdatesWindow = UIFactory?.ShowCheckingForUpdates(); |
1404 |
if (CheckingForUpdatesWindow != null) |
1405 |
{ |
1406 |
CheckingForUpdatesWindow.UpdatesUIClosing += CheckingForUpdatesWindow_Closing; // to detect canceling |
1407 |
CheckingForUpdatesWindow.Show(); |
1408 |
} |
1409 |
|
1410 |
UpdateInfo updateData = await CheckForUpdates(); // handles UpdateStatus.UpdateAvailable (in terms of UI) |
1411 |
if (CheckingForUpdatesWindow != null) // if null, user closed 'Checking for Updates...' window or the UIFactory was null |
1412 |
{ |
1413 |
CheckingForUpdatesWindow?.Close(); |
1414 |
CallFuncConsideringUIThreads(() => |
1415 |
{ |
1416 |
switch (updateData.Status) |
1417 |
{ |
1418 |
case UpdateStatus.UpdateNotAvailable: |
1419 |
UIFactory?.ShowVersionIsUpToDate(); |
1420 |
break; |
1421 |
case UpdateStatus.UserSkipped: |
1422 |
UIFactory?.ShowVersionIsSkippedByUserRequest(); // they can get skipped version from Configuration |
1423 |
break; |
1424 |
case UpdateStatus.CouldNotDetermine: |
1425 |
UIFactory?.ShowCannotDownloadAppcast(AppCastUrl); |
1426 |
break; |
1427 |
} |
1428 |
}); |
1429 |
} |
1430 |
|
1431 |
return updateData;// in this case, we've already shown UI talking about the new version |
1432 |
} |
1433 |
|
1434 |
private void CheckingForUpdatesWindow_Closing(object sender, EventArgs e) |
1435 |
{ |
1436 |
CheckingForUpdatesWindow = null; |
1437 |
} |
1438 |
|
1439 |
/// <summary> |
1440 |
/// Check for updates, using interaction appropriate for where the user doesn't know you're doing it, so be polite. |
1441 |
/// Basically, this checks for updates without showing a UI. However, if a UIFactory is set and an update |
1442 |
/// is found, an update UI will be shown! |
1443 |
/// </summary> |
1444 |
public async Task<UpdateInfo> CheckForUpdatesQuietly() |
1445 |
{ |
1446 |
return await CheckForUpdates(); |
1447 |
} |
1448 |
|
1449 |
/// <summary> |
1450 |
/// Does a one-off check for updates |
1451 |
/// </summary> |
1452 |
private async Task<UpdateInfo> CheckForUpdates() |
1453 |
{ |
1454 |
// artificial delay -- if internet is super fast and the update check is super fast, the flash (fast show/hide) of the |
1455 |
// 'Checking for Updates...' window is very disorienting, so we add an artificial delay |
1456 |
bool isUserManuallyCheckingForUpdates = CheckingForUpdatesWindow != null; |
1457 |
if (isUserManuallyCheckingForUpdates) |
1458 |
{ |
1459 |
await Task.Delay(250); |
1460 |
} |
1461 |
UpdateCheckStarted?.Invoke(this); |
1462 |
Configuration config = Configuration; |
1463 |
|
1464 |
// check if update is required |
1465 |
_latestDownloadedUpdateInfo = await GetUpdateStatus(config); |
1466 |
List<AppCastItem> updates = _latestDownloadedUpdateInfo.Updates; |
1467 |
if (_latestDownloadedUpdateInfo.Status == UpdateStatus.UpdateAvailable) |
1468 |
{ |
1469 |
// show the update window |
1470 |
LogWriter.PrintMessage("Update needed from version {0} to version {1}", config.InstalledVersion, updates[0].Version); |
1471 |
|
1472 |
UpdateDetectedEventArgs ev = new UpdateDetectedEventArgs |
1473 |
{ |
1474 |
NextAction = NextUpdateAction.ShowStandardUserInterface, |
1475 |
ApplicationConfig = config, |
1476 |
LatestVersion = updates[0], |
1477 |
AppCastItems = updates |
1478 |
}; |
1479 |
|
1480 |
// if the client wants to intercept, send an event |
1481 |
if (UpdateDetected != null) |
1482 |
{ |
1483 |
UpdateDetected(this, ev); |
1484 |
// if the client wants the default UI then show them |
1485 |
switch (ev.NextAction) |
1486 |
{ |
1487 |
case NextUpdateAction.ShowStandardUserInterface: |
1488 |
LogWriter.PrintMessage("Showing standard update UI"); |
1489 |
OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates)); |
1490 |
break; |
1491 |
} |
1492 |
} |
1493 |
else |
1494 |
{ |
1495 |
// otherwise just go forward with the UI notification |
1496 |
if (isUserManuallyCheckingForUpdates && CheckingForUpdatesWindow != null) |
1497 |
{ |
1498 |
ShowUpdateNeededUI(updates); |
1499 |
} |
1500 |
} |
1501 |
} |
1502 |
UpdateCheckFinished?.Invoke(this, _latestDownloadedUpdateInfo.Status); |
1503 |
return _latestDownloadedUpdateInfo; |
1504 |
} |
1505 |
|
1506 |
/// <summary> |
1507 |
/// Cancels an in-progress download and deletes the temporary file. |
1508 |
/// </summary> |
1509 |
public void CancelFileDownload() |
1510 |
{ |
1511 |
LogWriter.PrintMessage("Canceling download..."); |
1512 |
if (UpdateDownloader != null && UpdateDownloader.IsDownloading) |
1513 |
{ |
1514 |
UpdateDownloader.CancelDownload(); |
1515 |
} |
1516 |
} |
1517 |
|
1518 |
/// <summary> |
1519 |
/// Events should always be fired on the thread that started the Sparkle object. |
1520 |
/// Used for events that are fired after coming from an update available window |
1521 |
/// or the download progress window. |
1522 |
/// Basically, if ShowsUIOnMainThread, just invokes the action. Otherwise, |
1523 |
/// uses the SynchronizationContext to call the action. Ensures that the action |
1524 |
/// is always on the main thread. |
1525 |
/// </summary> |
1526 |
/// <param name="action"></param> |
1527 |
private void CallFuncConsideringUIThreads(Action action) |
1528 |
{ |
1529 |
if (ShowsUIOnMainThread) |
1530 |
{ |
1531 |
action?.Invoke(); |
1532 |
} |
1533 |
else |
1534 |
{ |
1535 |
_syncContext.Post((state) => action?.Invoke(), null); |
1536 |
} |
1537 |
} |
1538 |
|
1539 |
/// <summary> |
1540 |
/// Events should always be fired on the thread that started the Sparkle object. |
1541 |
/// Used for events that are fired after coming from an update available window |
1542 |
/// or the download progress window. |
1543 |
/// Basically, if ShowsUIOnMainThread, just invokes the action. Otherwise, |
1544 |
/// uses the SynchronizationContext to call the action. Ensures that the action |
1545 |
/// is always on the main thread. |
1546 |
/// </summary> |
1547 |
/// <param name="action"></param> |
1548 |
private async Task CallFuncConsideringUIThreadsAsync(Func<Task> action) |
1549 |
{ |
1550 |
if (ShowsUIOnMainThread) |
1551 |
{ |
1552 |
await action?.Invoke(); |
1553 |
} |
1554 |
else |
1555 |
{ |
1556 |
_syncContext.Post(async (state) => await action?.Invoke(), null); |
1557 |
} |
1558 |
} |
1559 |
|
1560 |
/// <summary> |
1561 |
/// </summary> |
1562 |
/// <param name="sender">not used.</param> |
1563 |
/// <param name="args">Info on the user response and what update item they responded to</param> |
1564 |
private async void OnUserWindowUserResponded(object sender, UpdateResponseEventArgs args) |
1565 |
{ |
1566 |
LogWriter.PrintMessage("Update window response: {0}", args.Result); |
1567 |
var currentItem = args.UpdateItem; |
1568 |
var result = args.Result; |
1569 |
if (string.IsNullOrWhiteSpace(_downloadTempFileName)) |
1570 |
{ |
1571 |
// we need the download file name in order to tell the user the skipped version |
1572 |
// file path and/or to run the installer |
1573 |
_downloadTempFileName = await GetDownloadPathForAppCastItem(currentItem); |
1574 |
} |
1575 |
if (result == UpdateAvailableResult.SkipUpdate) |
1576 |
{ |
1577 |
// skip this version |
1578 |
Configuration.SetVersionToSkip(currentItem.Version); |
1579 |
CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); }); |
1580 |
} |
1581 |
else if (result == UpdateAvailableResult.InstallUpdate) |
1582 |
{ |
1583 |
await CallFuncConsideringUIThreadsAsync(async () => |
1584 |
{ |
1585 |
UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); |
1586 |
if (UserInteractionMode == UserInteractionMode.DownloadNoInstall && File.Exists(_downloadTempFileName)) |
1587 |
{ |
1588 |
// Binary should already be downloaded. Run it! |
1589 |
ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); |
1590 |
} |
1591 |
else |
1592 |
{ |
1593 |
// download the binaries |
1594 |
await InitAndBeginDownload(currentItem); |
1595 |
} |
1596 |
}); |
1597 |
} |
1598 |
else if (result == UpdateAvailableResult.RemindMeLater && currentItem != null) |
1599 |
{ |
1600 |
CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); }); |
1601 |
} |
1602 |
UpdateAvailableWindow?.Close(); |
1603 |
UpdateAvailableWindow = null; // done using the window so don't hold onto reference |
1604 |
CheckingForUpdatesWindow?.Close(); |
1605 |
CheckingForUpdatesWindow = null; |
1606 |
} |
1607 |
|
1608 |
/// <summary> |
1609 |
/// This method will be executed as worker thread |
1610 |
/// </summary> |
1611 |
private async void OnWorkerDoWork(object sender, DoWorkEventArgs e) |
1612 |
{ |
1613 |
// store the did run once feature |
1614 |
bool goIntoLoop = true; |
1615 |
bool checkTSP = true; |
1616 |
bool doInitialCheck = _doInitialCheck; |
1617 |
bool isInitialCheck = true; |
1618 |
|
1619 |
// start our lifecycles |
1620 |
do |
1621 |
{ |
1622 |
if (_cancelToken.IsCancellationRequested) |
1623 |
{ |
1624 |
break; |
1625 |
} |
1626 |
// set state |
1627 |
bool bUpdateRequired = false; |
1628 |
|
1629 |
// notify |
1630 |
LoopStarted?.Invoke(this); |
1631 |
|
1632 |
// report status |
1633 |
if (doInitialCheck) |
1634 |
{ |
1635 |
// report status |
1636 |
LogWriter.PrintMessage("Starting update loop..."); |
1637 |
|
1638 |
// read the config |
1639 |
LogWriter.PrintMessage("Reading config..."); |
1640 |
Configuration config = Configuration; |
1641 |
|
1642 |
// calc CheckTasp |
1643 |
bool checkTSPInternal = checkTSP; |
1644 |
|
1645 |
if (isInitialCheck && checkTSPInternal) |
1646 |
{ |
1647 |
checkTSPInternal = !_forceInitialCheck; |
1648 |
} |
1649 |
|
1650 |
// check if it's ok the recheck to software state |
1651 |
TimeSpan csp = DateTime.Now - config.LastCheckTime; |
1652 |
|
1653 |
if (!checkTSPInternal || csp >= _checkFrequency) |
1654 |
{ |
1655 |
checkTSP = true; |
1656 |
// when sparkle will be deactivated wait another cycle |
1657 |
if (config.CheckForUpdate == true) |
1658 |
{ |
1659 |
// update the runonce feature |
1660 |
goIntoLoop = !config.IsFirstRun; |
1661 |
|
1662 |
// check if update is required |
1663 |
if (_cancelToken.IsCancellationRequested || !goIntoLoop) |
1664 |
{ |
1665 |
break; |
1666 |
} |
1667 |
_latestDownloadedUpdateInfo = await GetUpdateStatus(config); |
1668 |
if (_cancelToken.IsCancellationRequested) |
1669 |
{ |
1670 |
break; |
1671 |
} |
1672 |
bUpdateRequired = _latestDownloadedUpdateInfo.Status == UpdateStatus.UpdateAvailable; |
1673 |
if (bUpdateRequired) |
1674 |
{ |
1675 |
List<AppCastItem> updates = _latestDownloadedUpdateInfo.Updates; |
1676 |
// show the update window |
1677 |
LogWriter.PrintMessage("Update needed from version {0} to version {1}", config.InstalledVersion, updates[0].Version); |
1678 |
|
1679 |
// send notification if needed |
1680 |
UpdateDetectedEventArgs ev = new UpdateDetectedEventArgs |
1681 |
{ |
1682 |
NextAction = NextUpdateAction.ShowStandardUserInterface, |
1683 |
ApplicationConfig = config, |
1684 |
LatestVersion = updates[0], |
1685 |
AppCastItems = updates |
1686 |
}; |
1687 |
UpdateDetected?.Invoke(this, ev); |
1688 |
|
1689 |
// check results |
1690 |
switch (ev.NextAction) |
1691 |
{ |
1692 |
case NextUpdateAction.PerformUpdateUnattended: |
1693 |
{ |
1694 |
LogWriter.PrintMessage("Unattended update desired from consumer"); |
1695 |
UserInteractionMode = UserInteractionMode.DownloadAndInstall; |
1696 |
OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates)); |
1697 |
break; |
1698 |
} |
1699 |
case NextUpdateAction.ProhibitUpdate: |
1700 |
{ |
1701 |
LogWriter.PrintMessage("Update prohibited from consumer"); |
1702 |
break; |
1703 |
} |
1704 |
default: |
1705 |
{ |
1706 |
LogWriter.PrintMessage("Preparing to show standard update UI"); |
1707 |
OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates)); |
1708 |
break; |
1709 |
} |
1710 |
} |
1711 |
} |
1712 |
} |
1713 |
else |
1714 |
{ |
1715 |
LogWriter.PrintMessage("Check for updates disabled"); |
1716 |
} |
1717 |
} |
1718 |
else |
1719 |
{ |
1720 |
LogWriter.PrintMessage("Update check performed within the last {0} minutes!", _checkFrequency.TotalMinutes); |
1721 |
} |
1722 |
} |
1723 |
else |
1724 |
{ |
1725 |
LogWriter.PrintMessage("Initial check prohibited, going to wait"); |
1726 |
doInitialCheck = true; |
1727 |
} |
1728 |
|
1729 |
// checking is done; this is now the "let's wait a while" section |
1730 |
|
1731 |
// reset initial check |
1732 |
isInitialCheck = false; |
1733 |
|
1734 |
// notify |
1735 |
LoopFinished?.Invoke(this, bUpdateRequired); |
1736 |
|
1737 |
// report wait statement |
1738 |
LogWriter.PrintMessage("Sleeping for an other {0} minutes, exit event or force update check event", _checkFrequency.TotalMinutes); |
1739 |
|
1740 |
// wait for |
1741 |
if (!goIntoLoop || _cancelToken.IsCancellationRequested) |
1742 |
{ |
1743 |
break; |
1744 |
} |
1745 |
|
1746 |
// build the event array |
1747 |
WaitHandle[] handles = new WaitHandle[1]; |
1748 |
handles[0] = _exitHandle; |
1749 |
|
1750 |
// wait for any |
1751 |
if (_cancelToken.IsCancellationRequested) |
1752 |
{ |
1753 |
break; |
1754 |
} |
1755 |
int i = WaitHandle.WaitAny(handles, _checkFrequency); |
1756 |
if (_cancelToken.IsCancellationRequested) |
1757 |
{ |
1758 |
break; |
1759 |
} |
1760 |
if (WaitHandle.WaitTimeout == i) |
1761 |
{ |
1762 |
LogWriter.PrintMessage("{0} minutes are over", _checkFrequency.TotalMinutes); |
1763 |
continue; |
1764 |
} |
1765 |
|
1766 |
// check the exit handle |
1767 |
if (i == 0) |
1768 |
{ |
1769 |
LogWriter.PrintMessage("Got exit signal"); |
1770 |
break; |
1771 |
} |
1772 |
|
1773 |
// check an other check needed |
1774 |
if (i == 1) |
1775 |
{ |
1776 |
LogWriter.PrintMessage("Got force update check signal"); |
1777 |
checkTSP = false; |
1778 |
} |
1779 |
if (_cancelToken.IsCancellationRequested) |
1780 |
{ |
1781 |
break; |
1782 |
} |
1783 |
} while (goIntoLoop); |
1784 |
|
1785 |
// reset the islooping handle |
1786 |
_loopingHandle.Reset(); |
1787 |
} |
1788 |
|
1789 |
/// <summary> |
1790 |
/// This method will be notified by the SparkleUpdater loop when |
1791 |
/// some update info has been downloaded. If the info has been |
1792 |
/// downloaded fully (e.ProgressPercentage == 1), the UI |
1793 |
/// for downloading updates will be shown (if not downloading silently) |
1794 |
/// or the download will be performed (if downloading silently). |
1795 |
/// </summary> |
1796 |
private void OnWorkerProgressChanged(object sender, ProgressChangedEventArgs e) |
1797 |
{ |
1798 |
switch (e.ProgressPercentage) |
1799 |
{ |
1800 |
case 1: |
1801 |
UpdatesHaveBeenDownloaded(e.UserState as List<AppCastItem>); |
1802 |
break; |
1803 |
case 0: |
1804 |
LogWriter.PrintMessage(e.UserState.ToString()); |
1805 |
break; |
1806 |
} |
1807 |
} |
1808 |
|
1809 |
/// <summary> |
1810 |
/// Updates from appcast have been downloaded from the server |
1811 |
/// </summary> |
1812 |
/// <param name="updates">updates to be installed</param> |
1813 |
private async void UpdatesHaveBeenDownloaded(List<AppCastItem> updates) |
1814 |
{ |
1815 |
if (updates != null) |
1816 |
{ |
1817 |
if (IsDownloadingSilently()) |
1818 |
{ |
1819 |
await InitAndBeginDownload(updates[0]); // install only latest |
1820 |
} |
1821 |
else |
1822 |
{ |
1823 |
// show the update UI |
1824 |
ShowUpdateNeededUI(updates); |
1825 |
} |
1826 |
} |
1827 |
} |
1828 |
} |
1829 |
} |