프로젝트

일반

사용자정보

통계
| 브랜치(Branch): | 개정판:

markus / MarkusAutoUpdate / src / NetSparkle / SparkleUpdater.cs @ 92c9cab8

이력 | 보기 | 이력해설 | 다운로드 (75.8 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 38d69491 taeseongkim
                    var assembly = new AssemblyDiagnosticsAccessor(_appReferenceAssembly);
237 d8f5045e taeseongkim
#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 3dcc7c99 taeseongkim
                    if (SignatureVerifier.SecurityMode != SecurityMode.Unsafe)
997
                    {
998
                        validationRes = SignatureVerifier.VerifySignatureOfFile(_itemBeingDownloaded?.DownloadSignature, _downloadTempFileName);
999
                    }
1000
                    else
1001
                    {
1002
                        validationRes = ValidationResult.Valid;
1003
                    }
1004 d8f5045e taeseongkim
                }
1005
            }
1006
1007
            bool isSignatureInvalid = validationRes == ValidationResult.Invalid; // if Unchecked, we accept download as valid
1008
            if (shouldShowUIItems)
1009
            {
1010
                CallFuncConsideringUIThreads(() => { ProgressWindow?.FinishedDownloadingFile(!isSignatureInvalid); });
1011
            }
1012
            // signature of file isn't valid so exit with error
1013
            if (isSignatureInvalid)
1014
            {
1015
                LogWriter.PrintMessage("Invalid signature for downloaded file for app cast: {0}", _downloadTempFileName);
1016
                string errorMessage = "Downloaded file has invalid signature!";
1017
                DownloadedFileIsCorrupt?.Invoke(_itemBeingDownloaded, _downloadTempFileName);
1018
                // Default to showing errors in the progress window. Only go to the UIFactory to show errors if necessary.
1019
                CallFuncConsideringUIThreads(() =>
1020
                {
1021
                    if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage))
1022
                    {
1023
                        UIFactory?.ShowDownloadErrorMessage(errorMessage, AppCastUrl);
1024
                    }
1025
                    DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(errorMessage));
1026
                });
1027
            }
1028
            else
1029
            {
1030
                LogWriter.PrintMessage("DSA Signature is valid. File successfully downloaded!");
1031
                DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName);
1032
                bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall;
1033
                if (shouldInstallAndRelaunch)
1034
                {
1035
                    CallFuncConsideringUIThreads(() => { ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); });
1036
                }
1037
            }
1038
            _itemBeingDownloaded = null;
1039
        }
1040
1041
        /// <summary>
1042
        /// Run the provided app cast item update regardless of what else is going on.
1043
        /// Note that a more up to date download may be taking place, so if you don't
1044
        /// want to run a potentially out-of-date installer, don't use this. This should
1045
        /// only be used if your user wants to update before another update has been
1046
        /// installed AND the file is already downloaded.
1047
        /// This function will verify that the file exists and that the DSA 
1048
        /// signature is valid before running. It will also utilize the
1049
        /// PreparingToExit event to ensure that the application can close.
1050
        /// </summary>
1051
        /// <param name="item">AppCastItem to install</param>
1052
        /// <param name="installPath">Install path to the executable. If not provided, will ask the server for the download path.</param>
1053
        public async void InstallUpdate(AppCastItem item, string installPath = null)
1054
        {
1055
            ProgressWindow?.SetDownloadAndInstallButtonEnabled(false); // disable while we ask if we can close up the software
1056
            if (await AskApplicationToSafelyCloseUp())
1057
            {
1058
                var path = installPath != null && File.Exists(installPath) ? installPath : await GetDownloadPathForAppCastItem(item);
1059
                if (File.Exists(path))
1060
                {
1061
                    var result = SignatureVerifier.VerifySignatureOfFile(item.DownloadSignature, path);
1062
                    if (result == ValidationResult.Valid || result == ValidationResult.Unchecked)
1063
                    {
1064
                        await RunDownloadedInstaller(path);
1065
                    }
1066
                }
1067
            }
1068
            ProgressWindow?.SetDownloadAndInstallButtonEnabled(true);
1069
        }
1070
1071
        /// <summary>
1072
        /// Checks to see
1073
        /// </summary>
1074
        /// <param name="item"></param>
1075
        /// <returns></returns>
1076
        public bool IsDownloadingItem(AppCastItem item)
