markus / MarkusAutoUpdate / src / NetSparkle / SparkleUpdater.cs @ d8f5045e
이력 | 보기 | 이력해설 | 다운로드 (75.5 KB)
1 | d8f5045e | taeseongkim | 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 | } |