// Copyright (C) 2014, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Linq; using System.Collections.Generic; namespace Duplicati.Library.AutoUpdater { public enum AutoUpdateStrategy { CheckBefore, CheckDuring, CheckAfter, InstallBefore, InstallDuring, InstallAfter, Never } public static class UpdaterManager { private static readonly System.Security.Cryptography.RSACryptoServiceProvider SIGN_KEY = AutoUpdateSettings.SignKey; private static readonly string[] MANIFEST_URLS = AutoUpdateSettings.URLs; private static readonly string APPNAME = AutoUpdateSettings.AppName; private static readonly string INSTALLDIR; private static readonly string INSTALLED_BASE_DIR = string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(string.Format(INSTALLDIR_ENVNAME_TEMPLATE, APPNAME))) ? System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) : System.Environment.GetEnvironmentVariable(string.Format(INSTALLDIR_ENVNAME_TEMPLATE, APPNAME)); private static readonly bool DISABLE_UPDATE_DOMAIN = !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(string.Format(SKIPUPDATE_ENVNAME_TEMPLATE, APPNAME))); public static bool RequiresRespawn { get; set; } private static KeyValuePair? m_hasUpdateInstalled; private static readonly UpdateInfo SelfVersion; public static event Action OnError; private const string DATETIME_FORMAT = "yyyymmddhhMMss"; private const string INSTALLDIR_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_INSTALL_ROOT"; private const string SKIPUPDATE_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SKIP_UPDATE"; private const string RUN_UPDATED_FOLDER_PATH = "AUTOUPDATER_LOAD_UPDATE"; private const string SLEEP_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SLEEP"; private const string UPDATE_STRATEGY_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_POLICY"; private const string UPDATE_MANIFEST_FILENAME = "autoupdate.manifest"; private const string README_FILE = "README.txt"; private const string INSTALL_FILE = "installation.txt"; private const string CURRENT_FILE = "current"; /// /// Gets the original directory that this application was installed into /// /// The original directory that this application was installed into public static string InstalledBaseDir { get { return INSTALLED_BASE_DIR; } } static UpdaterManager() { string installdir = null; var attempts = new string[] { System.IO.Path.Combine(InstalledBaseDir, "updates"), System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), APPNAME, "updates"), System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), APPNAME, "updates"), System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), APPNAME, "updates"), }; foreach(var p in attempts) if (TestDirectoryIsWriteable(p)) { installdir = p; break; } INSTALLDIR = installdir; if (INSTALLDIR != null) { if (!System.IO.File.Exists(System.IO.Path.Combine(INSTALLDIR, README_FILE))) System.IO.File.WriteAllText(System.IO.Path.Combine(INSTALLDIR, README_FILE), AutoUpdateSettings.UpdateFolderReadme); if (!System.IO.File.Exists(System.IO.Path.Combine(INSTALLDIR, INSTALL_FILE))) System.IO.File.WriteAllText(System.IO.Path.Combine(INSTALLDIR, INSTALL_FILE), AutoUpdateSettings.UpdateInstallFileText); } UpdateInfo selfVersion = null; try { selfVersion = ReadInstalledManifest(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location)); } catch { } if (selfVersion == null) selfVersion = new UpdateInfo() { Displayname = "Current", Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), ReleaseTime = new DateTime(0), ReleaseType = #if DEBUG "Debug" #else "Nightly" #endif }; SelfVersion = selfVersion; } private static Version TryParseVersion(string str) { Version v; if (Version.TryParse(str, out v)) return v; else return new Version(0, 0); } public static bool HasUpdateInstalled { get { if (!m_hasUpdateInstalled.HasValue) { var selfversion = TryParseVersion(SelfVersion.Version); m_hasUpdateInstalled = (from n in FindInstalledVersions() let nversion = TryParseVersion(n.Value.Version) let newerVersion = selfversion < nversion where newerVersion && VerifyUnpackedFolder(n.Key, n.Value) orderby nversion descending select n) .FirstOrDefault(); } return m_hasUpdateInstalled.Value.Value != null; } } private static bool TestDirectoryIsWriteable(string path) { var p2 = System.IO.Path.Combine(path, "test-" + DateTime.UtcNow.ToString(DATETIME_FORMAT)); var probe = System.IO.Directory.Exists(path) ? p2 : path; if (!System.IO.Directory.Exists(probe)) { try { System.IO.Directory.CreateDirectory(probe); if (probe != path) System.IO.Directory.Delete(probe); return true; } catch { } } return false; } private static string InstallID { get { try { return System.IO.File.ReadAllText(System.IO.Path.Combine(INSTALLDIR, INSTALL_FILE)).Replace('\r', '\n').Split(new char[] { '\n' }).FirstOrDefault().Trim() ?? ""; } catch { } return ""; } } public static UpdateInfo CheckForUpdate() { foreach(var url in MANIFEST_URLS) { try { using(var tmpfile = new Library.Utility.TempFile()) { System.Net.WebClient wc = new System.Net.WebClient(); wc.Headers.Add(System.Net.HttpRequestHeader.UserAgent, string.Format("{0} v{1}", APPNAME, SelfVersion.Version)); wc.Headers.Add("X-Install-ID", InstallID); wc.DownloadFile(url, tmpfile); using(var fs = System.IO.File.OpenRead(tmpfile)) using(var ss = new SignatureReadingStream(fs, SIGN_KEY)) using(var tr = new System.IO.StreamReader(ss)) using(var jr = new Newtonsoft.Json.JsonTextReader(tr)) { var update = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); if (TryParseVersion(update.Version) <= TryParseVersion(SelfVersion.Version)) return null; if (string.Equals(SelfVersion.ReleaseType, "Debug", StringComparison.InvariantCultureIgnoreCase) && !string.Equals(update.ReleaseType, SelfVersion.ReleaseType, StringComparison.CurrentCultureIgnoreCase)) return null; return update; } } } catch (Exception ex) { if (OnError != null) OnError(ex); } } return null; } private static UpdateInfo ReadInstalledManifest(string folder) { var manifest = System.IO.Path.Combine(folder, UPDATE_MANIFEST_FILENAME); if (System.IO.File.Exists(manifest)) { try { using(var fs = System.IO.File.OpenRead(manifest)) using(var ss = new SignatureReadingStream(fs, SIGN_KEY)) using(var tr = new System.IO.StreamReader(ss)) using(var jr = new Newtonsoft.Json.JsonTextReader(tr)) return new Newtonsoft.Json.JsonSerializer().Deserialize(jr); } catch (Exception ex) { if (OnError != null) OnError(ex); } } return null; } public static IEnumerable> FindInstalledVersions() { var res = new List>(); if (INSTALLDIR != null) foreach(var folder in System.IO.Directory.GetDirectories(INSTALLDIR)) { var r = ReadInstalledManifest(folder); if (r != null) res.Add(new KeyValuePair(folder, r)); } return res; } public static bool DownloadAndUnpackUpdate(UpdateInfo version, Action progress = null) { if (INSTALLDIR == null) return false; var updates = version.RemoteURLS.ToList(); // If alternate update URLs are specified, // we look for packages there as well if (AutoUpdateSettings.UsesAlternateURLs) { var packagepath = new Library.Utility.Uri(updates[0]).Path; var packagename = packagepath.Split('/').Last(); foreach(var alt_url in AutoUpdateSettings.URLs.Reverse()) { var alt_uri = new Library.Utility.Uri(alt_url); var path_components = alt_uri.Path.Split('/'); var path = string.Join("/", path_components.Take(path_components.Count() - 1).Union(new string[] { packagename})); var new_path = alt_uri.SetPath(path); updates.Insert(0, new_path.ToString()); } } using(var tempfile = new Library.Utility.TempFile()) { foreach(var url in updates) { try { Action cb = null; if (progress != null) cb = (s) => { progress(Math.Min(1.0, Math.Max(0.0, (double)s / version.CompressedSize))); }; var wreq = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url); wreq.UserAgent = string.Format("{0} v{1}", APPNAME, SelfVersion.Version); wreq.Headers.Add("X-Install-ID", InstallID); using(var resp = wreq.GetResponse()) using(var rss = resp.GetResponseStream()) using(var pgs = new Duplicati.Library.Utility.ProgressReportingStream(rss, version.CompressedSize, cb)) using(var fs = System.IO.File.Open(tempfile, System.IO.FileMode.Create)) Duplicati.Library.Utility.Utility.CopyStream(pgs, fs); var sha256 = System.Security.Cryptography.SHA256.Create(); var md5 = System.Security.Cryptography.MD5.Create(); using(var s = System.IO.File.OpenRead(tempfile)) { if (s.Length != version.CompressedSize) throw new Exception(string.Format("Invalid file size {0}, expected {1} for {2}", s.Length, version.CompressedSize, url)); var sha256hash = Convert.ToBase64String(sha256.ComputeHash(s)); if (sha256hash != version.SHA256) throw new Exception(string.Format("Damaged or corrupted file, sha256 mismatch for {0}", url)); } using(var s = System.IO.File.OpenRead(tempfile)) { var md5hash = Convert.ToBase64String(md5.ComputeHash(s)); if (md5hash != version.MD5) throw new Exception(string.Format("Damaged or corrupted file, md5 mismatch for {0}", url)); } using(var tempfolder = new Duplicati.Library.Utility.TempFolder()) using(var zip = new Duplicati.Library.Compression.FileArchiveZip(tempfile, new Dictionary())) { foreach(var file in zip.ListFilesWithSize("")) { if (System.IO.Path.IsPathRooted(file.Key) || file.Key.Trim().StartsWith("..", StringComparison.InvariantCultureIgnoreCase)) throw new Exception(string.Format("Out-of-place file path detected: {0}", file.Key)); var targetpath = System.IO.Path.Combine(tempfolder, file.Key); var targetfolder = System.IO.Path.GetDirectoryName(targetpath); if (!System.IO.Directory.Exists(targetfolder)) System.IO.Directory.CreateDirectory(targetfolder); using(var zs = zip.OpenRead(file.Key)) using(var fs = System.IO.File.Create(targetpath)) zs.CopyTo(fs); } if (VerifyUnpackedFolder(tempfolder, version)) { var versionstring = TryParseVersion(version.Version).ToString(); var targetfolder = System.IO.Path.Combine(INSTALLDIR, versionstring); if (System.IO.Directory.Exists(targetfolder)) System.IO.Directory.Delete(targetfolder, true); System.IO.Directory.CreateDirectory(targetfolder); var tempfolderpath = Duplicati.Library.Utility.Utility.AppendDirSeparator(tempfolder); var tempfolderlength = tempfolderpath.Length; // Would be nice, but does not work :( //System.IO.Directory.Move(tempfolder, targetfolder); foreach(var e in Duplicati.Library.Utility.Utility.EnumerateFileSystemEntries(tempfolder)) { var relpath = e.Substring(tempfolderlength); if (string.IsNullOrWhiteSpace(relpath)) continue; var fullpath = System.IO.Path.Combine(targetfolder, relpath); if (relpath.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString())) System.IO.Directory.CreateDirectory(fullpath); else System.IO.File.Copy(e, fullpath); } // Verification will kick in when we list the installed updates //VerifyUnpackedFolder(targetfolder, version); System.IO.File.WriteAllText(System.IO.Path.Combine(INSTALLDIR, CURRENT_FILE), versionstring); m_hasUpdateInstalled = null; var obsolete = (from n in FindInstalledVersions() where n.Value.Version != version.Version && n.Value.Version != SelfVersion.Version let x = TryParseVersion(n.Value.Version) orderby x descending select n).Skip(1).ToArray(); foreach(var f in obsolete) try { System.IO.Directory.Delete(f.Key, true); } catch { } return true; } else { throw new Exception(string.Format("Unable to verify unpacked folder for url: {0}", url)); } } } catch(Exception ex) { if (OnError != null) OnError(ex); } } } return false; } public static bool VerifyUnpackedFolder(string folder, UpdateInfo version = null) { try { UpdateInfo update; FileEntry manifest; var sha256 = System.Security.Cryptography.SHA256.Create(); var md5 = System.Security.Cryptography.MD5.Create(); using(var fs = System.IO.File.OpenRead(System.IO.Path.Combine(folder, UPDATE_MANIFEST_FILENAME))) { using(var ss = new SignatureReadingStream(fs, SIGN_KEY)) using(var tr = new System.IO.StreamReader(ss)) using(var jr = new Newtonsoft.Json.JsonTextReader(tr)) update = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); sha256.Initialize(); md5.Initialize(); fs.Position = 0; var h1 = Convert.ToBase64String(sha256.ComputeHash(fs)); fs.Position = 0; var h2 = Convert.ToBase64String(md5.ComputeHash(fs)); manifest = new FileEntry() { Path = UPDATE_MANIFEST_FILENAME, Ignore = false, LastWriteTime = update.ReleaseTime, SHA256 = h1, MD5 = h2 }; } if (version != null && (update.Displayname != version.Displayname || update.ReleaseTime != version.ReleaseTime)) throw new Exception("The found version was not the expected version"); var paths = update.Files.Where(x => !x.Ignore).ToDictionary(x => x.Path.Replace('/', System.IO.Path.DirectorySeparatorChar), Library.Utility.Utility.ClientFilenameStringComparer); paths.Add(manifest.Path, manifest); var ignores = (from x in update.Files where x.Ignore select Library.Utility.Utility.AppendDirSeparator(x.Path.Replace('/', System.IO.Path.DirectorySeparatorChar))).ToList(); folder = Library.Utility.Utility.AppendDirSeparator(folder); var baselen = folder.Length; foreach(var file in Library.Utility.Utility.EnumerateFileSystemEntries(folder)) { var relpath = file.Substring(baselen); if (string.IsNullOrWhiteSpace(relpath)) continue; FileEntry fe; if (!paths.TryGetValue(relpath, out fe)) { var ignore = false; foreach(var c in ignores) if (ignore = relpath.StartsWith(c)) break; if (ignore) continue; throw new Exception(string.Format("Found unexpected file: {0}", file)); } paths.Remove(relpath); if (fe.Path.EndsWith("/")) continue; sha256.Initialize(); md5.Initialize(); using(var fs = System.IO.File.OpenRead(file)) { if (Convert.ToBase64String(sha256.ComputeHash(fs)) != fe.SHA256) throw new Exception(string.Format("Invalid sha256 hash for file: {0}", file)); fs.Position = 0; if (Convert.ToBase64String(md5.ComputeHash(fs)) != fe.MD5) throw new Exception(string.Format("Invalid md5 hash for file: {0}", file)); } } var filteredpaths = (from p in paths where !string.IsNullOrWhiteSpace(p.Key) && !p.Key.EndsWith("/") select p.Key).ToList(); if (filteredpaths.Count == 1) throw new Exception(string.Format("Folder {0} is missing: {1}", folder, filteredpaths.First())); else if (filteredpaths.Count > 0) throw new Exception(string.Format("Folder {0} is missing {1} and {2} other file(s)", folder, filteredpaths.First(), filteredpaths.Count - 1)); return true; } catch (Exception ex) { if (OnError != null) OnError(ex); } return false; } public static bool SetRunUpdate() { if (HasUpdateInstalled) { AppDomain.CurrentDomain.SetData(RUN_UPDATED_FOLDER_PATH, m_hasUpdateInstalled.Value.Key); return true; } return false; } public static void CreateUpdatePackage(System.Security.Cryptography.RSACryptoServiceProvider key, string inputfolder, string outputfolder, string manifest = null) { // Read the existing manifest UpdateInfo remoteManifest; var manifestpath = manifest ?? System.IO.Path.Combine(inputfolder, UPDATE_MANIFEST_FILENAME); using(var s = System.IO.File.OpenRead(manifestpath)) using(var sr = new System.IO.StreamReader(s)) using(var jr = new Newtonsoft.Json.JsonTextReader(sr)) remoteManifest = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); if (remoteManifest.Files == null) remoteManifest.Files = new FileEntry[0]; if (remoteManifest.ReleaseTime.Ticks == 0) remoteManifest.ReleaseTime = DateTime.UtcNow; var ignoreFiles = (from n in remoteManifest.Files where n.Ignore select n).ToArray(); var ignoreMap = ignoreFiles.ToDictionary(k => k.Path, k => "", Duplicati.Library.Utility.Utility.ClientFilenameStringComparer); remoteManifest.MD5 = null; remoteManifest.SHA256 = null; remoteManifest.Files = null; remoteManifest.UncompressedSize = 0; var localManifest = remoteManifest.Clone(); localManifest.RemoteURLS = null; inputfolder = Duplicati.Library.Utility.Utility.AppendDirSeparator(inputfolder); var baselen = inputfolder.Length; var dirsep = System.IO.Path.DirectorySeparatorChar.ToString(); ignoreMap.Add(UPDATE_MANIFEST_FILENAME, ""); var md5 = System.Security.Cryptography.MD5.Create(); var sha256 = System.Security.Cryptography.SHA256.Create(); Func computeMD5 = (path) => { md5.Initialize(); using(var fs = System.IO.File.OpenRead(path)) return Convert.ToBase64String(md5.ComputeHash(fs)); }; Func computeSHA256 = (path) => { sha256.Initialize(); using(var fs = System.IO.File.OpenRead(path)) return Convert.ToBase64String(sha256.ComputeHash(fs)); }; // Build a zip using (var archive_temp = new Duplicati.Library.Utility.TempFile()) { using (var zipfile = new Duplicati.Library.Compression.FileArchiveZip(archive_temp, new Dictionary())) { Func addToArchive = (path, relpath) => { if (ignoreMap.ContainsKey(relpath)) return false; if (path.EndsWith(dirsep)) return true; using (var source = System.IO.File.OpenRead(path)) using (var target = zipfile.CreateFile(relpath, Duplicati.Library.Interface.CompressionHint.Compressible, System.IO.File.GetLastAccessTimeUtc(path))) { source.CopyTo(target); remoteManifest.UncompressedSize += source.Length; } return true; }; // Build the update manifest localManifest.Files = (from fse in Duplicati.Library.Utility.Utility.EnumerateFileSystemEntries(inputfolder) let relpath = fse.Substring(baselen) where addToArchive(fse, relpath) select new FileEntry() { Path = relpath, LastWriteTime = System.IO.File.GetLastAccessTimeUtc(fse), MD5 = fse.EndsWith(dirsep) ? null : computeMD5(fse), SHA256 = fse.EndsWith(dirsep) ? null : computeSHA256(fse) }) .Union(ignoreFiles).ToArray(); // Write a signed manifest with the files using (var ms = new System.IO.MemoryStream()) using (var sw = new System.IO.StreamWriter(ms)) { new Newtonsoft.Json.JsonSerializer().Serialize(sw, localManifest); sw.Flush(); using (var ms2 = new System.IO.MemoryStream()) { SignatureReadingStream.CreateSignedStream(ms, ms2, key); ms2.Position = 0; using (var sigfile = zipfile.CreateFile(UPDATE_MANIFEST_FILENAME, Duplicati.Library.Interface.CompressionHint.Compressible, DateTime.UtcNow)) ms2.CopyTo(sigfile); } } } remoteManifest.CompressedSize = new System.IO.FileInfo(archive_temp).Length; remoteManifest.MD5 = computeMD5(archive_temp); remoteManifest.SHA256 = computeSHA256(archive_temp); System.IO.File.Move(archive_temp, System.IO.Path.Combine(outputfolder, "package.zip")); } // Write a signed manifest for upload using(var tf = new Duplicati.Library.Utility.TempFile()) { using (var ms = new System.IO.MemoryStream()) using (var sw = new System.IO.StreamWriter(ms)) { new Newtonsoft.Json.JsonSerializer().Serialize(sw, remoteManifest); sw.Flush(); using (var fs = System.IO.File.Create(tf)) SignatureReadingStream.CreateSignedStream(ms, fs, key); } System.IO.File.Move(tf, System.IO.Path.Combine(outputfolder, UPDATE_MANIFEST_FILENAME)); } } private static void WrapWithUpdater(AutoUpdateStrategy defaultstrategy, Action wrappedFunction) { string optstr = Environment.GetEnvironmentVariable(string.Format(UPDATE_STRATEGY_ENVNAME_TEMPLATE, APPNAME)); AutoUpdateStrategy strategy; if (string.IsNullOrWhiteSpace(optstr) || !Enum.TryParse(optstr, out strategy)) strategy = defaultstrategy; System.Threading.Thread backgroundChecker = null; UpdateInfo updateDetected = null; bool updateInstalled = false; bool checkForUpdate; bool downloadUpdate; bool runAfter; bool runDuring; bool runBefore; switch (strategy) { case AutoUpdateStrategy.CheckBefore: case AutoUpdateStrategy.CheckDuring: case AutoUpdateStrategy.CheckAfter: checkForUpdate = true; downloadUpdate = false; break; case AutoUpdateStrategy.InstallBefore: case AutoUpdateStrategy.InstallDuring: case AutoUpdateStrategy.InstallAfter: checkForUpdate = true; downloadUpdate = true; break; default: checkForUpdate = false; downloadUpdate = false; break; } switch (strategy) { case AutoUpdateStrategy.CheckBefore: case AutoUpdateStrategy.InstallBefore: runBefore = true; runDuring = false; runAfter = false; break; case AutoUpdateStrategy.CheckAfter: case AutoUpdateStrategy.InstallAfter: runBefore = false; runDuring = false; runAfter = true; break; case AutoUpdateStrategy.CheckDuring: case AutoUpdateStrategy.InstallDuring: runBefore = false; runDuring = true; runAfter = false; break; default: runBefore = false; runDuring = false; runAfter = false; break; } if (checkForUpdate) { backgroundChecker = new System.Threading.Thread(() => { // Don't run "during" if the task is short if (runDuring) System.Threading.Thread.Sleep(TimeSpan.FromSeconds(10)); updateDetected = CheckForUpdate(); if (updateDetected != null && downloadUpdate) { if (!runDuring) Console.WriteLine("Update to {0} detected, installing...", updateDetected.Displayname); updateInstalled = DownloadAndUnpackUpdate(updateDetected); } }); backgroundChecker.IsBackground = true; backgroundChecker.Name = "BackgroundUpdateChecker"; if (!runAfter) backgroundChecker.Start(); if (runBefore) { Console.WriteLine("Checking for update ..."); backgroundChecker.Join(); if (downloadUpdate) { if (updateInstalled) Console.WriteLine("Install succeeded, running updated version"); else Console.WriteLine("Install or download failed, using current version"); } else if (updateDetected != null) { Console.WriteLine("Update \"{0}\" detected", updateDetected.Displayname); } backgroundChecker = null; } } wrappedFunction(); if (backgroundChecker != null && runAfter) { Console.WriteLine("Checking for update ..."); backgroundChecker.Start(); backgroundChecker.Join(); } if (backgroundChecker != null && updateDetected != null) { if (backgroundChecker.IsAlive) { Console.WriteLine("Waiting for update \"{0}\" to complete", updateDetected.Displayname); backgroundChecker.Join(); } if (downloadUpdate) { if (updateInstalled) Console.WriteLine("Install succeeded, running updated version on next launch"); else Console.WriteLine("Install or download failed, using current version on next launch"); } else { Console.WriteLine("Update \"{0}\" detected", updateDetected.Displayname); } } } private static int RunMethod(System.Reflection.MethodInfo method, string[] args) { try { var n = method.Invoke(null, new object[] { args }); if (method.ReturnType == typeof(int)) return (int)n; return 0; } catch (System.Reflection.TargetInvocationException tex) { try { Console.WriteLine("Crash! {0}{1}", Environment.NewLine, tex.ToString()); } catch { } try { var report_file = System.IO.Path.Combine( string.IsNullOrEmpty(INSTALLDIR) ? Library.Utility.TempFolder.SystemTempPath : INSTALLDIR, string.Format("{0}-crashlog.txt", AutoUpdateSettings.AppName) ); System.IO.File.WriteAllText(report_file, tex.ToString()); } catch { } if (tex.InnerException != null) throw tex.InnerException; else throw; } } public static int RunFromMostRecent(System.Reflection.MethodInfo method, string[] cmdargs, AutoUpdateStrategy defaultstrategy = AutoUpdateStrategy.InstallDuring) { // If the update is disabled, go straight in if (DISABLE_UPDATE_DOMAIN) return RunMethod(method, cmdargs); // If we are not the primary domain, just execute if (!AppDomain.CurrentDomain.IsDefaultAppDomain()) { int r = 0; WrapWithUpdater(defaultstrategy, () => { r = RunMethod(method, cmdargs); }); return r; } // If we are a re-launch, wait briefly for the other process to exit var sleepmarker = System.Environment.GetEnvironmentVariable(string.Format(SLEEP_ENVNAME_TEMPLATE, APPNAME)); if (!string.IsNullOrWhiteSpace(sleepmarker)) { System.Environment.SetEnvironmentVariable(string.Format(SLEEP_ENVNAME_TEMPLATE, APPNAME), null); System.Threading.Thread.Sleep(TimeSpan.FromSeconds(10)); } // Check if there are updates installed, otherwise use current KeyValuePair best = new KeyValuePair(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, SelfVersion); if (HasUpdateInstalled) best = m_hasUpdateInstalled.Value; if (INSTALLDIR != null && System.IO.File.Exists(System.IO.Path.Combine(INSTALLDIR, CURRENT_FILE))) { try { var current = System.IO.File.ReadAllText(System.IO.Path.Combine(INSTALLDIR, CURRENT_FILE)).Trim(); if (!string.IsNullOrWhiteSpace(current)) { var targetfolder = System.IO.Path.Combine(INSTALLDIR, current); var currentmanifest = ReadInstalledManifest(targetfolder); if (currentmanifest != null && VerifyUnpackedFolder(targetfolder, currentmanifest)) best = new KeyValuePair(targetfolder, currentmanifest); } } catch (Exception ex) { if (OnError != null) OnError(ex); } } Environment.SetEnvironmentVariable(string.Format(INSTALLDIR_ENVNAME_TEMPLATE, APPNAME), InstalledBaseDir); var folder = best.Key; // Basic idea with the loop is that the running AppDomain can use // RUN_UPDATED_ENVNAME_TEMPLATE to signal that a new version is ready // when the caller exits, the new update is executed // // This allows more or less seamless updates // int result = 0; while (!string.IsNullOrWhiteSpace(folder) && System.IO.Directory.Exists(folder)) { var prevfolder = folder; // Create the new domain var domain = AppDomain.CreateDomain( "UpdateDomain", null, folder, "", false ); result = domain.ExecuteAssemblyByName(method.DeclaringType.Assembly.GetName().Name, cmdargs); folder = (string)domain.GetData(RUN_UPDATED_FOLDER_PATH); try { AppDomain.Unload(domain); } catch (Exception ex) { Console.WriteLine("Appdomain unload error: {0}", ex); } if (!string.IsNullOrWhiteSpace(folder)) { if (!VerifyUnpackedFolder(folder)) folder = prevfolder; //Go back and run the previous version else if (RequiresRespawn) { // We have a valid update, and the current instance is terminated. // But due to external libraries, we need to re-spawn the original process try { var args = Environment.CommandLine; var app = Environment.GetCommandLineArgs().First(); args = args.Substring(app.Length); if (!System.IO.Path.IsPathRooted(app)) app = System.IO.Path.Combine(InstalledBaseDir, app); // Re-launch but give the OS a little time to fully unload all open handles, etc. var si = new System.Diagnostics.ProcessStartInfo(app, args); si.UseShellExecute = false; si.EnvironmentVariables.Add(string.Format(SLEEP_ENVNAME_TEMPLATE, APPNAME), "1"); System.Diagnostics.Process.Start(si); return 0; } catch (Exception ex) { if (OnError != null) OnError(ex); folder = prevfolder; } } } } return result; } } }