1077
        {
1078
            return _itemBeingDownloaded?.DownloadSignature == item.DownloadSignature;
1079
        }
1080
1081
        /// <summary>
1082
        /// True if the user has silent updates enabled; false otherwise.
1083
        /// </summary>
1084
        private bool IsDownloadingSilently()
1085
        {
1086
            return UserInteractionMode != UserInteractionMode.NotSilent;
1087
        }
1088
1089
        /// <summary>
1090
        /// Checks to see if two extensions match (this is basically just a 
1091
        /// convenient string comparison). Both extensions should include the
1092
        /// initial . (full-stop/period) in the extension.
1093
        /// </summary>
1094
        /// <param name="extension">first extension to check</param>
1095
        /// <param name="otherExtension">other extension to check</param>
1096
        /// <returns>true if the extensions match; false otherwise</returns>
1097
        protected bool DoExtensionsMatch(string extension, string otherExtension)
1098
        {
1099
            return extension.Equals(otherExtension, StringComparison.CurrentCultureIgnoreCase);
1100
        }
1101
1102
        /// <summary>
1103
        /// Get the install command for the file at the given path. Figures out which
1104
        /// command to use based on the download file path's file extension.
1105
        /// Currently supports .exe, .msi, and .msp.
1106
        /// </summary>
1107
        /// <param name="downloadFilePath">Path to the downloaded update file</param>
1108
        /// <returns>the installer command if the file has one of the given 
1109
        /// extensions; the initial downloadFilePath if not.</returns>
1110
        protected virtual string GetWindowsInstallerCommand(string downloadFilePath)
1111
        {
1112
            string installerExt = Path.GetExtension(downloadFilePath);
1113
            if (DoExtensionsMatch(installerExt, ".exe"))
1114
            {
1115
                return "\"" + downloadFilePath + "\"";
1116
            }
1117
            if (DoExtensionsMatch(installerExt, ".msi"))
1118
            {
1119
                return "msiexec /i \"" + downloadFilePath + "\"";
1120
            }
1121
            if (DoExtensionsMatch(installerExt, ".msp"))
1122
            {
1123
                return "msiexec /p \"" + downloadFilePath + "\"";
1124
            }
1125
            return downloadFilePath;
1126
        }
1127
1128
        /// <summary>
1129
        /// Get the install command for the file at the given path. Figures out which
1130
        /// command to use based on the download file path's file extension.
1131
        /// <para>Windows: currently supports .exe, .msi, and .msp.</para>
1132
        /// <para>macOS: currently supports .pkg, .dmg, and .zip.</para>
1133
        /// <para>Linux: currently supports .tar.gz, .deb, and .rpm.</para>
1134
        /// </summary>
1135
        /// <param name="downloadFilePath">Path to the downloaded update file</param>
1136
        /// <returns>the installer command if the file has one of the given 
1137
        /// extensions; the initial downloadFilePath if not.</returns>
1138
        protected virtual string GetInstallerCommand(string downloadFilePath)
1139
        {
1140
            // get the file type
1141
#if NETFRAMEWORK
1142
            return GetWindowsInstallerCommand(downloadFilePath);
1143
#else
1144
            string installerExt = Path.GetExtension(downloadFilePath);
1145
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
1146
            {
1147
                return GetWindowsInstallerCommand(downloadFilePath);
1148
            }
1149
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
1150
            {
1151
                if (DoExtensionsMatch(installerExt, ".pkg") ||
1152
                    DoExtensionsMatch(installerExt, ".dmg"))
1153
                {
1154
                    return "open \"" + downloadFilePath + "\"";
1155
                }
1156
            }
1157
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
1158
            {
1159
                if (DoExtensionsMatch(installerExt, ".deb"))
1160
                {
1161
                    return "sudo dpkg -i \"" + downloadFilePath + "\"";
1162
                }
1163
                if (DoExtensionsMatch(installerExt, ".rpm"))
1164
                {
1165
                    return "sudo rpm -i \"" + downloadFilePath + "\"";
1166
                }
1167
            }
1168
            return downloadFilePath;
1169
#endif
1170
        }
1171
1172
        private bool IsZipDownload(string downloadFilePath)
