bovender framework
C# framework that implements MVVM and more
Updater.cs
1 /* Updater.cs
2  * part of Daniel's XL Toolbox NG
3  *
4  * Copyright 2014-2018 Daniel Kraus
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 using System;
19 using System.IO;
20 using System.Net;
21 using System.Text.RegularExpressions;
22 using Bovender.Mvvm;
23 using Bovender.Mvvm.Messaging;
24 using Bovender.Text;
25 using System.Threading;
26 
27 namespace Bovender.Versioning
28 {
41  public class Updater : Mvvm.Models.ProcessModel, IDisposable
42  {
43  #region Public properties
44 
45  public UpdaterStatus Status { get; protected set; }
46 
47  public Exception Exception { get; protected set; }
48 
49  public IReleaseInfo ReleaseInfo { get; set; }
50 
51  public string DestinationFolder { get; set; }
52 
53  public SemanticVersion CurrentVersion { get; set; }
54 
55  public int PercentDownloaded { get; protected set; }
56 
57  public long DownloadBytesTotal { get; protected set; }
58 
59  public long DownloadBytesReceived { get; protected set; }
60 
61  #endregion
62 
63  #region Public methods
64 
65  public bool Install()
66  {
67  bool result = false;
68  if (Status == UpdaterStatus.Downloaded)
69  {
70  if (Verify())
71  {
72  if (CheckAuthorization())
73  {
74  string command = GetInstallerCommand();
75  string arguments = GetInstallerParameters();
76  try
77  {
78  Logger.Info("Install: {0} {1}", command, arguments);
79  var process = System.Diagnostics.Process.Start(command, arguments);
80  Logger.Info("Install: Process: {0}", process);
81  Status = UpdaterStatus.InstallationStarted;
82  result = true;
83  }
84  catch (Exception e)
85  {
86  Logger.Fatal("Install: Could not start process");
87  Logger.Fatal(e);
88  Status = UpdaterStatus.InstallationFailed;
89  Exception = e;
90  }
91  }
92  else
93  {
94  Logger.Warn("Install: Not authorized to install!");
95  Status = UpdaterStatus.NotAuthorizedToInstall;
96  }
97  }
98  else
99  {
100  Logger.Warn("Install: Verification failed");
101  Status = UpdaterStatus.VerificationFailed;
102  }
103  }
104  else
105  {
106  Logger.Warn("Install: No update available!");
107  }
108  return result;
109  }
110 
111  #endregion
112 
113  #region Constructors
114 
115  public Updater() : base() { }
116 
117  public Updater(IReleaseInfo releaseInfo)
118  : this()
119  {
120  ReleaseInfo = releaseInfo;
121  }
122 
123  #endregion
124 
125  #region Disposal
126 
127  ~Updater()
128  {
129  Dispose(false);
130  }
131 
132  public void Dispose()
133  {
134  Dispose(true);
135  GC.SuppressFinalize(this);
136  }
137 
138  protected void Dispose(bool disposing)
139  {
140  if (!_disposed)
141  {
142  _disposed = true;
143  if (disposing)
144  {
145  // May clean up managed resources
146  if (_webClient != null)
147  {
148  _webClient.Dispose();
149  }
150  }
151  }
152  }
153 
154  #endregion
155 
156  #region Implementation of ProcessModel
157 
158  public override bool Execute()
159  {
160  if (ReleaseInfo == null)
161  {
162  throw new InvalidOperationException("Cannot download release because ReleaseInfo is null");
163  }
164  if (ReleaseInfo.Status != ReleaseInfoStatus.InfoAvailable)
165  {
166  throw new InvalidOperationException("Cannot download release because no release info is available");
167  }
168  bool result = false;
169  _destinationFileName = GetDestinationFileName();
170 
171  if (!(File.Exists(_destinationFileName) && Verify()))
172  {
173  Logger.Info("Execute: Starting download");
174  // http://stackoverflow.com/a/25834736/270712
175  _webClient = new WebClient();
176  _webClient.DownloadFileCompleted += WebClient_DownloadFileCompleted;
177  _webClient.DownloadProgressChanged += WebClient_DownloadProgressChanged;
178  var lockObject = new Object();
179  lock (lockObject)
180  {
181  _webClient.DownloadFileAsync(ReleaseInfo.DownloadUri, _destinationFileName, lockObject);
182  Monitor.Wait(lockObject);
183  }
184  result = true;
185  Logger.Info("Execute: exiting");
186  }
187  else
188  {
189  Logger.Info("Execute: Skipping download because destination file exists and is verified");
190  Status = UpdaterStatus.Downloaded;
191  }
192  return result;
193  }
194 
195  #endregion
196 
197  #region Protected helper methods
198 
210  protected virtual string GetDestinationFileName()
211  {
212  string downloadUri = ReleaseInfo.DownloadUri.ToString();
213  Logger.Info("GetDestinationFileName: Examining {0}", downloadUri);
214  string fn;
215  Regex r = new Regex(@"(?<fn>[^/]+?\.exe)");
216  Match m = r.Match(downloadUri);
217  if (m.Success)
218  {
219  fn = m.Groups["fn"].Value;
220  }
221  else
222  {
223  Logger.Warn("GetDestinationFileName: Did not find file name pattern in download URI!");
224  fn = String.Format("release-{0}.exe", ReleaseInfo.ReleaseVersion.ToString());
225  };
226  Logger.Warn("GetDestinationFileName: {0}", fn);
227  if (String.IsNullOrEmpty(DestinationFolder))
228  {
229  Logger.Warn("GetDestinationFileName: No destination folder!");
230  return fn;
231  }
232  else
233  {
234  return Path.Combine(DestinationFolder, fn);
235  }
236  }
237 
242  protected virtual string GetInstallerCommand()
243  {
244  return _destinationFileName;
245  }
246 
250  protected virtual string GetInstallerParameters()
251  {
252  // silencing parameters for InnoSetup installers
253  return "/UPDATE /SP- /SILENT /SUPPRESSMSGBOXES";
254  }
255 
261  protected virtual bool CheckAuthorization()
262  {
263  string installFolder = AppDomain.CurrentDomain.BaseDirectory;
264  /* Todo: compute permissions, rather than try and catch */
265  try
266  {
267  Logger.Info("CheckAuthorization: Attempting write to assembly's folder");
268  Logger.Info("CheckAuthorization: {0}", installFolder);
269  string fn = Path.Combine(installFolder, "bovender-framework-check-auth.txt");
270  using (FileStream f = new FileStream(fn, FileMode.Create, FileAccess.Write))
271  {
272  f.WriteByte(0xff);
273  };
274  File.Delete(fn);
275  Logger.Warn("CheckAuthorization: Successfully created and deleted test file");
276  return true;
277  }
278  catch (Exception e)
279  {
280  Logger.Warn("CheckAuthorization: Evidently not authorized");
281  Logger.Warn(e);
282  return false;
283  }
284  }
285 
291  protected virtual bool Verify()
292  {
293  string actual;
294  bool verified = false;
295  Logger.Info("Verify: Expected: {0}", ReleaseInfo.ExpectedHash);
296  switch (ReleaseInfo.ExpectedHash.Length)
297  {
298  case 40:
299  actual = FileHelpers.Sha1Hash(_destinationFileName); break;
300  case 64:
301  actual = FileHelpers.Sha256Hash(_destinationFileName); break;
302  default:
303  return false;
304  }
305  Logger.Info("Verify: Actual: {0}", actual);
306  verified = actual == ReleaseInfo.ExpectedHash;
307  if (verified)
308  {
309  Logger.Info("Verify: OK");
310  }
311  else
312  {
313  Logger.Warn("Verify: Checksum mismacth!");
314  }
315  return verified;
316  }
317 
318  #endregion
319 
320  #region Overrides
321 
322  protected override void OnCancelling()
323  {
324  base.OnCancelling();
325  if (_webClient != null && _webClient.IsBusy)
326  {
327  _webClient.CancelAsync();
328  }
329  }
330 
331  #endregion
332 
333  #region Private event handlers
334 
335  private void WebClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs args)
336  {
337  PercentDownloaded = args.ProgressPercentage;
338  DownloadBytesReceived = args.BytesReceived;
339  DownloadBytesTotal = args.TotalBytesToReceive;
340  }
341 
342  private void WebClient_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs args)
343  {
344  lock (args.UserState)
345  {
346  Monitor.Pulse(args.UserState);
347  }
348  if (args.Cancelled)
349  {
350  Logger.Info("WebClient_DownloadFileCompleted: Cancelled");
351  try
352  {
353  System.IO.File.Delete(_destinationFileName);
354  Logger.Info("WebClient_DownloadFileCompleted: Deleted partially downloaded file");
355  }
356  catch (Exception e)
357  {
358  Logger.Warn("WebClient_DownloadFileCompleted: Could not remove partially downloaded file");
359  Logger.Warn(e);
360  }
361  Status = UpdaterStatus.DownloadCancelled;
362  }
363  else
364  {
365  if (args.Error == null)
366  {
367  Logger.Info("WebClient_DownloadFileCompleted: Downloaded");
368  Status = UpdaterStatus.Downloaded;
369  }
370  else
371  {
372  Logger.Warn("WebClient_DownloadFileCompleted: Error!");
373  Logger.Warn(args.Error);
374  Status = UpdaterStatus.DownloadFailed;
375  Exception = args.Error;
376  }
377  }
378  }
379 
380  #endregion
381 
382  #region Private fields
383 
384  private bool _disposed;
385  private WebClient _webClient;
386  private string _destinationFileName;
387 
388  #endregion
389 
390  #region Class logger
391 
392  private static NLog.Logger Logger { get { return _logger.Value; } }
393 
394  private static readonly Lazy<NLog.Logger> _logger = new Lazy<NLog.Logger>(() => NLog.LogManager.GetCurrentClassLogger());
395 
396  #endregion
397  }
398 }
override bool Execute()
This method may be called by a ProcessViewModelBase-derived class in a worker task that wraps this me...
Definition: Updater.cs:158
virtual bool CheckAuthorization()
Determines whether the current user is authorized to write to the folder where the addin files are st...
Definition: Updater.cs:261
static string Sha1Hash(string file)
Computes the Sha1 hash of a given file.
Definition: FileHelpers.cs:74
Interface for classes that fetch current release information.
Definition: IReleaseInfo.cs:30
virtual string GetInstallerCommand()
Returns the command to execute in the shell to install the update.
Definition: Updater.cs:242
virtual string GetDestinationFileName()
Builds the destination file name from the download URI and the destination folder (which is stored in...
Definition: Updater.cs:210
virtual bool Verify()
Compares the actual and expected checksum hashes and sets the IsVerifiedDownload property accordingly...
Definition: Updater.cs:291
virtual string GetInstallerParameters()
Returns commandline parameters for the update installer.
Definition: Updater.cs:250
static string Sha256Hash(string file)
Computes the Sha256 hash of a given file.
Definition: FileHelpers.cs:36
Class that handles semantic versioning.
Fetches version information from the internet and raises an UpdateAvailable event if a new version is...
Definition: Updater.cs:41
Fetches and digests release information.
Definition: ReleaseInfo.cs:32
override string ToString()
Returns the full version string.