Added silent install option
[pithos-ms-client] / trunk / NetSparkle / NetSparkle.cs
1 using System;
2 using System.ComponentModel;
3 using System.Reflection;
4 using System.Threading;
5 using System.Net;
6 using System.Windows.Forms;
7 using System.Drawing;
8 using System.Security.Cryptography.X509Certificates;
9 using System.Net.Security;
10 using log4net;
11 using Point = System.Drawing.Point;
12
13 namespace AppLimit.NetSparkle
14 {
15     public delegate void LoopStartedOperation(Object sender);
16     public delegate void LoopFinishedOperation(Object sender, Boolean UpdateRequired);
17
18     /// <summary>
19     /// Everytime when netsparkle detects an update the 
20     /// consumer can decide what should happen as next with the help 
21     /// of the UpdateDatected event
22     /// </summary>
23     public enum nNextUpdateAction
24     {
25         showStandardUserInterface = 1,
26         performUpdateUnattended = 2,
27         prohibitUpdate = 3
28     }
29
30     /// <summary>
31     /// Contains all information for the update detected event
32     /// </summary>
33     public class UpdateDetectedEventArgs : EventArgs
34     {
35         public nNextUpdateAction NextAction;
36         public NetSparkleConfiguration ApplicationConfig;
37         public NetSparkleAppCastItem LatestVersion;        
38     }
39
40     /// <summary>
41     /// This delegate will be used when an update was detected to allow library 
42     /// consumer to add own user interface capabilities.    
43     /// </summary>
44     /// <param name="sender"></param>
45     /// <param name="e"></param>
46     public delegate void UpdateDetected(Object sender, UpdateDetectedEventArgs e);
47
48     public class Sparkle : IDisposable
49     {
50         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
51
52         private BackgroundWorker _worker = new BackgroundWorker();
53
54         private String _AppCastUrl;
55         private String _AppReferenceAssembly;
56
57         private Boolean _DoInitialCheck;
58         private Boolean _ForceInitialCheck;
59
60         private EventWaitHandle _exitHandle;
61         private EventWaitHandle _loopingHandle;
62         
63         private NetSparkleMainWindows _DiagnosticWindow;
64
65         private TimeSpan _CheckFrequency;
66
67         /// <summary>
68         /// Enables system profiling against a profile server
69         /// </summary>
70         public Boolean EnableSystemProfiling = false;
71
72         /// <summary>
73         /// Hides the release notes view when an update was found. This 
74         /// mode is switched on automatically when no sparkle:releaseNotesLink
75         /// tag was found in the app cast         
76         /// </summary>
77         public Boolean HideReleaseNotes = false;
78
79         /// <summary>
80         /// Contains the profile url for System profiling
81         /// </summary>
82         public Uri SystemProfileUrl;
83
84         /// <summary>
85         /// This event will be raised when a check loop will be started
86         /// </summary>
87         public event LoopStartedOperation checkLoopStarted;
88
89         /// <summary>
90         /// This event will be raised when a check loop is finished
91         /// </summary>
92         public event LoopFinishedOperation checkLoopFinished;
93
94         /// <summary>
95         /// This event can be used to override the standard user interface
96         /// process when an update is detected
97         /// </summary>
98         public event UpdateDetected updateDetected;
99
100         /// <summary>
101         /// This property holds an optional application icon
102         /// which will be displayed in the software update dialog. The icon has
103         /// to be 48x48 pixels.
104         /// </summary>
105         public Image ApplicationIcon { get; set; }
106
107         /// <summary>
108         /// This property returns an optional application icon 
109         /// which will displayed in the windows as self
110         /// </summary>
111         public Icon ApplicationWindowIcon { get; set; }
112
113         /// <summary>
114         /// This property enables a diagnostic window for debug reasons
115         /// </summary>
116         public Boolean ShowDiagnosticWindow { get; set; }
117
118         /// <summary>
119         /// This property enables the silent mode, this means 
120         /// the application will be updated without user interaction
121         /// </summary>
122         public Boolean EnableSilentMode { get; set; }
123
124         /// <summary>
125         /// This property returns true when the upadete loop is running
126         /// and files when the loop is not running
127         /// </summary>
128         public Boolean IsUpdateLoopRunning
129         {
130             get
131             {
132                 return _loopingHandle.WaitOne(0);
133             }
134         }
135
136         /// <summary>
137         /// This property defines if we trust every ssl connection also when 
138         /// this connection has not a valid cert
139         /// </summary>
140         public Boolean TrustEverySSLConnection { get; set; }
141
142         public string LatestVersion { get; set; }
143
144         /// <summary>
145         /// ctor which needs the appcast url
146         /// </summary>
147         /// <param name="appcastUrl"></param>
148         public Sparkle(String appcastUrl)
149             : this(appcastUrl, null)
150         { }
151
152         /// <summary>
153         /// ctor which needs the appcast url and a referenceassembly
154         /// </summary>
155         public Sparkle(String appcastUrl, String referenceAssembly)
156             : this(appcastUrl, referenceAssembly, false)
157         { }
158
159         /// <summary>
160         /// ctor which needs the appcast url and a referenceassembly
161         /// </summary>        
162         public Sparkle(String appcastUrl, String referenceAssembly, Boolean ShowDiagnostic)
163         {
164             // preconfige ssl trust
165             TrustEverySSLConnection = false;
166
167             // configure ssl cert link
168             ServicePointManager.ServerCertificateValidationCallback += RemoteCertificateValidation;
169
170             // enable visual style to ensure that we have XP style or higher
171             // also in WPF applications
172             System.Windows.Forms.Application.EnableVisualStyles();
173
174             // reset vars
175             ApplicationIcon = null;
176             _AppReferenceAssembly = null;            
177
178             // set var
179             ShowDiagnosticWindow = ShowDiagnostic;
180
181             // create the diagnotic window
182             _DiagnosticWindow = new NetSparkleMainWindows();
183
184             // set the reference assembly
185             if (referenceAssembly != null)
186             {
187                 _AppReferenceAssembly = referenceAssembly;
188                 _DiagnosticWindow.Report("Checking the following file: " + _AppReferenceAssembly);
189             }
190
191             // show if needed
192             ShowDiagnosticWindowIfNeeded();            
193
194             // adjust the delegates
195             _worker.WorkerReportsProgress = true;
196             _worker.DoWork += new DoWorkEventHandler(_worker_DoWork);
197             _worker.ProgressChanged += new ProgressChangedEventHandler(_worker_ProgressChanged);
198
199             // build the wait handle
200             _exitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
201             _loopingHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
202
203             // set the url
204             _AppCastUrl = appcastUrl;
205             _DiagnosticWindow.Report("Using the following url: " + _AppCastUrl);            
206         }
207
208         /// <summary>
209         /// The method starts a NetSparkle background loop
210         /// If NetSparkle is configured to check for updates on startup, proceeds to perform 
211         /// the check. You should only call this function when your app is initialized and 
212         /// shows its main window.        
213         /// </summary>        
214         /// <param name="doInitialCheck"></param>
215         public void StartLoop(Boolean doInitialCheck)
216         {
217             StartLoop(doInitialCheck, false);
218         }
219
220         /// <summary>
221         /// The method starts a NetSparkle background loop
222         /// If NetSparkle is configured to check for updates on startup, proceeds to perform 
223         /// the check. You should only call this function when your app is initialized and 
224         /// shows its main window.
225         /// </summary>
226         /// <param name="doInitialCheck"></param>
227         /// <param name="checkFrequency"></param>
228         public void StartLoop(Boolean doInitialCheck, TimeSpan checkFrequency)
229         {
230             StartLoop(doInitialCheck, false, checkFrequency);
231         }
232
233         /// <summary>
234         /// The method starts a NetSparkle background loop
235         /// If NetSparkle is configured to check for updates on startup, proceeds to perform 
236         /// the check. You should only call this function when your app is initialized and 
237         /// shows its main window.
238         /// </summary>
239         /// <param name="doInitialCheck"></param>
240         /// <param name="forceInitialCheck"></param>
241         public void StartLoop(Boolean doInitialCheck, Boolean forceInitialCheck)
242         {
243             StartLoop(doInitialCheck, forceInitialCheck, TimeSpan.FromHours(24));
244         }
245
246         /// <summary>
247         /// The method starts a NetSparkle background loop
248         /// If NetSparkle is configured to check for updates on startup, proceeds to perform 
249         /// the check. You should only call this function when your app is initialized and 
250         /// shows its main window.
251         /// </summary>
252         /// <param name="doInitialCheck"></param>
253         /// <param name="forceInitialCheck"></param>
254         /// <param name="checkFrequency"></param>
255         public void StartLoop(Boolean doInitialCheck, Boolean forceInitialCheck, TimeSpan checkFrequency)
256         {
257             // first set the event handle
258             _loopingHandle.Set();
259
260             // Start the helper thread as a background worker to 
261             // get well ui interaction                        
262
263             // show if needed
264             ShowDiagnosticWindowIfNeeded();
265
266             // store infos
267             _DoInitialCheck = doInitialCheck;
268             _ForceInitialCheck = forceInitialCheck;
269             _CheckFrequency = checkFrequency;
270
271             // create and configure the worker
272             _DiagnosticWindow.Report("Starting background worker");
273
274             // start the work
275             _worker.RunWorkerAsync();
276         }
277
278         /// <summary>
279         /// This method will stop the sparkle background loop and is called
280         /// through the disposable interface automatically
281         /// </summary>
282         public void StopLoop()
283         {
284             // ensure the work will finished
285             _exitHandle.Set();                       
286         }
287
288         /// <summary>
289         /// Is called in the using context and will stop all background activities
290         /// </summary>
291         public void Dispose()
292         {
293             StopLoop();
294         }
295
296         /// <summary>
297         /// This method updates the profile information which can be sended to the server if enabled    
298         /// </summary>
299         /// <param name="config"></param>
300         public void UpdateSystemProfileInformation(NetSparkleConfiguration config)
301         {
302             // check if profile data is enabled
303             if (!EnableSystemProfiling)
304                 return;
305
306             // check if we need an update
307             if (DateTime.Now - config.LastProfileUpdate < new TimeSpan(7, 0, 0, 0))
308                 return;
309
310             // touch the profile update time
311             config.TouchProfileTime();
312
313             // start the profile thread
314             Thread t = new Thread(ProfileDataThreadStart);
315             t.Start(config);
316         }
317
318         /// <summary>
319         /// Profile data thread
320         /// </summary>
321         /// <param name="obj"></param>
322         private void ProfileDataThreadStart(object obj)
323         {
324             try
325             {
326                 if (SystemProfileUrl != null)
327                 {
328                     // get the config
329                     NetSparkleConfiguration config = obj as NetSparkleConfiguration;
330
331                     // collect data
332                     NetSparkleDeviceInventory inv = new NetSparkleDeviceInventory(config);
333                     inv.CollectInventory();
334
335                     // build url
336                     String requestUrl = inv.BuildRequestUrl(SystemProfileUrl.ToString() + "?");
337
338                     HttpWebRequest.DefaultWebProxy = HttpWebRequest.GetSystemWebProxy();
339
340                     // perform the webrequest
341                     HttpWebRequest request = HttpWebRequest.Create(requestUrl) as HttpWebRequest;
342                     using (WebResponse response = request.GetResponse())
343                     {
344                         // close the response 
345                         response.Close();
346                     }
347                 }
348             }
349             catch (Exception ex)
350             {
351                 // No exception during data send 
352                 ReportDiagnosticMessage(ex.Message);
353             }
354         }
355
356         /// <summary>
357         /// This method checks if an update is required. During this process the appcast
358         /// will be downloaded and checked against the reference assembly. Ensure that
359         /// the calling process has access to the internet and read access to the 
360         /// reference assembly. This method is also called from the background loops.
361         /// </summary>
362         /// <param name="config"></param>
363         /// <returns></returns>
364         public Boolean IsUpdateRequired(NetSparkleConfiguration config, out NetSparkleAppCastItem latestVersion)
365         {
366             // report
367             ReportDiagnosticMessage("Downloading and checking appcast");
368
369             // init the appcast
370             NetSparkleAppCast cast = new NetSparkleAppCast(_AppCastUrl, config);
371
372             // check if any updates are available
373             try
374             {
375                 latestVersion = cast.GetLatestVersion();
376             }
377             catch (Exception e)
378             {
379                 // show the exeception message 
380                 ReportDiagnosticMessage("Error during app cast download: " + e.Message);
381
382                 // just null the version info
383                 latestVersion = null;
384             }
385
386             if (latestVersion == null)
387             {
388                 ReportDiagnosticMessage("No version information in app cast found");
389                 return false;
390             }
391             else
392             {
393                 ReportDiagnosticMessage("Lastest version on the server is " + latestVersion.Version);
394             }
395
396             // set the last check time
397             ReportDiagnosticMessage("Touch the last check timestamp");
398             config.TouchCheckTime();
399
400             // check if the available update has to be skipped
401             if (latestVersion.Version.Equals(config.SkipThisVersion))
402             {
403                 ReportDiagnosticMessage("Latest update has to be skipped (user decided to skip version " + config.SkipThisVersion + ")");
404                 return false;
405             }
406
407             // check if the version will be the same then the installed version
408             Version v1 = new Version(config.InstalledVersion);
409             Version v2 = new Version(latestVersion.Version);
410
411             if (v2 <= v1)
412             {
413                 ReportDiagnosticMessage("Installed version is valid, no update needed (" + config.InstalledVersion + ")");
414                 return false;
415             }
416
417             // ok we need an update
418             return true;
419         }
420
421         /// <summary>
422         /// This method reads the local sparkle configuration for the given
423         /// reference assembly
424         /// </summary>
425         /// <param name="AppReferenceAssembly"></param>
426         /// <returns></returns>
427         public NetSparkleConfiguration GetApplicationConfig()
428         {
429             NetSparkleConfiguration config;
430             config = new NetSparkleConfiguration(_AppReferenceAssembly);
431             return config;
432         }
433
434         /// <summary>
435         /// This method shows the update ui and allows to perform the 
436         /// update process
437         /// </summary>
438         /// <param name="currentItem"></param>
439         public void ShowUpdateNeededUI(NetSparkleAppCastItem currentItem)
440         {
441
442             // create the form
443             NetSparkleForm frm = new NetSparkleForm(currentItem, ApplicationIcon, ApplicationWindowIcon);
444
445             // configure the form
446             frm.TopMost = true;
447
448             if (HideReleaseNotes)
449                 frm.RemoveReleaseNotesControls();
450
451             
452             // show it
453             DialogResult dlgResult = frm.ShowDialog();
454             
455
456             if (dlgResult == DialogResult.No)
457             {
458                 // skip this version
459                 NetSparkleConfiguration config = new NetSparkleConfiguration(_AppReferenceAssembly);
460                 config.SetVersionToSkip(currentItem.Version);
461             }
462             else if (dlgResult == DialogResult.Yes)
463             {
464                 // download the binaries
465                 InitDownloadAndInstallProcess(currentItem);
466             }
467             
468         }
469
470         /// <summary>
471         /// This method reports a message in the diagnostic window
472         /// </summary>
473         /// <param name="message"></param>
474         public void ReportDiagnosticMessage(String message)
475         {
476             Log.Info(message);
477             if (_DiagnosticWindow.InvokeRequired)
478             {
479                 _DiagnosticWindow.Invoke(new Action<String>(ReportDiagnosticMessage), message);                
480             }
481             else
482             {
483                 _DiagnosticWindow.Report(message);
484             }
485         }
486
487         /// <summary>
488         /// This method will be executed as worker thread
489         /// </summary>
490         /// <param name="sender"></param>
491         /// <param name="e"></param>
492         void _worker_DoWork(object sender, DoWorkEventArgs e)
493         {
494             // store the did run once feature
495             Boolean goIntoLoop = true;
496             Boolean checkTSP = true;
497             Boolean doInitialCheck = _DoInitialCheck;
498             Boolean isInitialCheck = true;
499
500             // start our lifecycles
501             do
502             {
503                 // set state
504                 Boolean bUpdateRequired = false;
505
506                 // notify
507                 if (checkLoopStarted != null)
508                     checkLoopStarted(this);
509
510                 // report status
511                 if (doInitialCheck == false)
512                 {
513                     ReportDiagnosticMessage("Initial check prohibited, going to wait");
514                     doInitialCheck = true;
515                     goto WaitSection;
516                 }
517
518                 // report status
519                 ReportDiagnosticMessage("Starting update loop...");
520
521                 // read the config
522                 ReportDiagnosticMessage("Reading config...");
523                 NetSparkleConfiguration config = GetApplicationConfig();
524
525                 // calc CheckTasp
526                 Boolean checkTSPInternal = checkTSP;
527
528                 if (isInitialCheck && checkTSPInternal)
529                     checkTSPInternal = !_ForceInitialCheck;
530
531                 // check if it's ok the recheck to software state
532                 if (checkTSPInternal)
533                 {
534                     TimeSpan csp = DateTime.Now - config.LastCheckTime;
535                     if (csp < _CheckFrequency)
536                     {
537                         ReportDiagnosticMessage(String.Format("Update check performed within the last {0} minutes!", _CheckFrequency.TotalMinutes));
538                         goto WaitSection;
539                     }
540                 }
541                 else
542                     checkTSP = true;
543
544                 // when sparkle will be deactivated wait an other cycle
545                 if (config.CheckForUpdate == false)
546                 {
547                     ReportDiagnosticMessage("Check for updates disabled");
548                     goto WaitSection;
549                 }
550
551                 // update the runonce feature
552                 goIntoLoop = !config.DidRunOnce;
553
554                 // update profile information is needed
555                 UpdateSystemProfileInformation(config);
556
557                 // check if update is required
558                 NetSparkleAppCastItem latestVersion = null;
559                 bUpdateRequired = IsUpdateRequired(config, out latestVersion);
560                 this.LatestVersion = latestVersion.Version??"";                
561                 if (!bUpdateRequired)
562                     goto WaitSection;
563
564                 // show the update window
565                 ReportDiagnosticMessage("Update needed from version " + config.InstalledVersion + " to version " + latestVersion.Version);
566
567                 // send notification if needed
568                 UpdateDetectedEventArgs ev = new UpdateDetectedEventArgs() { NextAction = nNextUpdateAction.showStandardUserInterface, ApplicationConfig = config, LatestVersion = latestVersion };
569                 if (updateDetected != null)
570                     updateDetected(this, ev);
571                 
572                 // check results
573                 switch(ev.NextAction)
574                 {
575                     case nNextUpdateAction.performUpdateUnattended:
576                         {
577                             ReportDiagnosticMessage("Unattended update whished from consumer");
578                             EnableSilentMode = true;
579                             _worker.ReportProgress(1, latestVersion);
580                             break;
581                         }
582                     case nNextUpdateAction.prohibitUpdate:
583                         {
584                             ReportDiagnosticMessage("Update prohibited from consumer");
585                             break;
586                         }
587                     case nNextUpdateAction.showStandardUserInterface:
588                     default:
589                         {
590                             ReportDiagnosticMessage("Standard UI update whished from consumer");
591                             _worker.ReportProgress(1, latestVersion);
592                             break;
593                         }
594                 }                
595
596             WaitSection:
597                 // reset initialcheck
598                 isInitialCheck = false;
599
600                 // notify
601                 if (checkLoopFinished != null)
602                     checkLoopFinished(this, bUpdateRequired);
603
604                 // report wait statement
605                 ReportDiagnosticMessage(String.Format("Sleeping for an other {0} minutes, exit event or force update check event", _CheckFrequency.TotalMinutes));
606
607                 // wait for
608                 if (!goIntoLoop)
609                     break;
610                 else
611                 {
612                     // build the event array
613                     WaitHandle[] handles = new WaitHandle[1];
614                     handles[0] = _exitHandle;
615
616                     // wait for any
617                     int i = WaitHandle.WaitAny(handles, _CheckFrequency);
618                     if (WaitHandle.WaitTimeout == i)
619                     {
620                         ReportDiagnosticMessage(String.Format("{0} minutes are over", _CheckFrequency.TotalMinutes));
621                         continue;
622                     }
623
624                     // check the exit hadnle
625                     if (i == 0)
626                     {
627                         ReportDiagnosticMessage("Got exit signal");
628                         break;
629                     }
630
631                     // check an other check needed
632                     if (i == 1)
633                     {
634                         ReportDiagnosticMessage("Got force update check signal");
635                         checkTSP = false;
636                         continue;
637                     }
638                 }
639             } while (goIntoLoop);
640
641             // reset the islooping handle
642             _loopingHandle.Reset();
643         }
644
645         /// <summary>
646         /// This method will be notified
647         /// </summary>
648         /// <param name="sender"></param>
649         /// <param name="e"></param>
650         private void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
651         {
652             switch (e.ProgressPercentage)
653             {
654                 case 1:
655                     {
656                         // get the current item
657                         NetSparkleAppCastItem currentItem = e.UserState as NetSparkleAppCastItem;
658
659                         // show the update ui
660                         if (EnableSilentMode == true)
661                             InitDownloadAndInstallProcess(currentItem);
662                         else
663                             ShowUpdateNeededUI(currentItem);
664
665                         break;
666                     }
667                 case 0:
668                     {
669                         ReportDiagnosticMessage(e.UserState.ToString());
670                         break;
671                     }
672             }
673         }
674
675         private void InitDownloadAndInstallProcess(NetSparkleAppCastItem item)
676         {
677             NetSparkleDownloadProgress dlProgress = new NetSparkleDownloadProgress(this, item, _AppReferenceAssembly, ApplicationIcon, ApplicationWindowIcon, EnableSilentMode);
678             dlProgress.ShowDialog();
679         }
680
681         private void ShowDiagnosticWindowIfNeeded()
682         {
683             if (_DiagnosticWindow.InvokeRequired)
684             {
685                 _DiagnosticWindow.Invoke(new Action(ShowDiagnosticWindowIfNeeded));
686             }
687             else
688             {
689                 // check the diagnotic value
690                 NetSparkleConfiguration config = new NetSparkleConfiguration(_AppReferenceAssembly);
691                 if (config.ShowDiagnosticWindow || ShowDiagnosticWindow)
692                 {
693                     Point newLocation = new Point();
694
695                     newLocation.X = Screen.PrimaryScreen.Bounds.Width - _DiagnosticWindow.Width;
696                     newLocation.Y = 0;
697
698                     _DiagnosticWindow.Location = newLocation;
699                     _DiagnosticWindow.Show();
700                 }
701             }
702         }
703
704         private bool RemoteCertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
705         {
706             if (TrustEverySSLConnection)
707             {
708                 // verify if we talk about our app cast dll 
709                 HttpWebRequest req = sender as HttpWebRequest;
710                 if (req == null)
711                     return (certificate is X509Certificate2) ? ((X509Certificate2)certificate).Verify() : false;
712
713                 // if so just return our trust 
714                 if (req.RequestUri.Equals(new Uri(_AppCastUrl)))
715                     return true;
716                 else
717                     return (certificate is X509Certificate2) ? ((X509Certificate2)certificate).Verify() : false;
718             }
719             else
720             {
721                 // check our cert                 
722                 return (certificate is X509Certificate2) ? ((X509Certificate2)certificate).Verify() : false;
723             }
724         }
725     }
726 }