1173
        {
1174
#if NETCORE
1175
            string installerExt = Path.GetExtension(downloadFilePath);
1176
            bool isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
1177
            bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
1178
            if ((isMacOS && DoExtensionsMatch(installerExt, ".zip")) ||
1179
                (isLinux && downloadFilePath.EndsWith(".tar.gz")))
1180
            {
1181
                return true;
1182
            }
1183
#endif
1184
            return false;
1185
        }
1186
1187
        /// <summary>
1188
        /// Updates the application via the file at the given path. Figures out which command needs
1189
        /// to be run, sets up the application so that it will start the downloaded file once the
1190
        /// main application stops, and then waits to start the downloaded update.
1191
        /// </summary>
1192
        /// <param name="downloadFilePath">path to the downloaded installer/updater</param>
1193
        /// <returns>the awaitable <see cref="Task"/> for the application quitting</returns>
1194
        protected virtual async Task RunDownloadedInstaller(string downloadFilePath)
1195
        {
1196
            LogWriter.PrintMessage("Running downloaded installer");
1197
            // get the commandline 
1198
            string cmdLine = Environment.CommandLine;
1199
            string workingDir = Utilities.GetFullBaseDirectory();
1200
1201
            // generate the batch file path
1202
#if NETFRAMEWORK
1203
            bool isWindows = true;
1204
            bool isMacOS = false;
1205
#else
1206
            bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
1207
            bool isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
1208
#endif
1209
            var extension = isWindows ? ".cmd" : ".sh";
1210
            string batchFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + extension);
1211
            string installerCmd;
1212
            try
1213
            {
1214
                installerCmd = GetInstallerCommand(downloadFilePath);
1215
                if (!string.IsNullOrEmpty(CustomInstallerArguments))
1216
                {
1217
                    installerCmd += " " + CustomInstallerArguments;
1218
                }
1219
            }
1220
            catch (InvalidDataException)
1221
            {
1222
                UIFactory?.ShowUnknownInstallerFormatMessage(downloadFilePath);
1223
                return;
1224
            }
1225
1226
            // generate the batch file                
1227
            LogWriter.PrintMessage("Generating batch in {0}", Path.GetFullPath(batchFilePath));
1228
1229
            string processID = Process.GetCurrentProcess().Id.ToString();
1230
1231
            using (StreamWriter write = new StreamWriter(batchFilePath, false, new UTF8Encoding(false)))
1232
            {
1233
                if (isWindows)
1234
                {
1235
                    write.WriteLine("@echo off");
1236
                    // We should wait until the host process has died before starting the installer.
1237
                    // This way, any DLLs or other items can be replaced properly.
1238
                    // Code from: http://stackoverflow.com/a/22559462/3938401
1239
                    string relaunchAfterUpdate = "";
1240
                    if (RelaunchAfterUpdate)
1241
                    {
1242
                        relaunchAfterUpdate = $@"
1243
                        cd {workingDir}
1244
                        {cmdLine}";
1245
                    }
1246
1247
                    string output = $@"
1248
                        set /A counter=0                       
1249
                        setlocal ENABLEDELAYEDEXPANSION
1250
                        :loop
1251
                        set /A counter=!counter!+1
1252
                        if !counter! == 90 (
1253
                            goto :afterinstall
1254
                        )
1255
                        tasklist | findstr ""\<{processID}\>"" > nul
1256
                        if not errorlevel 1 (
1257
                            timeout /t 1 > nul
1258
                            goto :loop
1259
                        )
1260
                        :install
1261
                        {installerCmd}
1262
                        {relaunchAfterUpdate}
1263
                        :afterinstall
1264
                        endlocal";
1265
                    write.Write(output);
1266
                    write.Close();
1267
                }
1268
                else
1269
                {
1270
                    // We should wait until the host process has died before starting the installer.
1271
                    var waitForFinish = $@"
1272
                        COUNTER=0;
1273
                        while ps -p {processID} > /dev/null;
1274
                            do sleep 1;
1275
                            COUNTER=$((++COUNTER));
1276
                            if [ $COUNTER -eq 90 ] 
1277
                            then
1278
                                exit -1;
1279
                            fi;
1280
                        done;
1281
                    ";
1282
                    string relaunchAfterUpdate = "";
1283
                    if (RelaunchAfterUpdate)
1284
                    {
1285
                        relaunchAfterUpdate = $@"{Process.GetCurrentProcess().MainModule.FileName}";
1286
                    }
1287
                    if (IsZipDownload(downloadFilePath)) // .zip on macOS or .tar.gz on Linux
1288
                    {
1289
                        // waiting for finish based on http://blog.joncairns.com/2013/03/wait-for-a-unix-process-to-finish/
1290
                        // use tar to extract
1291
                        var tarCommand = isMacOS ? $"tar -x -f {downloadFilePath} -C \"{workingDir}\"" 
1292
                            : $"tar -xf {downloadFilePath} -C \"{workingDir}\" --overwrite ";
1293
                        var output = $@"
1294
                            {waitForFinish}
1295
                            {tarCommand}
1296
                            {relaunchAfterUpdate}";
1297
                        write.Write(output);
1298
                    }
1299
                    else
1300
                    {
1301
                        string installerExt = Path.GetExtension(downloadFilePath);
1302
                        if (DoExtensionsMatch(installerExt, ".pkg") ||
1303
                            DoExtensionsMatch(installerExt, ".dmg"))
1304
                        {
1305
                            relaunchAfterUpdate = ""; // relaunching not supported for pkg or dmg downloads
1306
                        }
1307
                        var output = $@"
1308
                            {waitForFinish}
1309
                            {installerCmd}
1310
                            {relaunchAfterUpdate}";
1311
                        write.Write(output);
1312
                    }
1313
                    write.Close();
1314
                }
1315
            }
