프로젝트

일반

사용자정보

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

markus / MarkusAutoUpdate / src / NetSparkle / SparkleUpdater.cs @ d8f5045e

이력 | 보기 | 이력해설 | 다운로드 (75.5 KB)

1
using System;
2
using System.ComponentModel;
3
using System.Net;
4
using System.Net.Security;
5
using System.Security.Cryptography.X509Certificates;
6
using System.Threading;
7
using NetSparkleUpdater.Interfaces;
8
using System.IO;
9
using System.Diagnostics;
10
using System.Threading.Tasks;
11
using NetSparkleUpdater.Enums;
12
using System.Net.Http;
13
using NetSparkleUpdater.Events;
14
using System.Collections.Generic;
15
using NetSparkleUpdater.Downloaders;
16
using NetSparkleUpdater.Configurations;
17
using NetSparkleUpdater.SignatureVerifiers;
18
using NetSparkleUpdater.AppCastHandlers;
19
using NetSparkleUpdater.AssemblyAccessors;
20
using System.Text;
21
using System.Globalization;
22
#if NETSTANDARD
23
using System.Runtime.InteropServices;
24
#endif
25

    
26
namespace NetSparkleUpdater
27
{
28
    /// <summary>
29
    /// Class to communicate with a sparkle-based appcast to download
30
    /// and install updates to an application
31
    /// </summary>
32
    public partial class SparkleUpdater : IDisposable
33
    {
34
        #region Protected/Private Members
35

    
36
        /// <summary>
37
        /// The <see cref="Process"/> responsible for launching the downloaded update.
38
        /// Only valid once the application is about to quit and the update is going to
39
        /// be launched.
40
        /// </summary>
41
        protected Process _installerProcess;
42

    
43
        private ILogger _logWriter;
44
        private readonly Task _taskWorker;
45
        private CancellationToken _cancelToken;
46
        private readonly CancellationTokenSource _cancelTokenSource;
47
        private readonly SynchronizationContext _syncContext;
48
        private readonly string _appReferenceAssembly;
49

    
50
        private bool _doInitialCheck;
51
        private bool _forceInitialCheck;
52

    
53
        private readonly EventWaitHandle _exitHandle;
54
        private readonly EventWaitHandle _loopingHandle;
55
        private TimeSpan _checkFrequency;
56
        private string _tmpDownloadFilePath;
57
        private string _downloadTempFileName;
58
        private AppCastItem _itemBeingDownloaded;
59
        private bool _hasAttemptedFileRedownload;
60
        private UpdateInfo _latestDownloadedUpdateInfo;
61
        private IUIFactory _uiFactory;
62
        private bool _disposed;
63
        private Configuration _configuration;
64

    
65
        #endregion
66

    
67
        #region Constructors
68

    
69
        /// <summary>
70
        /// ctor which needs the appcast url
71
        /// </summary>
72
        /// <param name="appcastUrl">the URL of the appcast file</param>
73
        /// <param name="signatureVerifier">the object that will verify your appcast signatures.</param>
74
        public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier)
75
            : this(appcastUrl, signatureVerifier, null)
76
        { }
77

    
78
        /// <summary>
79
        /// ctor which needs the appcast url and a referenceassembly
80
        /// </summary>        
81
        /// <param name="appcastUrl">the URL of the appcast file</param>
82
        /// <param name="signatureVerifier">the object that will verify your appcast signatures.</param>
83
        /// <param name="referenceAssembly">the name of the assembly to use for comparison when checking update versions</param>
84
        public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly)
85
            : this(appcastUrl, signatureVerifier, referenceAssembly, null)
86
        { }
87

    
88
        /// <summary>
89
        /// ctor which needs the appcast url and a referenceassembly
90
        /// </summary>        
91
        /// <param name="appcastUrl">the URL of the appcast file</param>
92
        /// <param name="signatureVerifier">the object that will verify your appcast signatures.</param>
93
        /// <param name="referenceAssembly">the name of the assembly to use for comparison when checking update versions</param>
94
        /// <param name="factory">a UI factory to use in place of the default UI</param>
95
        public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly, IUIFactory factory)
96
        {
97
            _latestDownloadedUpdateInfo = null;
98
            _hasAttemptedFileRedownload = false;
99
            UIFactory = factory;
100
            SignatureVerifier = signatureVerifier;
101
            // Syncronization Context
102
            _syncContext = SynchronizationContext.Current;
103
            if (_syncContext == null)
104
            {
105
                _syncContext = new SynchronizationContext();
106
            }
107
            // init UI
108
            UIFactory?.Init();
109
            _appReferenceAssembly = null;
110
            // set the reference assembly
111
            if (referenceAssembly != null)
112
            {
113
                _appReferenceAssembly = referenceAssembly;
114
                LogWriter.PrintMessage("Checking the following file: " + _appReferenceAssembly);
115
            }
116

    
117
            // adjust the delegates
118
            _taskWorker = new Task(() =>
119
            {
120
                OnWorkerDoWork(null, null);
121
            });
122
            _cancelTokenSource = new CancellationTokenSource();
123
            _cancelToken = _cancelTokenSource.Token;
124

    
125
            // build the wait handle
126
            _exitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
127
            _loopingHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
128

    
129
            // set the url
130
            AppCastUrl = appcastUrl;
131
            LogWriter.PrintMessage("Using the following url: {0}", AppCastUrl);
132
            UserInteractionMode = UserInteractionMode.NotSilent;
133
            TmpDownloadFilePath = "";
134
        }
135

    
136
        #endregion
137

    
138
        #region Properties
139

    
140
        /// <summary>
141
        /// The security protocol used by NetSparkle. Setting this property will also set this 
142
        /// for the current AppDomain of the caller. Needs to be set to 
143
        /// SecurityProtocolType.Tls12 for some cases (such as when downloading from GitHub).
144
        /// </summary>
145
        public SecurityProtocolType SecurityProtocolType
146
        {
147
            get
148
            {
149
                return ServicePointManager.SecurityProtocol;
150
            }
151
            set
152
            {
153
                ServicePointManager.SecurityProtocol = value;
154
            }
155
        }
156

    
157
        /// <summary>
158
        /// Set the user interaction mode for Sparkle to use when there is a valid update for the software
159
        /// </summary>
160
        public UserInteractionMode UserInteractionMode { get; set; }
161

    
162
        /// <summary>
163
        /// If set, downloads files to this path. If the folder doesn't already exist, creates
164
        /// the folder at download time (and not before). 
165
        /// Note that this variable is a path, not a full file name.
166
        /// </summary>
167
        public string TmpDownloadFilePath
168
        {
169
            get { return _tmpDownloadFilePath; }
170
            set { _tmpDownloadFilePath = value?.Trim(); }
171
        }
172

    
173
        /// <summary>
174
        /// Defines if the application needs to be relaunched after executing the downloaded installer
175
        /// </summary>
176
        public bool RelaunchAfterUpdate { get; set; }
177

    
178
        /// <summary>
179
        /// Run the downloaded installer with these arguments
180
        /// </summary>
181
        public string CustomInstallerArguments { get; set; }
182

    
183
        /// <summary>
184
        /// Function that is called asynchronously to clean up old installers that have been
185
        /// downloaded with SilentModeTypes.DownloadNoInstall or SilentModeTypes.DownloadAndInstall.
186
        /// </summary>
187
        public Action ClearOldInstallers { get; set; }
188

    
189
        /// <summary>
190
        /// Whether or not the update loop is running
191
        /// </summary>
192
        public bool IsUpdateLoopRunning
193
        {
194
            get
195
            {
196
                return _loopingHandle.WaitOne(0);
197
            }
198
        }
199

    
200
        /// <summary>
201
        /// Factory for creating UI elements like progress window, etc.
202
        /// </summary>
203
        public IUIFactory UIFactory
204
        { 
205
            get { return _uiFactory; } 
206
            set { _uiFactory = value; _uiFactory?.Init(); }
207
        }
208

    
209
        /// <summary>
210
        /// The user interface window that shows the release notes and
211
        /// asks the user to skip, remind me later, or update
212
        /// </summary>
213
        private IUpdateAvailable UpdateAvailableWindow { get; set; }
214

    
215
        /// <summary>
216
        /// The user interface window that shows a download progress bar,
217
        /// and then asks to install and relaunch the application
218
        /// </summary>
219
        private IDownloadProgress ProgressWindow { get; set; }
220

    
221
        /// <summary>
222
        /// The user interface window that shows the 'Checking for Updates...'
223
        /// form.
224
        /// </summary>
225
        private ICheckingForUpdates CheckingForUpdatesWindow { get; set; }
226

    
227
        /// <summary>
228
        /// The NetSparkle configuration object for the current assembly.
229
        /// </summary>
230
        public Configuration Configuration 
