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