1316
1317
            // report
1318
            LogWriter.PrintMessage("Going to execute script at path: {0}", batchFilePath);
1319
1320
            // init the installer helper
1321
            _installerProcess = new Process
1322
            {
1323
                StartInfo =
1324
                {
1325
                    FileName = batchFilePath,
1326
                    WindowStyle = ProcessWindowStyle.Hidden,
1327
                    UseShellExecute = false,
1328
                    CreateNoWindow = true
1329
                }
1330
            };
1331
            // start the installer process. the batch file will wait for the host app to close before starting.
1332
            _installerProcess.Start();
1333
            await QuitApplication();
1334
        }
1335
1336
        /// <summary>
1337
        /// Quits the application (host application) 
1338
        /// </summary>
1339
        /// <returns>Runs asynchrously, so returns a Task</returns>
1340
        public async Task QuitApplication()
1341
        {
1342
            // quit the app
1343
            _exitHandle?.Set(); // make SURE the loop exits!
1344
                                // In case the user has shut the window that started this Sparkle window/instance, don't crash and burn.
1345
                                // If you have better ideas on how to figure out if they've shut all other windows, let me know...
1346
            try
1347
            {
1348
                await CallFuncConsideringUIThreadsAsync(new Func<Task>(async () =>
1349
                {
1350
                    if (CloseApplicationAsync != null)
1351
                    {
1352
                        await CloseApplicationAsync.Invoke();
1353
                    }
1354
                    else if (CloseApplication != null)
1355
                    {
1356
                        CloseApplication.Invoke();
1357
1358
                    }
1359
                    else
1360
                    {
1361
                        // Because the download/install window is usually on a separate thread,
1362
                        // send dual shutdown messages via both the sync context (kills "main" app)
1363
                        // and the current thread (kills current thread)
1364
                        UIFactory?.Shutdown();
1365
                    }
1366
                }));
1367
            }
1368
            catch (Exception e)
1369
            {
1370
                LogWriter.PrintMessage(e.Message);
1371
            }
1372
        }
1373
1374
        /// <summary>
1375
        /// Apps may need, for example, to let user save their work
1376
        /// </summary>
1377
        /// <returns>true if it's OK to run the installer</returns>
1378
        private async Task<bool> AskApplicationToSafelyCloseUp()
1379
        {
1380
            try
1381
            {
1382
                // In case the user has shut the window that started this Sparkle window/instance, don't crash and burn.
1383
                // If you have better ideas on how to figure out if they've shut all other windows, let me know...
1384
                if (PreparingToExitAsync != null)
1385
                {
1386
                    var args = new CancelEventArgs();
1387
                    await PreparingToExitAsync(this, args);
1388
                    return !args.Cancel;
1389
                }
1390
                else if (PreparingToExit != null)
1391
                {
1392
                    var args = new CancelEventArgs();
1393
                    PreparingToExit(this, args);
1394
                    return !args.Cancel;
1395
                }
1396
            }
1397
            catch (Exception e)
1398
            {
1399
                LogWriter.PrintMessage(e.Message);
1400
            }
1401
            return true;
1402
        }