231
        { 
232
            get
233
            {
234
                if (_configuration == null)
235
                {
236
                    var assembly = new AssemblyReflectionAccessor(_appReferenceAssembly);
237
#if NETSTANDARD
238
                    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
239
                    {
240
                        _configuration = new RegistryConfiguration(assembly);
241
                    }
242
                    else
243
                    {
244
                        _configuration = new JSONConfiguration(assembly);
245
                    }
246
#else
247
                    _configuration = new RegistryConfiguration(assembly);
248
#endif
249

    
250
                    assembly = null;
251
                }
252
                return _configuration;
253
            } 
254
            set { _configuration = value; } 
255
        }
256

    
257
        /// <summary>
258
        /// The object that verifies signatures (DSA or otherwise) of downloaded items
259
        /// </summary>
260
        public ISignatureVerifier SignatureVerifier { get; set; }
261

    
262
        /// <summary>
263
        /// Gets or sets the appcast URL
264
        /// </summary>
265
        public string AppCastUrl { get; set; }
266

    
267
        /// <summary>
268
        /// Specifies if you want to use the notification toast
269
        /// </summary>
270
        public bool UseNotificationToast { get; set; }
271

    
272
        /// <summary>
273
        /// WinForms/WPF only. 
274
        /// If true, tries to run UI code on the main thread using <see cref="SynchronizationContext"/>.
275
        /// Must be set to true if using NetSparkleUpdater from Avalonia.
276
        /// </summary>
277
        public bool ShowsUIOnMainThread { get; set; }
278

    
279
        /// <summary>
280
        /// Object that handles any diagnostic messages for NetSparkle.
281
        /// If you want to use your own class for this, you should just
282
        /// need to override <see cref="LogWriter.PrintMessage"/> in your own class.
283
        /// Make sure to set this object before calling <see cref="StartLoop(bool)"/> to guarantee
284
        /// that all messages will get sent to the right place!
285
        /// </summary>
286
        public ILogger LogWriter
287
        {
288
            get
289
            {
290
                if (_logWriter == null)
291
                {
292
                    _logWriter = new LogWriter();
293
                }
294
                return _logWriter;
295
            }
296
            set
297
            {
298
                _logWriter = value;
299
            }
300
        }
301

    
302
        /// <summary>
303
        /// Whether or not to check with the online server to verify download
304
        /// file names.
305
        /// </summary>
306
        public bool CheckServerFileName { get; set; } = true;
307

    
308
        /// <summary>
309
        /// Returns the latest appcast items to the caller. Might be null.
310
        /// </summary>
311
        public List<AppCastItem> LatestAppCastItems
312
        {
313
            get
314
            {
315
                return _latestDownloadedUpdateInfo?.Updates;
316
            }
317
        }
318

    
319
        /// <summary>
320
        /// Loops through all of the most recently grabbed app cast items
321
        /// and checks if any of them are marked as critical
322
        /// </summary>
323
        public bool UpdateMarkedCritical
324
        {
325
            get
326
            {
327
                if (LatestAppCastItems != null)
328
                {
329
                    foreach (AppCastItem item in LatestAppCastItems)
330
                    {
331
                        if (item.IsCriticalUpdate)
332
                        {
333
                            return true;
334
                        }
335
                    }
336
                }
337
                return false;
338
            }
339
        }
340

    
341
        /// <summary>
342
        /// The object responsable for downloading update files for your application
343
        /// </summary>
344
        public IUpdateDownloader UpdateDownloader { get; set; }
345

    
346
        /// <summary>
347
        /// The object responsible for downloading app cast and app cast signature
348
        /// information for your application
349
        /// </summary>
350
        public IAppCastDataDownloader AppCastDataDownloader { get; set; }
351

    
352
        /// <summary>
353
        /// The object responsible for parsing app cast information and checking to
354
        /// see if any updates are available in a given app cast
355
        /// </summary>
356
        public IAppCastHandler AppCastHandler { get; set; }
357

    
358
        #endregion
359

    
360
        /// <summary>
361
        /// Starts a NetSparkle background loop to check for updates every 24 hours.
362
        /// <para>You should only call this function when your app is initialized and shows its main window.</para>
363
        /// </summary>
364
        /// <param name="doInitialCheck">whether the first check should happen before or after the first interval</param>
365
        public void StartLoop(bool doInitialCheck)
366
        {
367
            StartLoop(doInitialCheck, false);
368
        }
369

    
370
        /// <summary>
371
        /// Starts a NetSparkle background loop to check for updates on a given interval.
372
        /// <para>You should only call this function when your app is initialized and shows its main window.</para>
373
        /// </summary>
374
        /// <param name="doInitialCheck">whether the first check should happen before or after the first interval</param>
375
        /// <param name="checkFrequency">the interval to wait between update checks</param>
376
        public void StartLoop(bool doInitialCheck, TimeSpan checkFrequency)
377
        {
378
            StartLoop(doInitialCheck, false, checkFrequency);
379
        }
380

    
381
        /// <summary>
382
        /// Starts a NetSparkle background loop to check for updates every 24 hours.
383
        /// <para>You should only call this function when your app is initialized and shows its main window.</para>
384
        /// </summary>
385
        /// <param name="doInitialCheck">whether the first check should happen before or after the first interval</param>
386
        /// <param name="forceInitialCheck">if <paramref name="doInitialCheck"/> is true, whether the first check
387
        /// should happen even if the last check was less than 24 hours ago</param>
388
        public void StartLoop(bool doInitialCheck, bool forceInitialCheck)
389
        {
390
            StartLoop(doInitialCheck, forceInitialCheck, TimeSpan.FromHours(24));
391
        }
392

    
393
        /// <summary>
394
        /// Starts a NetSparkle background loop to check for updates on a given interval.
395
        /// <para>You should only call this function when your app is initialized and shows its main window.</para>
396
        /// </summary>
397
        /// <param name="doInitialCheck">whether the first check should happen before or after the first period</param>
398
        /// <param name="forceInitialCheck">if <paramref name="doInitialCheck"/> is true, whether the first check
399
        /// should happen even if the last check was within the last <paramref name="checkFrequency"/> interval</param>
400
        /// <param name="checkFrequency">the interval to wait between update checks</param>
401
        public async void StartLoop(bool doInitialCheck, bool forceInitialCheck, TimeSpan checkFrequency)
402
        {
403
            if (ClearOldInstallers != null)
404
            {
405
                try
406
                {
407
                    await Task.Run(ClearOldInstallers);
408
                }
409
                catch (Exception e)
410
                {
411
                    LogWriter.PrintMessage("ClearOldInstallers threw an exception: {0}", e.Message);
412
                }
413
            }
414
            // first set the event handle
415
            _loopingHandle.Set();
416

    
417
            // Start the helper thread as a background worker                     
418

    
419
            // store info
420
            _doInitialCheck = doInitialCheck;
421
            _forceInitialCheck = forceInitialCheck;
422
            _checkFrequency = checkFrequency;
423

    
424
            LogWriter.PrintMessage("Starting background worker");
425

    
426
            // start the work
427
            //var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
428
            //_taskWorker.Start(scheduler);
429
            // don't allow starting the task 2x
430
            if (_taskWorker.IsCompleted == false && _taskWorker.Status != TaskStatus.Running && 
431
                _taskWorker.Status != TaskStatus.WaitingToRun && _taskWorker.Status != TaskStatus.WaitingForActivation)
432
            {
433
                _taskWorker.Start();
434
            }
435
        }
436

    
437
        /// <summary>
438
        /// Stops the Sparkle background loop. Called automatically by <see cref="Dispose()"/>.
439
        /// </summary>
440
        public void StopLoop()
441
        {
442
            // ensure the work will finished
443
            _exitHandle.Set();
444
        }
445

    
446
        /// <summary>
447
        /// Finalizer
448
        /// </summary>
449
        ~SparkleUpdater()
450
        {
451
            Dispose(false);
452
        }
453

    
454
#region IDisposable
455

    
456
        /// <summary>
457
        /// Inherited from IDisposable. Stops all background activities.
458
        /// </summary>
459
        public void Dispose()
460
        {
461
            Dispose(true);
462
            GC.SuppressFinalize(this);
463
        }
464

    
465
        /// <summary>
466
        /// Dispose of managed and unmanaged resources
467
        /// </summary>
468
        /// <param name="disposing"></param>
469
        protected virtual void Dispose(bool disposing)
470
        {
471
            if (!_disposed)
472
            {
473
                if (disposing)
474
                {
475
                    // Dispose managed resources.
476
                    StopLoop();
477
                    UnregisterEvents();
478
                    _cancelTokenSource?.Dispose();
479
                    _exitHandle?.Dispose();
480
                    _loopingHandle?.Dispose();
481
                    UpdateDownloader?.Dispose();
482
                    _installerProcess?.Dispose();
483
                }
484
                // There are no unmanaged resources to release, but
485
                // if we add them, they need to be released here.
486
            }
487
            _disposed = true;
488
        }
489

    
490
        /// <summary>
491
        /// Unregisters events so that we don't have multiple items updating
