PageRenderTime 45ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/StarryEyes/Models/Subsystems/AutoUpdateService.cs

https://github.com/a1lic/StarryEyes
C# | 309 lines | 283 code | 15 blank | 11 comment | 25 complexity | 4e37baf25660e4ff4cb6c27957f1a671 MD5 | raw file
  1. using System;
  2. using System.Diagnostics;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net.Http;
  7. using System.Reactive.Linq;
  8. using System.Security.Cryptography;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using System.Xml.Linq;
  12. using StarryEyes.Albireo.Helpers;
  13. using StarryEyes.Globalization.Models;
  14. using StarryEyes.Models.Backstages.NotificationEvents;
  15. using StarryEyes.Settings;
  16. using TaskDialogInterop;
  17. using Application = System.Windows.Application;
  18. using TaskDialog = StarryEyes.Nightmare.Windows.TaskDialog;
  19. using TaskDialogCommonButtons = StarryEyes.Nightmare.Windows.TaskDialogCommonButtons;
  20. using TaskDialogOptions = StarryEyes.Nightmare.Windows.TaskDialogOptions;
  21. using VistaTaskDialogIcon = StarryEyes.Nightmare.Windows.VistaTaskDialogIcon;
  22. namespace StarryEyes.Models.Subsystems
  23. {
  24. public static class AutoUpdateService
  25. {
  26. private static bool _isUpdateNotified = false;
  27. private static string _patcherUri;
  28. private static string _patcherSignUri;
  29. public static event Action UpdateStateChanged;
  30. private static string ExecutablePath
  31. {
  32. get { return Path.Combine(App.LocalUpdateStorePath, App.UpdaterFileName); }
  33. }
  34. private static string PostUpdateFilePath
  35. {
  36. get { return Path.Combine(App.LocalUpdateStorePath, App.PostUpdateFileName); }
  37. }
  38. public static bool IsUpdateBinaryExisted()
  39. {
  40. return File.Exists(ExecutablePath);
  41. }
  42. public static bool IsPostUpdateFileExisted()
  43. {
  44. return File.Exists(PostUpdateFilePath);
  45. }
  46. private static async Task<bool> CheckUpdate(Version version)
  47. {
  48. if (IsUpdateBinaryExisted()) return true;
  49. try
  50. {
  51. string verXml;
  52. using (var http = new HttpClient())
  53. {
  54. verXml = await http.GetStringAsync(App.RemoteVersionXml);
  55. }
  56. var xdoc = XDocument.Load(new StringReader(verXml));
  57. if (xdoc.Root == null)
  58. {
  59. throw new Exception("could not read definition xml.");
  60. }
  61. _patcherUri = xdoc.Root.Attribute("patcher").Value;
  62. _patcherSignUri = xdoc.Root.Attribute("sign").Value;
  63. var releases = xdoc.Root.Descendants("release");
  64. var latest = releases.Select(r => Version.Parse(r.Attribute("version").Value))
  65. .Where(v => Setting.AcceptUnstableVersion.Value || v.Revision <= 0)
  66. .OrderByDescending(v => v)
  67. .FirstOrDefault();
  68. if (version != null && latest > version)
  69. {
  70. return true;
  71. }
  72. }
  73. catch (Exception ex)
  74. {
  75. BackstageModel.RegisterEvent(new OperationFailedEvent(
  76. SubsystemResources.FailedCheckingUpdate, ex));
  77. }
  78. return false;
  79. }
  80. internal static async Task<bool> CheckPrepareUpdate(Version version)
  81. {
  82. if (!await CheckUpdate(version))
  83. {
  84. return false;
  85. }
  86. if (String.IsNullOrEmpty(_patcherUri) || String.IsNullOrEmpty(_patcherSignUri))
  87. {
  88. return false;
  89. }
  90. try
  91. {
  92. if (IsUpdateBinaryExisted() || Directory.Exists(App.LocalUpdateStorePath))
  93. {
  94. // files are already downloaded.
  95. return true;
  96. }
  97. try
  98. {
  99. Directory.CreateDirectory(App.LocalUpdateStorePath);
  100. using (var http = new HttpClient())
  101. {
  102. var patcher = await http.GetByteArrayAsync(_patcherUri);
  103. var patcherSign = await http.GetByteArrayAsync(_patcherSignUri);
  104. var pubkey = File.ReadAllText(App.PublicKeyFile);
  105. if (!VerifySignature(patcher, patcherSign, pubkey))
  106. {
  107. throw new Exception("Updater signature is invalid.");
  108. }
  109. File.WriteAllBytes(ExecutablePath, patcher);
  110. }
  111. UpdateStateChanged.SafeInvoke();
  112. return true;
  113. }
  114. catch
  115. {
  116. try
  117. {
  118. Directory.Delete(App.LocalUpdateStorePath, true);
  119. }
  120. catch
  121. {
  122. }
  123. throw;
  124. }
  125. }
  126. catch (Exception ex)
  127. {
  128. BackstageModel.RegisterEvent(new OperationFailedEvent(
  129. SubsystemResources.FailedPrepareUpdate, ex));
  130. return false;
  131. }
  132. }
  133. internal static void StartUpdate(Version version)
  134. {
  135. try
  136. {
  137. var ver = "0.0.0";
  138. if (version != null)
  139. {
  140. ver = version.ToString(3);
  141. if (Setting.IsLoaded && Setting.AcceptUnstableVersion.Value)
  142. {
  143. ver = App.Version.ToString(4);
  144. }
  145. }
  146. var pubkey = Path.Combine(App.ExeFileDir, App.PublicKeyFile);
  147. var dest = App.ExeFileDir;
  148. var pid = Process.GetCurrentProcess().Id;
  149. var args = new[]
  150. {
  151. ver,
  152. pubkey,
  153. dest,
  154. pid.ToString(CultureInfo.InvariantCulture)
  155. }.Select(s => "\"" + s + "\"").JoinString(" ");
  156. var startInfo = new ProcessStartInfo(ExecutablePath)
  157. {
  158. Arguments = args,
  159. UseShellExecute = true,
  160. WorkingDirectory = App.LocalUpdateStorePath,
  161. };
  162. MainWindowModel.SuppressCloseConfirmation = true;
  163. Process.Start(startInfo);
  164. Application.Current.Shutdown();
  165. }
  166. catch (Exception ex)
  167. {
  168. TaskDialog.Show(new TaskDialogOptions
  169. {
  170. Title = SubsystemResources.AutoUpdateFailedTitle,
  171. MainIcon = VistaTaskDialogIcon.Error,
  172. MainInstruction = SubsystemResources.AutoUpdateFailedInst,
  173. Content = SubsystemResources.AutoUpdateFailedContent,
  174. ExpandedInfo = ex.ToString(),
  175. CommonButtons = TaskDialogCommonButtons.Close
  176. });
  177. // cleanup update binaries
  178. try
  179. {
  180. if (Directory.Exists(App.LocalUpdateStorePath))
  181. {
  182. Directory.Delete(App.LocalUpdateStorePath, true);
  183. }
  184. }
  185. catch
  186. {
  187. }
  188. }
  189. }
  190. internal static void PostUpdate()
  191. {
  192. var retryCount = 0;
  193. try
  194. {
  195. var directory = new DirectoryInfo(App.LocalUpdateStorePath);
  196. while (directory.Exists)
  197. {
  198. try
  199. {
  200. // remove "read-only" attribute
  201. directory.GetFiles("*", SearchOption.AllDirectories)
  202. .Where(file => file.Attributes.HasFlag(FileAttributes.ReadOnly))
  203. .ForEach(f => f.Attributes ^= FileAttributes.ReadOnly);
  204. // delete directory
  205. directory.Delete(true);
  206. break;
  207. }
  208. catch (Exception)
  209. {
  210. if (retryCount > 10)
  211. {
  212. // exit loop
  213. throw;
  214. }
  215. Thread.Sleep(1000);
  216. // refresh directory state
  217. directory.Refresh();
  218. retryCount++;
  219. }
  220. }
  221. }
  222. catch (Exception)
  223. {
  224. TaskDialog.Show(new TaskDialogOptions
  225. {
  226. Title = SubsystemResources.UpdateCompleteErrorTitle,
  227. MainIcon = VistaTaskDialogIcon.Error,
  228. MainInstruction = SubsystemResources.UpdateCompleteErrorInst,
  229. Content = SubsystemResources.UpdateCompleteErrorContent,
  230. ExpandedInfo = SubsystemResources.UpdateCompleteErrorExInfo,
  231. CommonButtons = TaskDialogCommonButtons.Close
  232. });
  233. }
  234. }
  235. private static bool VerifySignature(byte[] bytes, byte[] signature, String publicKey)
  236. {
  237. using (var sha = new SHA256Managed())
  238. using (var rsa = new RSACryptoServiceProvider())
  239. {
  240. // Compute hash
  241. var hash = sha.ComputeHash(bytes);
  242. // RSA Initialize
  243. rsa.FromXmlString(publicKey);
  244. // format
  245. var deformatter = new RSAPKCS1SignatureDeformatter(rsa);
  246. deformatter.SetHashAlgorithm("SHA256");
  247. return deformatter.VerifySignature(hash, signature);
  248. }
  249. }
  250. internal static void StartSchedule(bool initial = true)
  251. {
  252. var rand = new Random(Environment.TickCount);
  253. // first startup -> 36 seconds later
  254. var next = initial ? 0.01 : 3 + 6 * rand.NextDouble();
  255. Observable
  256. .Timer(TimeSpan.FromHours(next))
  257. .Subscribe(async _ =>
  258. {
  259. if (await CheckPrepareUpdate(App.Version))
  260. {
  261. if (_isUpdateNotified) return;
  262. _isUpdateNotified = true;
  263. var option = new TaskDialogOptions
  264. {
  265. Title = SubsystemResources.UpdateAvailableTitle,
  266. MainIcon = VistaTaskDialogIcon.Information,
  267. MainInstruction = SubsystemResources.UpdateAvailableInst,
  268. Content = SubsystemResources.UpdateAvailableContent,
  269. CustomButtons = new[]
  270. {
  271. SubsystemResources.UpdateAvailableButtonImmediate,
  272. SubsystemResources.UpdateAvailableButtonLater
  273. },
  274. Callback = (dialog, args, data) =>
  275. {
  276. if (args.Notification == VistaTaskDialogNotification.ButtonClicked &&
  277. args.ButtonIndex == 0)
  278. {
  279. // update immediately
  280. StartUpdate(App.Version);
  281. }
  282. return false;
  283. }
  284. };
  285. MainWindowModel.ShowTaskDialog(option);
  286. }
  287. else
  288. {
  289. StartSchedule(false);
  290. }
  291. });
  292. }
  293. }
  294. }