1403
1404
1405
        /// <summary>
1406
        /// Check for updates, using UI interaction appropriate for if the user initiated the update request
1407
        /// </summary>
1408
        public async Task<UpdateInfo> CheckForUpdatesAtUserRequest()
1409
        {
1410
            CheckingForUpdatesWindow = UIFactory?.ShowCheckingForUpdates();
1411
            if (CheckingForUpdatesWindow != null)
1412
            {
1413
                CheckingForUpdatesWindow.UpdatesUIClosing += CheckingForUpdatesWindow_Closing; // to detect canceling
1414
                CheckingForUpdatesWindow.Show();
1415
            }
1416
1417
            UpdateInfo updateData = await CheckForUpdates(); // handles UpdateStatus.UpdateAvailable (in terms of UI)
1418
            if (CheckingForUpdatesWindow != null) // if null, user closed 'Checking for Updates...' window or the UIFactory was null
1419
            {
1420
                CheckingForUpdatesWindow?.Close();
1421
                CallFuncConsideringUIThreads(() => 
1422
                {
1423
                    switch (updateData.Status)
1424
                    {
1425
                        case UpdateStatus.UpdateNotAvailable:
1426
                            UIFactory?.ShowVersionIsUpToDate();
1427
                            break;
1428
                        case UpdateStatus.UserSkipped:
1429
                            UIFactory?.ShowVersionIsSkippedByUserRequest(); // they can get skipped version from Configuration
1430
                            break;
1431
                        case UpdateStatus.CouldNotDetermine:
1432
                            UIFactory?.ShowCannotDownloadAppcast(AppCastUrl);
1433
                            break;
1434
                    }
1435
                });
1436
            }
1437
1438
            return updateData;// in this case, we've already shown UI talking about the new version
1439
        }
1440
1441
        private void CheckingForUpdatesWindow_Closing(object sender, EventArgs e)
1442
        {
1443
            CheckingForUpdatesWindow = null;
1444
        }
1445
1446
        /// <summary>
1447
        /// Check for updates, using interaction appropriate for where the user doesn't know you're doing it, so be polite.
1448
        /// Basically, this checks for updates without showing a UI. However, if a UIFactory is set and an update
1449
        /// is found, an update UI will be shown!
1450
        /// </summary>
1451
        public async Task<UpdateInfo> CheckForUpdatesQuietly()
1452
        {
1453
            return await CheckForUpdates();
1454
        }
1455
1456
        /// <summary>
1457
        /// Does a one-off check for updates
1458
        /// </summary>
1459
        private async Task<UpdateInfo> CheckForUpdates()
1460
        {
1461
            // artificial delay -- if internet is super fast and the update check is super fast, the flash (fast show/hide) of the
1462
            // 'Checking for Updates...' window is very disorienting, so we add an artificial delay
1463
            bool isUserManuallyCheckingForUpdates = CheckingForUpdatesWindow != null;
1464
            if (isUserManuallyCheckingForUpdates)
1465
            {
1466
                await Task.Delay(250);
1467
            }
1468
            UpdateCheckStarted?.Invoke(this);
1469
            Configuration config = Configuration;
1470
1471
            // check if update is required
1472
            _latestDownloadedUpdateInfo = await GetUpdateStatus(config);
1473
            List<AppCastItem> updates = _latestDownloadedUpdateInfo.Updates;
1474
            if (_latestDownloadedUpdateInfo.Status == UpdateStatus.UpdateAvailable)
1475
            {
1476
                // show the update window
1477
                LogWriter.PrintMessage("Update needed from version {0} to version {1}", config.InstalledVersion, updates[0].Version);
1478
1479
                UpdateDetectedEventArgs ev = new UpdateDetectedEventArgs
1480
                {
1481
                    NextAction = NextUpdateAction.ShowStandardUserInterface,
1482
                    ApplicationConfig = config,
1483
                    LatestVersion = updates[0],
1484
                    AppCastItems = updates
1485
                };
1486
1487
                // if the client wants to intercept, send an event
1488
                if (UpdateDetected != null)
1489
                {
1490
                    UpdateDetected(this, ev);
1491
                    // if the client wants the default UI then show them
1492
                    switch (ev.NextAction)
1493
                    {
1494
                        case NextUpdateAction.ShowStandardUserInterface:
1495
                            LogWriter.PrintMessage("Showing standard update UI");
1496
                            OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates));
1497
                            break;
1498
                    }
1499
                }