492
        /// </summary>
493
        private void UnregisterEvents()
494
        {
495
            _cancelTokenSource.Cancel();
496

    
497
            CleanUpUpdateDownloader();
498

    
499
            if (UpdateAvailableWindow != null)
500
            {
501
                UpdateAvailableWindow.UserResponded -= OnUserWindowUserResponded;
502
                UpdateAvailableWindow = null;
503
            }
504

    
505
            if (ProgressWindow != null)
506
            {
507
                ProgressWindow.DownloadProcessCompleted -= ProgressWindowCompleted;
508
                ProgressWindow = null;
509
            }
510
        }
511

    
512
#endregion
513

    
514
        /// <summary>
515
        /// This method checks if an update is required. During this process the appcast
516
        /// will be downloaded and checked against the reference assembly. Ensure that
517
        /// the calling process has read access to the reference assembly.
518
        /// This method is also called from the background loops.
519
        /// </summary>
520
        /// <param name="config">the NetSparkle configuration for the reference assembly</param>
521
        /// <returns><see cref="UpdateInfo"/> with information on whether there is an update available or not.</returns>
522
        protected async Task<UpdateInfo> GetUpdateStatus(Configuration config)
523
        {
524
            List<AppCastItem> updates = null;
525
            // report
526
            LogWriter.PrintMessage("Downloading and checking appcast");
527

    
528
            // init the appcast
529
            if (AppCastDataDownloader == null)
530
            {
531
                AppCastDataDownloader = new WebRequestAppCastDataDownloader();
532
            }
533
            if (AppCastHandler == null)
534
            {
535
                AppCastHandler = new XMLAppCast();
536
            }
537
            AppCastHandler.SetupAppCastHandler(AppCastDataDownloader, AppCastUrl, config, SignatureVerifier, LogWriter);
538
            // check if any updates are available
539
            try
540
            {
541
                await Task.Factory.StartNew(() =>
542
                {
543
                    LogWriter.PrintMessage("About to start downloading the app cast...");
544
                    if (AppCastHandler.DownloadAndParse())
545
                    {
546
                        LogWriter.PrintMessage("App cast successfully downloaded and parsed. Getting available updates...");
547
                        updates = AppCastHandler.GetAvailableUpdates();
548
                    }
549
                });
550
            }
551
            catch (Exception e)
552
            {
553
                LogWriter.PrintMessage("Couldn't read/parse the app cast: {0}", e.Message);
554
                updates = null;
555
            }
556

    
557
            if (updates == null)
558
            {
559
                LogWriter.PrintMessage("No version information in app cast found");
560
                return new UpdateInfo(UpdateStatus.CouldNotDetermine);
561
            }
562

    
563
            // set the last check time
564
            LogWriter.PrintMessage("Touch the last check timestamp");
565
            config.TouchCheckTime();
566

    
567
            // check if the version will be the same then the installed version
568
            if (updates.Count == 0)
569
            {
570
                LogWriter.PrintMessage("Installed version is valid, no update needed ({0})", config.InstalledVersion);
571
                return new UpdateInfo(UpdateStatus.UpdateNotAvailable);
572
            }
573
            LogWriter.PrintMessage("Latest version on the server is {0}", updates[0].Version);
574

    
575
            // check if the available update has to be skipped
576
            if (updates[0].Version.Equals(config.LastVersionSkipped))
577
            {
578
                LogWriter.PrintMessage("Latest update has to be skipped (user decided to skip version {0})", config.LastVersionSkipped);
579
                return new UpdateInfo(UpdateStatus.UserSkipped);
580
            }
581

    
582
            // ok we need an update
583
            return new UpdateInfo(UpdateStatus.UpdateAvailable, updates);
584
        }
585

    
586
        /// <summary>
587
        /// Shows the update needed UI with the given set of updates.
588
        /// </summary>
589
        /// <param name="updates">updates to show UI for</param>
590
        /// <param name="isUpdateAlreadyDownloaded">If true, make sure UI text shows that the user is about to install the file instead of download it.</param>
591
        public void ShowUpdateNeededUI(List<AppCastItem> updates, bool isUpdateAlreadyDownloaded = false)
592
        {
593
            if (updates != null)
594
            {
595
                if (UseNotificationToast && (bool)UIFactory?.CanShowToastMessages())
596
                {
597
                    UIFactory?.ShowToast(updates, OnToastClick);
598
                }
599
                else
600
                {
601
                    ShowUpdateAvailableWindow(updates, isUpdateAlreadyDownloaded);
602
                }
603
            }
604
        }
605

    
606
        /// <summary>
607
        /// Shows the update UI with the latest downloaded update information.
608
        /// </summary>
609
        /// <param name="isUpdateAlreadyDownloaded">If true, make sure UI text shows that the user is about to install the file instead of download it.</param>
610
        public void ShowUpdateNeededUI(bool isUpdateAlreadyDownloaded = false)
611
        {
612
            ShowUpdateNeededUI(_latestDownloadedUpdateInfo?.Updates, isUpdateAlreadyDownloaded);
613
        }
614

    
615
        private void OnToastClick(List<AppCastItem> updates)
616
        {
617
            ShowUpdateAvailableWindow(updates);
618
        }
619

    
620
        private void ShowUpdateAvailableWindow(List<AppCastItem> updates, bool isUpdateAlreadyDownloaded = false)
621
        {
622
            if (UpdateAvailableWindow != null)
623
            {
624
                // close old window
625
                if (ShowsUIOnMainThread)
626
                {
627
                    _syncContext.Post((state) =>
628
                    {
629
                        UpdateAvailableWindow.Close();
630
                        UpdateAvailableWindow = null;
631
                    }, null);
632
                }
633
                else
634
                {
635
                    UpdateAvailableWindow.Close();
636
                    UpdateAvailableWindow = null;
637
                }
638
            }
639

    
640
            // create the form
641
            Thread thread = new Thread(() =>
642
            {
643
                try
644
                {
645
                    // define action
646
                    Action<object> showSparkleUI = (state) =>
647
                    {
648
                        UpdateAvailableWindow = UIFactory?.CreateUpdateAvailableWindow(this, updates, isUpdateAlreadyDownloaded);
649

    
650
                        if (UpdateAvailableWindow != null)
651
                        {
652

    
653
                            // clear if already set.
654
                            UpdateAvailableWindow.UserResponded += OnUserWindowUserResponded;
655
                            UpdateAvailableWindow.Show(ShowsUIOnMainThread);
656
                        }
657
                    };
658

    
659
                    // call action
660
                    if (ShowsUIOnMainThread)
661
                    {
662
                        _syncContext.Post((state) => showSparkleUI(state), null);
663
                    }
664
                    else
665
                    {
666
                        showSparkleUI(null);
667
                    }
668
                }
669
                catch (Exception e)
670
                {
671
                    LogWriter.PrintMessage("Error showing sparkle form: {0}", e.Message);
672
                }
673
            });
674
#if NETFRAMEWORK
675
            thread.SetApartmentState(ApartmentState.STA);
676
#else
677
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
678
            {
679
                thread.SetApartmentState(ApartmentState.STA); // only supported on Windows
680
            }
681
#endif
682
            thread.Start();
683
        }
684

    
685
        /// <summary>
686
        /// Get the download path for a given app cast item.
687
        /// If any directories need to be created, this function
688
        /// will create those directories.
689
        /// </summary>
690
        /// <param name="item">The item that you want to generate a download path for</param>
691
        /// <returns>The download path for an app cast item if item is not null and has valid download link
692
        /// Otherwise returns null.</returns>
693
        public async Task<string> GetDownloadPathForAppCastItem(AppCastItem item)
694
        {
695
            if (item != null && item.DownloadLink != null)
696
            {
697
                string filename = string.Empty;
698

    
699
                // default to using the server's file name as the download file name
700
                if (CheckServerFileName && UpdateDownloader != null)
701
                {
702
                    try
703
                    {
704
                        filename = await UpdateDownloader.RetrieveDestinationFileNameAsync(item);
705
                    }
706
                    catch (Exception)
707
                    {
708
                        // ignore
709
                    }
710
                }
711

    
712
                if (string.IsNullOrEmpty(filename))
713
                {
714
                    // attempt to get download file name based on download link
715
                    try
716
                    {
717
                        filename = Path.GetFileName(new Uri(item.DownloadLink).LocalPath);
718
                    }
719
                    catch (UriFormatException)
720
                    {
721
                        // ignore
722
                    }
723
                }
724

    
725
                if (!string.IsNullOrEmpty(filename))
726
                {
727
                    string tmpPath = string.IsNullOrEmpty(TmpDownloadFilePath) ? Path.GetTempPath() : TmpDownloadFilePath;
728

    
729
                    // Creates all directories and subdirectories in the specific path unless they already exist.
730
                    Directory.CreateDirectory(tmpPath);
731

    
732
                    return Path.Combine(tmpPath, filename);
733
                }
734
            }
735
            return null;
736
        }