1500
                else
1501
                {
1502
                    // otherwise just go forward with the UI notification
1503
                    if (isUserManuallyCheckingForUpdates && CheckingForUpdatesWindow != null)
1504
                    {
1505
                        ShowUpdateNeededUI(updates);
1506
                    }
1507
                }
1508
            }
1509
            UpdateCheckFinished?.Invoke(this, _latestDownloadedUpdateInfo.Status);
1510
            return _latestDownloadedUpdateInfo;
1511
        }
1512
1513
        /// <summary>
1514
        /// Cancels an in-progress download and deletes the temporary file.
1515
        /// </summary>
1516
        public void CancelFileDownload()
1517
        {
1518
            LogWriter.PrintMessage("Canceling download...");
1519
            if (UpdateDownloader != null && UpdateDownloader.IsDownloading)
1520
            {
1521
                UpdateDownloader.CancelDownload();
1522
            }
1523
        }
1524
1525
        /// <summary>
1526
        /// Events should always be fired on the thread that started the Sparkle object.
1527
        /// Used for events that are fired after coming from an update available window
1528
        /// or the download progress window.
1529
        /// Basically, if ShowsUIOnMainThread, just invokes the action. Otherwise,
1530
        /// uses the SynchronizationContext to call the action. Ensures that the action
1531
        /// is always on the main thread.
1532
        /// </summary>
1533
        /// <param name="action"></param>
1534
        private void CallFuncConsideringUIThreads(Action action)
1535
        {
1536
            if (ShowsUIOnMainThread)
1537
            {
1538
                action?.Invoke();
1539
            }
1540
            else
1541
            {
1542
                _syncContext.Post((state) => action?.Invoke(), null);
1543
            }
1544
        }
1545
1546
        /// <summary>
1547
        /// Events should always be fired on the thread that started the Sparkle object.
1548
        /// Used for events that are fired after coming from an update available window
1549
        /// or the download progress window.
1550
        /// Basically, if ShowsUIOnMainThread, just invokes the action. Otherwise,
1551
        /// uses the SynchronizationContext to call the action. Ensures that the action
1552
        /// is always on the main thread.
1553
        /// </summary>
1554
        /// <param name="action"></param>
1555
        private async Task CallFuncConsideringUIThreadsAsync(Func<Task> action)
1556
        {
1557
            if (ShowsUIOnMainThread)
1558
            {
1559
                await action?.Invoke();
1560
            }
1561
            else
1562
            {
1563
                _syncContext.Post(async (state) => await action?.Invoke(), null);
1564
            }
1565
        }
1566
1567
        /// <summary>
1568
        /// </summary>
1569
        /// <param name="sender">not used.</param>
1570
        /// <param name="args">Info on the user response and what update item they responded to</param>
1571
        private async void OnUserWindowUserResponded(object sender, UpdateResponseEventArgs args)
1572
        {
1573
            LogWriter.PrintMessage("Update window response: {0}", args.Result);
1574
            var currentItem = args.UpdateItem;
1575
            var result = args.Result;
1576
            if (string.IsNullOrWhiteSpace(_downloadTempFileName))
1577
            {
1578
                // we need the download file name in order to tell the user the skipped version
1579
                // file path and/or to run the installer
1580
                _downloadTempFileName = await GetDownloadPathForAppCastItem(currentItem);
1581
            }
1582
            if (result == UpdateAvailableResult.SkipUpdate)
1583
            {
1584
                // skip this version
1585
                Configuration.SetVersionToSkip(currentItem.Version);
1586
                CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); });
1587
            }
1588
            else if (result == UpdateAvailableResult.InstallUpdate)
1589
            {
1590
                await CallFuncConsideringUIThreadsAsync(async () => 
1591
                {
1592
                    UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem));
1593
                    if (UserInteractionMode == UserInteractionMode.DownloadNoInstall && File.Exists(_downloadTempFileName))
1594
                    {
1595
                        // Binary should already be downloaded. Run it!
1596
                        ProgressWindowCompleted(this, new DownloadInstallEventArgs(true));
1597
                    }
1598
                    else
1599
                    {
1600
                        // download the binaries
1601
                        await InitAndBeginDownload(currentItem);
1602
                    }
1603
                });
1604
            }
1605
            else if (result == UpdateAvailableResult.RemindMeLater && currentItem != null)
1606
            {
1607
                CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); });
1608
            }
1609
            UpdateAvailableWindow?.Close();
1610
            UpdateAvailableWindow = null; // done using the window so don't hold onto reference
1611
            CheckingForUpdatesWindow?.Close();
1612
            CheckingForUpdatesWindow = null;
1613
        }
1614
1615
        /// <summary>
1616
        /// This method will be executed as worker thread
1617
        /// </summary>
1618
        private async void OnWorkerDoWork(object sender, DoWorkEventArgs e)
1619
        {
1620
            // store the did run once feature
1621
            bool goIntoLoop = true;
1622
            bool checkTSP = true;
1623
            bool doInitialCheck = _doInitialCheck;
1624
            bool isInitialCheck = true;
1625
1626
            // start our lifecycles
1627
            do
1628
            {
1629
                if (_cancelToken.IsCancellationRequested)
1630
                {
1631
                    break;
1632
                }
1633
                // set state
1634
                bool bUpdateRequired = false;
1635
1636
                // notify
1637
                LoopStarted?.Invoke(this);
1638
1639
                // report status
1640
                if (doInitialCheck)
1641
                {
1642
                    // report status
1643
                    LogWriter.PrintMessage("Starting update loop...");
1644
1645
                    // read the config
1646
                    LogWriter.PrintMessage("Reading config...");
1647
                    Configuration config = Configuration;
1648
1649
                    // calc CheckTasp
1650
                    bool checkTSPInternal = checkTSP;
1651
1652
                    if (isInitialCheck && checkTSPInternal)
1653
                    {
1654
                        checkTSPInternal = !_forceInitialCheck;
1655
                    }
1656
1657
                    // check if it's ok the recheck to software state
1658
                    TimeSpan csp = DateTime.Now - config.LastCheckTime;
1659
1660
                    if (!checkTSPInternal || csp >= _checkFrequency)
1661
                    {
1662
                        checkTSP = true;
1663
                        // when sparkle will be deactivated wait another cycle
1664
                        if (config.CheckForUpdate == true)
1665
                        {
1666
                            // update the runonce feature
1667
                            goIntoLoop = !config.IsFirstRun;
1668
1669
                            // check if update is required
1670
                            if (_cancelToken.IsCancellationRequested || !goIntoLoop)
1671
                            {
1672
                                break;
1673
                            }
1674
                            _latestDownloadedUpdateInfo = await GetUpdateStatus(config);
1675
                            if (_cancelToken.IsCancellationRequested)
1676
                            {
1677
                                break;
1678
                            }
1679
                            bUpdateRequired = _latestDownloadedUpdateInfo.Status == UpdateStatus.UpdateAvailable;
1680
                            if (bUpdateRequired)
1681
                            {
1682
                                List<AppCastItem> updates = _latestDownloadedUpdateInfo.Updates;
1683
                                // show the update window
1684
                                LogWriter.PrintMessage("Update needed from version {0} to version {1}", config.InstalledVersion, updates[0].Version);
1685
1686
                                // send notification if needed
1687
                                UpdateDetectedEventArgs ev = new UpdateDetectedEventArgs
1688
                                {
1689
                                    NextAction = NextUpdateAction.ShowStandardUserInterface,
1690
                                    ApplicationConfig = config,
1691
                                    LatestVersion = updates[0],
1692
                                    AppCastItems = updates
1693
                                };
1694
                                UpdateDetected?.Invoke(this, ev);
1695
1696
                                // check results
1697
                                switch (ev.NextAction)
1698
                                {
1699
                                    case NextUpdateAction.PerformUpdateUnattended:
1700
                                        {
1701
                                            LogWriter.PrintMessage("Unattended update desired from consumer");
1702
                                            UserInteractionMode = UserInteractionMode.DownloadAndInstall;
1703
                                            OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates));
1704
                                            break;
1705
                                        }
1706
                                    case NextUpdateAction.ProhibitUpdate:
1707
                                        {
1708
                                            LogWriter.PrintMessage("Update prohibited from consumer");
1709
                                            break;
1710
                                        }
1711
                                    default:
1712
                                        {
1713
                                            LogWriter.PrintMessage("Preparing to show standard update UI");
1714
                                            OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates));
1715
                                            break;
1716
                                        }
1717
                                }
1718
                            }
1719
                        }