737

    
738
        /// <summary>
739
        /// Starts the download process by grabbing the download path for
740
        /// the app cast item (asynchronous so that it can get the server's
741
        /// download name in case there is a redirect; cancel this by setting
742
        /// CheckServerFileName to false), then beginning the download
743
        /// process if the download file doesn't already exist
744
        /// </summary>
745
        /// <param name="item">the appcast item to download</param>
746
        public async Task InitAndBeginDownload(AppCastItem item)
747
        {
748
            if (UpdateDownloader != null && UpdateDownloader.IsDownloading)
749
            {
750
                return; // file is already downloading, don't do anything!
751
            }
752
            LogWriter.PrintMessage("Preparing to download {0}", item.DownloadLink);
753
            _itemBeingDownloaded = item;
754
            CreateUpdateDownloaderIfNeeded();
755
            _downloadTempFileName = await GetDownloadPathForAppCastItem(item);
756
            // Make sure the file doesn't already exist on disk. If it's already downloaded and the
757
            // DSA signature checks out, don't redownload the file!
758
            bool needsToDownload = true;
759
            if (File.Exists(_downloadTempFileName))
760
            {
761
                ValidationResult result = SignatureVerifier.VerifySignatureOfFile(item.DownloadSignature, _downloadTempFileName);
762
                if (result == ValidationResult.Valid)
763
                {
764
                    LogWriter.PrintMessage("File is already downloaded");
765
                    // We already have the file! Don't redownload it!
766
                    needsToDownload = false;
767
                    // Still need to set up the ProgressWindow for non-silent downloads, though,
768
                    // so that the user can actually perform the install
769
                    CreateAndShowProgressWindow(item, true);
770
                    CallFuncConsideringUIThreads(() => { DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName); });
771
                    bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall;
772
                    if (shouldInstallAndRelaunch)
773
                    {
774
                        CallFuncConsideringUIThreads(() => { ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); });
775
                    }
776
                }
777
                else if (!_hasAttemptedFileRedownload)
778
                {
779
                    // The file exists but it either has a bad DSA signature or SecurityMode is set to Unsafe.
780
                    // Redownload it!
781
                    _hasAttemptedFileRedownload = true;
782
                    LogWriter.PrintMessage("File is corrupt or DSA signature is Unchecked; deleting file and redownloading...");
783
                    try
784
                    {
785
                        File.Delete(_downloadTempFileName);
786
                    }
787
                    catch (Exception e)
788
                    {
789
                        LogWriter.PrintMessage("Hm, seems as though we couldn't delete the temporary file even though it is apparently corrupt. {0}",
790
                            e.Message);
791
                        // we won't be able to download anyway since we couldn't delete the file :( we'll try next time the
792
                        // update loop goes around.
793
                        needsToDownload = false;
794
                        CallFuncConsideringUIThreads(() => 
795
                        {
796
                            DownloadHadError?.Invoke(item, _downloadTempFileName, 
797
                                new Exception(string.Format("Unable to delete old download at {0}", _downloadTempFileName)));
798
                        });
799
                    }
800
                }
801
                else
802
                {
803
                    CallFuncConsideringUIThreads(() => { DownloadedFileIsCorrupt?.Invoke(item, _downloadTempFileName); });
804
                }
805
            }
806
            if (needsToDownload)
807
            {
808
                // remove any old event handlers so we don't fire 2x
809
                UpdateDownloader.DownloadProgressChanged -= OnDownloadProgressChanged;
810
                UpdateDownloader.DownloadFileCompleted -= OnDownloadFinished;
811

    
812
                CreateAndShowProgressWindow(item, false);
813
                UpdateDownloader.DownloadProgressChanged += OnDownloadProgressChanged;
814
                UpdateDownloader.DownloadFileCompleted += OnDownloadFinished;
815

    
816
                Uri url = Utilities.GetAbsoluteURL(item.DownloadLink, AppCastUrl);
817
                LogWriter.PrintMessage("Starting to download {0} to {1}", item.DownloadLink, _downloadTempFileName);
818
                UpdateDownloader.StartFileDownload(url, _downloadTempFileName);
819
                CallFuncConsideringUIThreads(() => { DownloadStarted?.Invoke(item, _downloadTempFileName); });
820
            }
821
        }
822

    
823
        private void OnDownloadProgressChanged(object sender, ItemDownloadProgressEventArgs args)
824
        {
825
            CallFuncConsideringUIThreads(() =>
826
            {
827
                DownloadMadeProgress?.Invoke(sender, _itemBeingDownloaded, args); 
828
            });
829
        }
830

    
831
        private void CleanUpUpdateDownloader()
832
        {
833
            if (UpdateDownloader != null)
834
            {
835
                if (ProgressWindow != null)
836
                {
837
                    UpdateDownloader.DownloadProgressChanged -= ProgressWindow.OnDownloadProgressChanged;
838
                }
839
                UpdateDownloader.DownloadProgressChanged -= OnDownloadProgressChanged;
840
                UpdateDownloader.DownloadFileCompleted -= OnDownloadFinished;
841
                UpdateDownloader?.Dispose();
842
                UpdateDownloader = null;
843
            }
844
        }
845

    
846
        private void CreateUpdateDownloaderIfNeeded()
847
        {
848
            if (UpdateDownloader == null)
849
            {
850
                UpdateDownloader = new WebClientFileDownloader();
851
            }
852
        }
853

    
854
        private void CreateAndShowProgressWindow(AppCastItem castItem, bool shouldShowAsDownloadedAlready)
855
        {
856
            if (ProgressWindow != null)
857
            {
858
                ProgressWindow.DownloadProcessCompleted -= ProgressWindowCompleted;
859
                UpdateDownloader.DownloadProgressChanged -= ProgressWindow.OnDownloadProgressChanged;
860
                ProgressWindow = null;
861
            }
862
            if (ProgressWindow == null && UIFactory != null && !IsDownloadingSilently())
863
            {
864
                if (!IsDownloadingSilently() && ProgressWindow == null)
865
                {
866
                    // create the form
867
                    Action<object> showSparkleDownloadUI = (state) =>
868
                    {
869
                        ProgressWindow = UIFactory?.CreateProgressWindow(castItem);
870
                        if (ProgressWindow != null)
871
                        {
872
                            ProgressWindow.DownloadProcessCompleted += ProgressWindowCompleted;
873
                            UpdateDownloader.DownloadProgressChanged += ProgressWindow.OnDownloadProgressChanged;
874
                            if (shouldShowAsDownloadedAlready)
875
                            {
876
                                ProgressWindow?.FinishedDownloadingFile(true);
877
                                _syncContext.Post((state2) => OnDownloadFinished(null, new AsyncCompletedEventArgs(null, false, null)), null);
878
                            }
879
                        }
880
                    };
881
                    Thread thread = new Thread(() =>
882
                    {
883
                        // call action
884
                        if (ShowsUIOnMainThread)
885
                        {
886
                            _syncContext.Post((state) =>
887
                            {
888
                                showSparkleDownloadUI(null);
889
                                ProgressWindow?.Show(ShowsUIOnMainThread);
890
                            }, null);
891
                        }
892
                        else
893
                        {
894
                            showSparkleDownloadUI(null);
895
                            ProgressWindow?.Show(ShowsUIOnMainThread);
896
                        }
897
                    });
898
#if NETFRAMEWORK
899
                    thread.SetApartmentState(ApartmentState.STA);
900
#else
901
                    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
902
                    {
903
                        thread.SetApartmentState(ApartmentState.STA); // only supported on Windows
904
                    }
905
#endif
906
                    thread.Start();
907
                }
908
            }
909
        }
910

    
911
        private async void ProgressWindowCompleted(object sender, DownloadInstallEventArgs args)
912
        {
913
            if (args.ShouldInstall)
914
            {
915
                ProgressWindow?.SetDownloadAndInstallButtonEnabled(false); // disable while we ask if we can close up the software
916
                if (await AskApplicationToSafelyCloseUp())
917
                {
918
                    ProgressWindow?.Close();
919
                    await RunDownloadedInstaller(_downloadTempFileName);
920
                }
921
                else
922
                {
923
                    ProgressWindow?.SetDownloadAndInstallButtonEnabled(true);
924
                }
925
            }
926
            else
927
            {
928
                CancelFileDownload();
929
                ProgressWindow?.Close();
930
            }
931
        }
932

    
933
        /// <summary>
934
        /// Called when the installer is downloaded
935
        /// </summary>
936
        /// <param name="sender">not used.</param>
937
        /// <param name="e">used to determine if the download was successful.</param>
938
        private void OnDownloadFinished(object sender, AsyncCompletedEventArgs e)