1720
                        else
1721
                        {
1722
                            LogWriter.PrintMessage("Check for updates disabled");
1723
                        }
1724
                    }
1725
                    else
1726
                    {
1727
                        LogWriter.PrintMessage("Update check performed within the last {0} minutes!", _checkFrequency.TotalMinutes);
1728
                    }
1729
                }
1730
                else
1731
                {
1732
                    LogWriter.PrintMessage("Initial check prohibited, going to wait");
1733
                    doInitialCheck = true;
1734
                }
1735
1736
                // checking is done; this is now the "let's wait a while" section
1737
1738
                // reset initial check
1739
                isInitialCheck = false;
1740
1741
                // notify
1742
                LoopFinished?.Invoke(this, bUpdateRequired);
1743
1744
                // report wait statement
1745
                LogWriter.PrintMessage("Sleeping for an other {0} minutes, exit event or force update check event", _checkFrequency.TotalMinutes);
1746
1747
                // wait for
1748
                if (!goIntoLoop || _cancelToken.IsCancellationRequested)
1749
                {
1750
                    break;
1751
                }
1752
1753
                // build the event array
1754
                WaitHandle[] handles = new WaitHandle[1];
1755
                handles[0] = _exitHandle;
1756
1757
                // wait for any
1758
                if (_cancelToken.IsCancellationRequested)
1759
                {
1760
                    break;
1761
                }
1762
                int i = WaitHandle.WaitAny(handles, _checkFrequency);
1763
                if (_cancelToken.IsCancellationRequested)
1764
                {
1765
                    break;
1766
                }
1767
                if (WaitHandle.WaitTimeout == i)
1768
                {
1769
                    LogWriter.PrintMessage("{0} minutes are over", _checkFrequency.TotalMinutes);
1770
                    continue;
1771
                }
1772
1773
                // check the exit handle
1774
                if (i == 0)
1775
                {
1776
                    LogWriter.PrintMessage("Got exit signal");
1777
                    break;
1778
                }
1779
1780
                // check an other check needed
1781
                if (i == 1)
1782
                {
1783
                    LogWriter.PrintMessage("Got force update check signal");
1784
                    checkTSP = false;
1785
                }
1786
                if (_cancelToken.IsCancellationRequested)
1787
                {
1788
                    break;
1789
                }
1790
            } while (goIntoLoop);
1791
1792
            // reset the islooping handle
1793
            _loopingHandle.Reset();
1794
        }
1795
1796
        /// <summary>
1797
        /// This method will be notified by the SparkleUpdater loop when
1798
        /// some update info has been downloaded. If the info has been 
1799
        /// downloaded fully (e.ProgressPercentage == 1), the UI
1800
        /// for downloading updates will be shown (if not downloading silently)
1801
        /// or the download will be performed (if downloading silently).
1802
        /// </summary>
1803
        private void OnWorkerProgressChanged(object sender, ProgressChangedEventArgs e)
1804
        {
1805
            switch (e.ProgressPercentage)
1806
            {
1807
                case 1:
1808
                    UpdatesHaveBeenDownloaded(e.UserState as List<AppCastItem>);
1809
                    break;
1810
                case 0:
1811
                    LogWriter.PrintMessage(e.UserState.ToString());
1812
                    break;
1813
            }
1814
        }
1815
1816
        /// <summary>
1817
        /// Updates from appcast have been downloaded from the server
1818
        /// </summary>
1819
        /// <param name="updates">updates to be installed</param>
1820
        private async void UpdatesHaveBeenDownloaded(List<AppCastItem> updates)
1821
        {
1822
            if (updates != null)
1823
            {
1824
                if (IsDownloadingSilently())
1825
                {
1826
                    await InitAndBeginDownload(updates[0]); // install only latest
1827
                }
1828
                else
1829
                {
1830
                    // show the update UI
1831
                    ShowUpdateNeededUI(updates);
1832
                }
1833
            }
1834
        }
1835
    }
1836
}
클립보드 이미지 추가 (최대 크기: 500 MB)