939
        {
940
            bool shouldShowUIItems = !IsDownloadingSilently();
941

    
942
            if (e.Cancelled)
943
            {
944
                DownloadCanceled?.Invoke(_itemBeingDownloaded, _downloadTempFileName);
945
                _hasAttemptedFileRedownload = false;
946
                if (File.Exists(_downloadTempFileName))
947
                {
948
                    File.Delete(_downloadTempFileName);
949
                }
950
                LogWriter.PrintMessage("Download was canceled");
951
                string errorMessage = "Download canceled";
952
                if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage))
953
                {
954
                    UIFactory?.ShowDownloadErrorMessage(errorMessage, AppCastUrl);
955
                }
956
                DownloadCanceled?.Invoke(_itemBeingDownloaded, _downloadTempFileName);
957
                return;
958
            }
959
            if (e.Error != null)
960
            {
961
                DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, e.Error);
962
                // Clean temp files on error too
963
                if (File.Exists(_downloadTempFileName))
964
                {
965
                    File.Delete(_downloadTempFileName);
966
                }
967
                LogWriter.PrintMessage("Error on download finished: {0}", e.Error.Message);
968
                if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(e.Error.Message))
969
                {
970
                    UIFactory?.ShowDownloadErrorMessage(e.Error.Message, AppCastUrl);
971
                }
972
                DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(e.Error.Message));
973
                return;
974
            }
975
            // test the item for signature
976
            var validationRes = ValidationResult.Invalid;
977
            if (!e.Cancelled && e.Error == null)
978
            {
979
                LogWriter.PrintMessage("Fully downloaded file exists at {0}", _downloadTempFileName);
980

    
981
                LogWriter.PrintMessage("Performing signature check");
982

    
983
                // get the assembly
984
                if (File.Exists(_downloadTempFileName))
985
                {
986
                    // check if the file was downloaded successfully
987
                    string absolutePath = Path.GetFullPath(_downloadTempFileName);
988
                    if (!File.Exists(absolutePath))
989
                    {
990
                        var message = "File not found even though it was reported as downloading successfully!";
991
                        LogWriter.PrintMessage(message);
992
                        DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(message));
993
                    }
994

    
995
                    // check the signature
996
                    validationRes = SignatureVerifier.VerifySignatureOfFile(_itemBeingDownloaded?.DownloadSignature, _downloadTempFileName);
997
                }
998
            }
999

    
1000
            bool isSignatureInvalid = validationRes == ValidationResult.Invalid; // if Unchecked, we accept download as valid
1001
            if (shouldShowUIItems)
1002
            {
1003
                CallFuncConsideringUIThreads(() => { ProgressWindow?.FinishedDownloadingFile(!isSignatureInvalid); });
1004
            }
1005
            // signature of file isn't valid so exit with error
1006
            if (isSignatureInvalid)
1007
            {
1008
                LogWriter.PrintMessage("Invalid signature for downloaded file for app cast: {0}", _downloadTempFileName);
1009
                string errorMessage = "Downloaded file has invalid signature!";
1010
                DownloadedFileIsCorrupt?.Invoke(_itemBeingDownloaded, _downloadTempFileName);
1011
                // Default to showing errors in the progress window. Only go to the UIFactory to show errors if necessary.
1012
                CallFuncConsideringUIThreads(() =>
1013
                {
1014
                    if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage))
1015
                    {
1016
                        UIFactory?.ShowDownloadErrorMessage(errorMessage, AppCastUrl);
1017
                    }
1018
                    DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(errorMessage));
1019
                });
1020
            }
1021
            else
1022
            {
1023
                LogWriter.PrintMessage("DSA Signature is valid. File successfully downloaded!");
1024
                DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName);
1025
                bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall;
1026
                if (shouldInstallAndRelaunch)
1027
                {
1028
                    CallFuncConsideringUIThreads(() => { ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); });
1029
                }
1030
            }
1031
            _itemBeingDownloaded = null;
1032
        }
1033

    
1034
        /// <summary>
1035
        /// Run the provided app cast item update regardless of what else is going on.
1036
        /// Note that a more up to date download may be taking place, so if you don't
1037
        /// want to run a potentially out-of-date installer, don't use this. This should
1038
        /// only be used if your user wants to update before another update has been
1039
        /// installed AND the file is already downloaded.
1040
        /// This function will verify that the file exists and that the DSA 
1041
        /// signature is valid before running. It will also utilize the
1042
        /// PreparingToExit event to ensure that the application can close.
1043
        /// </summary>
1044
        /// <param name="item">AppCastItem to install</param>
1045
        /// <param name="installPath">Install path to the executable. If not provided, will ask the server for the download path.</param>
1046
        public async void InstallUpdate(AppCastItem item, string installPath = null)
1047
        {
1048
            ProgressWindow?.SetDownloadAndInstallButtonEnabled(false); // disable while we ask if we can close up the software
1049
            if (await AskApplicationToSafelyCloseUp())
1050
            {
1051
                var path = installPath != null && File.Exists(installPath) ? installPath : await GetDownloadPathForAppCastItem(item);
1052
                if (File.Exists(path))
1053
                {
1054
                    var result = SignatureVerifier.VerifySignatureOfFile(item.DownloadSignature, path);
1055
                    if (result == ValidationResult.Valid || result == ValidationResult.Unchecked)
1056
                    {
1057
                        await RunDownloadedInstaller(path);
1058
                    }
1059
                }
1060
            }
1061
            ProgressWindow?.SetDownloadAndInstallButtonEnabled(true);
1062
        }
1063

    
1064
        /// <summary>
1065
        /// Checks to see
1066
        /// </summary>
1067
        /// <param name="item"></param>
1068
        /// <returns></returns>
1069
        public bool IsDownloadingItem(AppCastItem item)
1070
        {
1071
            return _itemBeingDownloaded?.DownloadSignature == item.DownloadSignature;
1072
        }
1073

    
1074
        /// <summary>
1075
        /// True if the user has silent updates enabled; false otherwise.
1076
        /// </summary>
1077
        private bool IsDownloadingSilently()
1078
        {
1079
            return UserInteractionMode != UserInteractionMode.NotSilent;
1080
        }
1081

    
1082
        /// <summary>
1083
        /// Checks to see if two extensions match (this is basically just a 
1084
        /// convenient string comparison). Both extensions should include the
1085
        /// initial . (full-stop/period) in the extension.
1086
        /// </summary>
1087
        /// <param name="extension">first extension to check</param>
1088
        /// <param name="otherExtension">other extension to check</param>
1089
        /// <returns>true if the extensions match; false otherwise</returns>
1090
        protected bool DoExtensionsMatch(string extension, string otherExtension)
1091
        {
1092
            return extension.Equals(otherExtension, StringComparison.CurrentCultureIgnoreCase);
1093
        }
1094

    
1095
        /// <summary>
1096
        /// Get the install command for the file at the given path. Figures out which
1097
        /// command to use based on the download file path's file extension.
1098
        /// Currently supports .exe, .msi, and .msp.
1099
        /// </summary>
1100
        /// <param name="downloadFilePath">Path to the downloaded update file</param>
1101
        /// <returns>the installer command if the file has one of the given 
1102
        /// extensions; the initial downloadFilePath if not.</returns>
1103
        protected virtual string GetWindowsInstallerCommand(string downloadFilePath)
1104
        {
1105
            string installerExt = Path.GetExtension(downloadFilePath);
1106
            if (DoExtensionsMatch(installerExt, ".exe"))
1107
            {
1108
                return "\"" + downloadFilePath + "\"";
1109
            }
1110
            if (DoExtensionsMatch(installerExt, ".msi"))
1111
            {
1112
                return "msiexec /i \"" + downloadFilePath + "\"";
1113
            }
1114
            if (DoExtensionsMatch(installerExt, ".msp"))
1115
            {
1116
                return "msiexec /p \"" + downloadFilePath + "\"";
1117
            }
1118
            return downloadFilePath;
1119
        }
1120

    
1121
        /// <summary>
1122
        /// Get the install command for the file at the given path. Figures out which
1123
        /// command to use based on the download file path's file extension.
1124
        /// <para>Windows: currently supports .exe, .msi, and .msp.</para>
1125
        /// <para>macOS: currently supports .pkg, .dmg, and .zip.</para>
1126
        /// <para>Linux: currently supports .tar.gz, .deb, and .rpm.</para>
1127
        /// </summary>
1128
        /// <param name="downloadFilePath">Path to the downloaded update file</param>
1129
        /// <returns>the installer command if the file has one of the given 
1130
        /// extensions; the initial downloadFilePath if not.</returns>
1131
        protected virtual string GetInstallerCommand(string downloadFilePath)
1132
        {
1133
            // get the file type
1134
#if NETFRAMEWORK
1135
            return GetWindowsInstallerCommand(downloadFilePath);
1136
#else
1137
            string installerExt = Path.GetExtension(downloadFilePath);
1138
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
1139
            {
1140
                return GetWindowsInstallerCommand(downloadFilePath);
1141
            }
1142
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
1143
            {
1144
                if (DoExtensionsMatch(installerExt, ".pkg") ||
1145
                    DoExtensionsMatch(installerExt, ".dmg"))
1146
                {
1147
                    return "open \"" + downloadFilePath + "\"";
1148
                }
1149
            }
1150
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
1151
            {
1152
                if (DoExtensionsMatch(installerExt, ".deb"))
1153
                {
1154
                    return "sudo dpkg -i \"" + downloadFilePath + "\"";
1155
                }
1156
                if (DoExtensionsMatch(installerExt, ".rpm"))
1157
                {
1158
                    return "sudo rpm -i \"" + downloadFilePath + "\"";
1159
                }
1160
            }
1161
            return downloadFilePath;
1162
#endif
1163
        }
1164

    
1165
        private bool IsZipDownload(string downloadFilePath)
1166
        {
1167
#if NETCORE
1168
            string installerExt = Path.GetExtension(downloadFilePath);
1169
            bool isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
1170
            bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
1171
            if ((isMacOS && DoExtensionsMatch(installerExt, ".zip")) ||
1172
                (isLinux && downloadFilePath.EndsWith(".tar.gz")))
1173
            {
1174
                return true;
1175
            }
1176
#endif
1177
            return false;
1178
        }
1179

    
1180
        /// <summary>
1181
        /// Updates the application via the file at the given path. Figures out which command needs
1182
        /// to be run, sets up the application so that it will start the downloaded file once the
1183
        /// main application stops, and then waits to start the downloaded update.
1184
        /// </summary>
1185
        /// <param name="downloadFilePath">path to the downloaded installer/updater</param>
1186
        /// <returns>the awaitable <see cref="Task"/> for the application quitting</returns>
1187
        protected virtual async Task RunDownloadedInstaller(string downloadFilePath)
1188
        {
1189
            LogWriter.PrintMessage("Running downloaded installer");
1190
            // get the commandline 
1191
            string cmdLine = Environment.CommandLine;
1192
            string workingDir = Utilities.GetFullBaseDirectory();
1193

    
1194
            // generate the batch file path
1195
#if NETFRAMEWORK
1196
            bool isWindows = true;
1197
            bool isMacOS = false;
1198
#else
1199
            bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
1200
            bool isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
1201
#endif
1202
            var extension = isWindows ? ".cmd" : ".sh";
1203
            string batchFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + extension);
1204
            string installerCmd;
1205
            try
1206
            {
1207
                installerCmd = GetInstallerCommand(downloadFilePath);
1208
                if (!string.IsNullOrEmpty(CustomInstallerArguments))
1209
                {
1210
                    installerCmd += " " + CustomInstallerArguments;
1211
                }
1212
            }
1213
            catch (InvalidDataException)
1214
            {
1215
                UIFactory?.ShowUnknownInstallerFormatMessage(downloadFilePath);
1216
                return;
1217
            }
1218

    
1219
            // generate the batch file                
1220
            LogWriter.PrintMessage("Generating batch in {0}", Path.GetFullPath(batchFilePath));
1221

    
1222
            string processID = Process.GetCurrentProcess().Id.ToString();
1223

    
1224
            using (StreamWriter write = new StreamWriter(batchFilePath, false, new UTF8Encoding(false)))
1225
            {
1226
                if (isWindows)
1227
                {
1228
                    write.WriteLine("@echo off");
1229
                    // We should wait until the host process has died before starting the installer.
1230
                    // This way, any DLLs or other items can be replaced properly.
1231
                    // Code from: http://stackoverflow.com/a/22559462/3938401
1232
                    string relaunchAfterUpdate = "";
1233
                    if (RelaunchAfterUpdate)
1234
                    {
1235
                        relaunchAfterUpdate = $@"
1236
                        cd {workingDir}
1237
                        {cmdLine}";
1238
                    }
1239

    
1240
                    string output = $@"
1241
                        set /A counter=0                       
1242
                        setlocal ENABLEDELAYEDEXPANSION
1243
                        :loop
1244
                        set /A counter=!counter!+1
1245
                        if !counter! == 90 (
1246
                            goto :afterinstall
1247
                        )
1248
                        tasklist | findstr ""\<{processID}\>"" > nul
1249
                        if not errorlevel 1 (
1250
                            timeout /t 1 > nul
1251
                            goto :loop
1252
                        )
1253
                        :install
1254
                        {installerCmd}
1255
                        {relaunchAfterUpdate}
1256
                        :afterinstall
1257
                        endlocal";
1258
                    write.Write(output);
1259
                    write.Close();
1260
                }
1261
                else
1262
                {
1263
                    // We should wait until the host process has died before starting the installer.
1264
                    var waitForFinish = $@"
1265
                        COUNTER=0;
1266
                        while ps -p {processID} > /dev/null;
1267
                            do sleep 1;
1268
                            COUNTER=$((++COUNTER));
1269
                            if [ $COUNTER -eq 90 ] 
1270
                            then
1271
                                exit -1;
1272
                            fi;
1273
                        done;
1274
                    ";
1275
                    string relaunchAfterUpdate = "";
1276
                    if (RelaunchAfterUpdate)
1277
                    {
1278
                        relaunchAfterUpdate = $@"{Process.GetCurrentProcess().MainModule.FileName}";
1279
                    }
1280
                    if (IsZipDownload(downloadFilePath)) // .zip on macOS or .tar.gz on Linux
1281
                    {
1282
                        // waiting for finish based on http://blog.joncairns.com/2013/03/wait-for-a-unix-process-to-finish/
1283
                        // use tar to extract
1284
                        var tarCommand = isMacOS ? $"tar -x -f {downloadFilePath} -C \"{workingDir}\"" 
1285
                            : $"tar -xf {downloadFilePath} -C \"{workingDir}\" --overwrite ";
1286
                        var output = $@"
1287
                            {waitForFinish}
1288
                            {tarCommand}
1289
                            {relaunchAfterUpdate}";
1290
                        write.Write(output);
1291
                    }
1292
                    else
1293
                    {
1294
                        string installerExt = Path.GetExtension(downloadFilePath);
1295
                        if (DoExtensionsMatch(installerExt, ".pkg") ||
1296
                            DoExtensionsMatch(installerExt, ".dmg"))
1297
                        {
1298
                            relaunchAfterUpdate = ""; // relaunching not supported for pkg or dmg downloads
1299
                        }
1300
                        var output = $@"
1301
                            {waitForFinish}
1302
                            {installerCmd}
1303
                            {relaunchAfterUpdate}";
1304
                        write.Write(output);
1305
                    }
1306
                    write.Close();
1307
                }
1308
            }
1309

    
1310
            // report
1311
            LogWriter.PrintMessage("Going to execute script at path: {0}", batchFilePath);
1312

    
1313
            // init the installer helper
1314
            _installerProcess = new Process
1315
            {
1316
                StartInfo =
1317
                {
1318
                    FileName = batchFilePath,
1319
                    WindowStyle = ProcessWindowStyle.Hidden,
1320
                    UseShellExecute = false,
1321
                    CreateNoWindow = true
1322
                }
1323
            };
1324
            // start the installer process. the batch file will wait for the host app to close before starting.
1325
            _installerProcess.Start();
1326
            await QuitApplication();
1327
        }
1328

    
1329
        /// <summary>
1330
        /// Quits the application (host application) 
1331
        /// </summary>
1332
        /// <returns>Runs asynchrously, so returns a Task</returns>
1333
        public async Task QuitApplication()
1334
        {
1335
            // quit the app
1336
            _exitHandle?.Set(); // make SURE the loop exits!
1337
                                // In case the user has shut the window that started this Sparkle window/instance, don't crash and burn.
1338
                                // If you have better ideas on how to figure out if they've shut all other windows, let me know...
1339
            try
1340
            {
1341
                await CallFuncConsideringUIThreadsAsync(new Func<Task>(async () =>
1342
                {
1343
                    if (CloseApplicationAsync != null)
1344
                    {
1345
                        await CloseApplicationAsync.Invoke();
1346
                    }
1347
                    else if (CloseApplication != null)
1348
                    {
1349
                        CloseApplication.Invoke();
1350

    
1351
                    }
1352
                    else
1353
                    {
1354
                        // Because the download/install window is usually on a separate thread,
1355
                        // send dual shutdown messages via both the sync context (kills "main" app)
1356
                        // and the current thread (kills current thread)
1357
                        UIFactory?.Shutdown();
1358
                    }
1359
                }));
1360
            }
1361
            catch (Exception e)
1362
            {
1363
                LogWriter.PrintMessage(e.Message);
1364
            }
1365
        }
1366

    
1367
        /// <summary>
1368
        /// Apps may need, for example, to let user save their work
1369
        /// </summary>
1370
        /// <returns>true if it's OK to run the installer</returns>
1371
        private async Task<bool> AskApplicationToSafelyCloseUp()
1372
        {
1373
            try
1374
            {
1375
                // In case the user has shut the window that started this Sparkle window/instance, don't crash and burn.
1376
                // If you have better ideas on how to figure out if they've shut all other windows, let me know...
1377
                if (PreparingToExitAsync != null)
1378
                {
1379
                    var args = new CancelEventArgs();
1380
                    await PreparingToExitAsync(this, args);
1381
                    return !args.Cancel;
1382
                }
1383
                else if (PreparingToExit != null)
1384
                {
1385
                    var args = new CancelEventArgs();
1386
                    PreparingToExit(this, args);
1387
                    return !args.Cancel;
1388
                }
1389
            }
1390
            catch (Exception e)
1391
            {
1392
                LogWriter.PrintMessage(e.Message);
1393
            }
1394
            return true;
1395
        }
1396

    
1397

    
1398
        /// <summary>
1399
        /// Check for updates, using UI interaction appropriate for if the user initiated the update request
1400
        /// </summary>
1401
        public async Task<UpdateInfo> CheckForUpdatesAtUserRequest()
1402
        {
1403
            CheckingForUpdatesWindow = UIFactory?.ShowCheckingForUpdates();
1404
            if (CheckingForUpdatesWindow != null)
1405
            {
1406
                CheckingForUpdatesWindow.UpdatesUIClosing += CheckingForUpdatesWindow_Closing; // to detect canceling
1407
                CheckingForUpdatesWindow.Show();
1408
            }
1409

    
1410
            UpdateInfo updateData = await CheckForUpdates(); // handles UpdateStatus.UpdateAvailable (in terms of UI)
1411
            if (CheckingForUpdatesWindow != null) // if null, user closed 'Checking for Updates...' window or the UIFactory was null
1412
            {
1413
                CheckingForUpdatesWindow?.Close();
1414
                CallFuncConsideringUIThreads(() => 
1415
                {
1416
                    switch (updateData.Status)
1417
                    {
1418
                        case UpdateStatus.UpdateNotAvailable:
1419
                            UIFactory?.ShowVersionIsUpToDate();
1420
                            break;
1421
                        case UpdateStatus.UserSkipped:
1422
                            UIFactory?.ShowVersionIsSkippedByUserRequest(); // they can get skipped version from Configuration
1423
                            break;
1424
                        case UpdateStatus.CouldNotDetermine:
1425
                            UIFactory?.ShowCannotDownloadAppcast(AppCastUrl);
1426
                            break;
1427
                    }
1428
                });
1429
            }
1430

    
1431
            return updateData;// in this case, we've already shown UI talking about the new version
1432
        }
1433

    
1434
        private void CheckingForUpdatesWindow_Closing(object sender, EventArgs e)
1435
        {
1436
            CheckingForUpdatesWindow = null;
1437
        }
1438

    
1439
        /// <summary>
1440
        /// Check for updates, using interaction appropriate for where the user doesn't know you're doing it, so be polite.
1441
        /// Basically, this checks for updates without showing a UI. However, if a UIFactory is set and an update
1442
        /// is found, an update UI will be shown!
1443
        /// </summary>
1444
        public async Task<UpdateInfo> CheckForUpdatesQuietly()
1445
        {
1446
            return await CheckForUpdates();
1447
        }
1448

    
1449
        /// <summary>
1450
        /// Does a one-off check for updates
1451
        /// </summary>
1452
        private async Task<UpdateInfo> CheckForUpdates()
1453
        {
1454
            // artificial delay -- if internet is super fast and the update check is super fast, the flash (fast show/hide) of the
1455
            // 'Checking for Updates...' window is very disorienting, so we add an artificial delay
1456
            bool isUserManuallyCheckingForUpdates = CheckingForUpdatesWindow != null;
1457
            if (isUserManuallyCheckingForUpdates)
1458
            {
1459
                await Task.Delay(250);
1460
            }
1461
            UpdateCheckStarted?.Invoke(this);
1462
            Configuration config = Configuration;
1463

    
1464
            // check if update is required
1465
            _latestDownloadedUpdateInfo = await GetUpdateStatus(config);
1466
            List<AppCastItem> updates = _latestDownloadedUpdateInfo.Updates;
1467
            if (_latestDownloadedUpdateInfo.Status == UpdateStatus.UpdateAvailable)
1468
            {
1469
                // show the update window
1470
                LogWriter.PrintMessage("Update needed from version {0} to version {1}", config.InstalledVersion, updates[0].Version);
1471

    
1472
                UpdateDetectedEventArgs ev = new UpdateDetectedEventArgs
1473
                {
1474
                    NextAction = NextUpdateAction.ShowStandardUserInterface,
1475
                    ApplicationConfig = config,
1476
                    LatestVersion = updates[0],
1477
                    AppCastItems = updates
1478
                };
1479

    
1480
                // if the client wants to intercept, send an event
1481
                if (UpdateDetected != null)
1482
                {
1483
                    UpdateDetected(this, ev);
1484
                    // if the client wants the default UI then show them
1485
                    switch (ev.NextAction)
1486
                    {
1487
                        case NextUpdateAction.ShowStandardUserInterface:
1488
                            LogWriter.PrintMessage("Showing standard update UI");
1489
                            OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates));
1490
                            break;
1491
                    }
1492
                }
1493
                else
1494
                {
1495
                    // otherwise just go forward with the UI notification
1496
                    if (isUserManuallyCheckingForUpdates && CheckingForUpdatesWindow != null)
1497
                    {
1498
                        ShowUpdateNeededUI(updates);
1499
                    }
1500
                }
1501
            }
1502
            UpdateCheckFinished?.Invoke(this, _latestDownloadedUpdateInfo.Status);
1503
            return _latestDownloadedUpdateInfo;
1504
        }
1505

    
1506
        /// <summary>
1507
        /// Cancels an in-progress download and deletes the temporary file.
1508
        /// </summary>
1509
        public void CancelFileDownload()
1510
        {
1511
            LogWriter.PrintMessage("Canceling download...");
1512
            if (UpdateDownloader != null && UpdateDownloader.IsDownloading)
1513
            {
1514
                UpdateDownloader.CancelDownload();
1515
            }
1516
        }
1517

    
1518
        /// <summary>
1519
        /// Events should always be fired on the thread that started the Sparkle object.
1520
        /// Used for events that are fired after coming from an update available window
1521
        /// or the download progress window.
1522
        /// Basically, if ShowsUIOnMainThread, just invokes the action. Otherwise,
1523
        /// uses the SynchronizationContext to call the action. Ensures that the action
1524
        /// is always on the main thread.
1525
        /// </summary>
1526
        /// <param name="action"></param>
1527
        private void CallFuncConsideringUIThreads(Action action)
1528
        {
1529
            if (ShowsUIOnMainThread)
1530
            {
1531
                action?.Invoke();
1532
            }
1533
            else
1534
            {
1535
                _syncContext.Post((state) => action?.Invoke(), null);
1536
            }
1537
        }
1538

    
1539
        /// <summary>
1540
        /// Events should always be fired on the thread that started the Sparkle object.
1541
        /// Used for events that are fired after coming from an update available window
1542
        /// or the download progress window.
1543
        /// Basically, if ShowsUIOnMainThread, just invokes the action. Otherwise,
1544
        /// uses the SynchronizationContext to call the action. Ensures that the action
1545
        /// is always on the main thread.
1546
        /// </summary>
1547
        /// <param name="action"></param>
1548
        private async Task CallFuncConsideringUIThreadsAsync(Func<Task> action)
1549
        {
1550
            if (ShowsUIOnMainThread)
1551
            {
1552
                await action?.Invoke();
1553
            }
1554
            else
1555
            {
1556
                _syncContext.Post(async (state) => await action?.Invoke(), null);
1557
            }
1558
        }
1559

    
1560
        /// <summary>
1561
        /// </summary>
1562
        /// <param name="sender">not used.</param>
1563
        /// <param name="args">Info on the user response and what update item they responded to</param>
1564
        private async void OnUserWindowUserResponded(object sender, UpdateResponseEventArgs args)
1565
        {
1566
            LogWriter.PrintMessage("Update window response: {0}", args.Result);
1567
            var currentItem = args.UpdateItem;
1568
            var result = args.Result;
1569
            if (string.IsNullOrWhiteSpace(_downloadTempFileName))
1570
            {
1571
                // we need the download file name in order to tell the user the skipped version
1572
                // file path and/or to run the installer
1573
                _downloadTempFileName = await GetDownloadPathForAppCastItem(currentItem);
1574
            }
1575
            if (result == UpdateAvailableResult.SkipUpdate)
1576
            {
1577
                // skip this version
1578
                Configuration.SetVersionToSkip(currentItem.Version);
1579
                CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); });
1580
            }
1581
            else if (result == UpdateAvailableResult.InstallUpdate)
1582
            {
1583
                await CallFuncConsideringUIThreadsAsync(async () => 
1584
                {
1585
                    UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem));
1586
                    if (UserInteractionMode == UserInteractionMode.DownloadNoInstall && File.Exists(_downloadTempFileName))
1587
                    {
1588
                        // Binary should already be downloaded. Run it!
1589
                        ProgressWindowCompleted(this, new DownloadInstallEventArgs(true));
1590
                    }
1591
                    else
1592
                    {
1593
                        // download the binaries
1594
                        await InitAndBeginDownload(currentItem);
1595
                    }
1596
                });
1597
            }
1598
            else if (result == UpdateAvailableResult.RemindMeLater && currentItem != null)
1599
            {
1600
                CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); });
1601
            }
1602
            UpdateAvailableWindow?.Close();
1603
            UpdateAvailableWindow = null; // done using the window so don't hold onto reference
1604
            CheckingForUpdatesWindow?.Close();
1605
            CheckingForUpdatesWindow = null;
1606
        }
1607

    
1608
        /// <summary>
1609
        /// This method will be executed as worker thread
1610
        /// </summary>
1611
        private async void OnWorkerDoWork(object sender, DoWorkEventArgs e)
1612
        {
1613
            // store the did run once feature
1614
            bool goIntoLoop = true;
1615
            bool checkTSP = true;
1616
            bool doInitialCheck = _doInitialCheck;
1617
            bool isInitialCheck = true;
1618

    
1619
            // start our lifecycles
1620
            do
1621
            {
1622
                if (_cancelToken.IsCancellationRequested)
1623
                {
1624
                    break;
1625
                }
1626
                // set state
1627
                bool bUpdateRequired = false;
1628

    
1629
                // notify
1630
                LoopStarted?.Invoke(this);
1631

    
1632
                // report status
1633
                if (doInitialCheck)
1634
                {
1635
                    // report status
1636
                    LogWriter.PrintMessage("Starting update loop...");
1637

    
1638
                    // read the config
1639
                    LogWriter.PrintMessage("Reading config...");
1640
                    Configuration config = Configuration;
1641

    
1642
                    // calc CheckTasp
1643
                    bool checkTSPInternal = checkTSP;
1644

    
1645
                    if (isInitialCheck && checkTSPInternal)
1646
                    {
1647
                        checkTSPInternal = !_forceInitialCheck;
1648
                    }
1649

    
1650
                    // check if it's ok the recheck to software state
1651
                    TimeSpan csp = DateTime.Now - config.LastCheckTime;
1652

    
1653
                    if (!checkTSPInternal || csp >= _checkFrequency)
1654
                    {
1655
                        checkTSP = true;
1656
                        // when sparkle will be deactivated wait another cycle
1657
                        if (config.CheckForUpdate == true)
1658
                        {
1659
                            // update the runonce feature
1660
                            goIntoLoop = !config.IsFirstRun;
1661

    
1662
                            // check if update is required
1663
                            if (_cancelToken.IsCancellationRequested || !goIntoLoop)
1664
                            {
1665
                                break;
1666
                            }
1667
                            _latestDownloadedUpdateInfo = await GetUpdateStatus(config);
1668
                            if (_cancelToken.IsCancellationRequested)
1669
                            {
1670
                                break;
1671
                            }
1672
                            bUpdateRequired = _latestDownloadedUpdateInfo.Status == UpdateStatus.UpdateAvailable;
1673
                            if (bUpdateRequired)
1674
                            {
1675
                                List<AppCastItem> updates = _latestDownloadedUpdateInfo.Updates;
1676
                                // show the update window
1677
                                LogWriter.PrintMessage("Update needed from version {0} to version {1}", config.InstalledVersion, updates[0].Version);
1678

    
1679
                                // send notification if needed
1680
                                UpdateDetectedEventArgs ev = new UpdateDetectedEventArgs
1681
                                {
1682
                                    NextAction = NextUpdateAction.ShowStandardUserInterface,
1683
                                    ApplicationConfig = config,
1684
                                    LatestVersion = updates[0],
1685
                                    AppCastItems = updates
1686
                                };
1687
                                UpdateDetected?.Invoke(this, ev);
1688

    
1689
                                // check results
1690
                                switch (ev.NextAction)
1691
                                {
1692
                                    case NextUpdateAction.PerformUpdateUnattended:
1693
                                        {
1694
                                            LogWriter.PrintMessage("Unattended update desired from consumer");
1695
                                            UserInteractionMode = UserInteractionMode.DownloadAndInstall;
1696
                                            OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates));
1697
                                            break;
1698
                                        }
1699
                                    case NextUpdateAction.ProhibitUpdate:
1700
                                        {
1701
                                            LogWriter.PrintMessage("Update prohibited from consumer");
1702
                                            break;
1703
                                        }
1704
                                    default:
1705
                                        {
1706
                                            LogWriter.PrintMessage("Preparing to show standard update UI");
1707
                                            OnWorkerProgressChanged(_taskWorker, new ProgressChangedEventArgs(1, updates));
1708
                                            break;
1709
                                        }
1710
                                }
1711
                            }
1712
                        }
1713
                        else
1714
                        {
1715
                            LogWriter.PrintMessage("Check for updates disabled");
1716
                        }
1717
                    }
1718
                    else
1719
                    {
1720
                        LogWriter.PrintMessage("Update check performed within the last {0} minutes!", _checkFrequency.TotalMinutes);
1721
                    }
1722
                }
1723
                else
1724
                {
1725
                    LogWriter.PrintMessage("Initial check prohibited, going to wait");
1726
                    doInitialCheck = true;
1727
                }
1728

    
1729
                // checking is done; this is now the "let's wait a while" section
1730

    
1731
                // reset initial check
1732
                isInitialCheck = false;
1733

    
1734
                // notify
1735
                LoopFinished?.Invoke(this, bUpdateRequired);
1736

    
1737
                // report wait statement
1738
                LogWriter.PrintMessage("Sleeping for an other {0} minutes, exit event or force update check event", _checkFrequency.TotalMinutes);
1739

    
1740
                // wait for
1741
                if (!goIntoLoop || _cancelToken.IsCancellationRequested)
1742
                {
1743
                    break;
1744
                }
1745

    
1746
                // build the event array
1747
                WaitHandle[] handles = new WaitHandle[1];
1748
                handles[0] = _exitHandle;
1749

    
1750
                // wait for any
1751
                if (_cancelToken.IsCancellationRequested)
1752
                {
1753
                    break;
1754
                }
1755
                int i = WaitHandle.WaitAny(handles, _checkFrequency);
1756
                if (_cancelToken.IsCancellationRequested)
1757
                {
1758
                    break;
1759
                }
1760
                if (WaitHandle.WaitTimeout == i)
1761
                {
1762
                    LogWriter.PrintMessage("{0} minutes are over", _checkFrequency.TotalMinutes);
1763
                    continue;
1764
                }
1765

    
1766
                // check the exit handle
1767
                if (i == 0)
1768
                {
1769
                    LogWriter.PrintMessage("Got exit signal");
1770
                    break;
1771
                }
1772

    
1773
                // check an other check needed
1774
                if (i == 1)
1775
                {
1776
                    LogWriter.PrintMessage("Got force update check signal");
1777
                    checkTSP = false;
1778
                }
1779
                if (_cancelToken.IsCancellationRequested)
1780
                {
1781
                    break;
1782
                }
1783
            } while (goIntoLoop);
1784

    
1785
            // reset the islooping handle
1786
            _loopingHandle.Reset();
1787
        }
1788

    
1789
        /// <summary>
1790
        /// This method will be notified by the SparkleUpdater loop when
1791
        /// some update info has been downloaded. If the info has been 
1792
        /// downloaded fully (e.ProgressPercentage == 1), the UI
1793
        /// for downloading updates will be shown (if not downloading silently)
1794
        /// or the download will be performed (if downloading silently).
1795
        /// </summary>
1796
        private void OnWorkerProgressChanged(object sender, ProgressChangedEventArgs e)
1797
        {
1798
            switch (e.ProgressPercentage)
1799
            {
1800
                case 1:
1801
                    UpdatesHaveBeenDownloaded(e.UserState as List<AppCastItem>);
1802
                    break;
1803
                case 0:
1804
                    LogWriter.PrintMessage(e.UserState.ToString());
1805
                    break;
1806
            }
1807
        }
1808

    
1809
        /// <summary>
1810
        /// Updates from appcast have been downloaded from the server
1811
        /// </summary>
1812
        /// <param name="updates">updates to be installed</param>
1813
        private async void UpdatesHaveBeenDownloaded(List<AppCastItem> updates)
1814
        {
1815
            if (updates != null)
1816
            {
1817
                if (IsDownloadingSilently())
1818
                {
1819
                    await InitAndBeginDownload(updates[0]); // install only latest
1820
                }
1821
                else
1822
                {
1823
                    // show the update UI
1824
                    ShowUpdateNeededUI(updates);
1825
                }
1826
            }
1827
        }
1828
    }
1829
}
클립보드 이미지 추가 (최대 크기: 500 MB)