public async Task <UpdateInfo> CheckForUpdate( string localReleaseFile, string updateUrlOrPath, bool ignoreDeltaUpdates = false, Action <int> progress = null, IFileDownloader urlDownloader = null) { progress = progress ?? (_ => { }); var localReleases = Enumerable.Empty <ReleaseEntry>(); bool shouldInitialize = false; try { localReleases = Utility.LoadLocalReleases(localReleaseFile); } catch (Exception ex) { // Something has gone pear-shaped, let's start from scratch this.Log().WarnException("Failed to load local releases, starting from scratch", ex); shouldInitialize = true; } if (shouldInitialize) { await initializeClientAppDirectory(); } string releaseFile; var latestLocalRelease = localReleases.Count() > 0 ? localReleases.MaxBy(x => x.Version).First() : default(ReleaseEntry); // Fetch the remote RELEASES file, whether it's a local dir or an // HTTP URL if (Utility.IsHttpUrl(updateUrlOrPath)) { if (updateUrlOrPath.EndsWith("/")) { updateUrlOrPath = updateUrlOrPath.Substring(0, updateUrlOrPath.Length - 1); } this.Log().Info("Downloading RELEASES file from {0}", updateUrlOrPath); int retries = 3; retry: try { var url = String.Format("{0}/{1}", updateUrlOrPath, "RELEASES"); if (latestLocalRelease != null) { url = String.Format("{0}/RELEASES?id={1}&localVersion={2}&arch={3}", updateUrlOrPath, Uri.EscapeUriString(latestLocalRelease.PackageName), Uri.EscapeUriString(latestLocalRelease.Version.ToString()), Environment.Is64BitOperatingSystem ? "amd64" : "x86"); } var data = await urlDownloader.DownloadUrl(url); releaseFile = Encoding.UTF8.GetString(data); } catch (WebException ex) { this.Log().InfoException("Download resulted in WebException (returning blank release list)", ex); if (retries <= 0) { throw; } retries--; goto retry; } progress(33); } else { this.Log().Info("Reading RELEASES file from {0}", updateUrlOrPath); if (!Directory.Exists(updateUrlOrPath)) { var message = String.Format( "The directory {0} does not exist, something is probably broken with your application", updateUrlOrPath); throw new Exception(message); } var fi = new FileInfo(Path.Combine(updateUrlOrPath, "RELEASES")); if (!fi.Exists) { var message = String.Format( "The file {0} does not exist, something is probably broken with your application", fi.FullName); this.Log().Warn(message); var packages = (new DirectoryInfo(updateUrlOrPath)).GetFiles("*.nupkg"); if (packages.Length == 0) { throw new Exception(message); } // NB: Create a new RELEASES file since we've got a directory of packages ReleaseEntry.WriteReleaseFile( packages.Select(x => ReleaseEntry.GenerateFromFile(x.FullName)), fi.FullName); } releaseFile = File.ReadAllText(fi.FullName, Encoding.UTF8); progress(33); } var ret = default(UpdateInfo); var remoteReleases = ReleaseEntry.ParseReleaseFile(releaseFile); progress(66); if (!remoteReleases.Any()) { throw new Exception("Remote release File is empty or corrupted"); } ret = determineUpdateInfo(localReleases, remoteReleases, ignoreDeltaUpdates); progress(100); return(ret); }
// NB: Once we uninstall the old version of the app, we try to schedule // it to be deleted at next reboot. Unfortunately, depending on whether // the user has admin permissions, this can fail. So as a failsafe, // before we try to apply any update, we assume previous versions in the // directory are "dead" (i.e. already uninstalled, but not deleted), and // we blow them away. This is to make sure that we don't attempt to run // an uninstaller on an already-uninstalled version. async Task cleanDeadVersions(SemanticVersion originalVersion, SemanticVersion currentVersion, bool forceUninstall = false) { if (currentVersion == null) { return; } var di = new DirectoryInfo(rootAppDirectory); if (!di.Exists) { return; } this.Log().Info("cleanDeadVersions: for version {0}", currentVersion); string originalVersionFolder = null; if (originalVersion != null) { originalVersionFolder = getDirectoryForRelease(originalVersion).Name; this.Log().Info("cleanDeadVersions: exclude folder {0}", originalVersionFolder); } string currentVersionFolder = null; if (currentVersion != null) { currentVersionFolder = getDirectoryForRelease(currentVersion).Name; this.Log().Info("cleanDeadVersions: exclude folder {0}", currentVersionFolder); } // NB: If we try to access a directory that has already been // scheduled for deletion by MoveFileEx it throws what seems like // NT's only error code, ERROR_ACCESS_DENIED. Squelch errors that // come from here. var toCleanup = di.GetDirectories() .Where(x => x.Name.ToLowerInvariant().Contains("app-")) .Where(x => x.Name != currentVersionFolder && x.Name != originalVersionFolder) .Where(x => !isAppFolderDead(x.FullName)); if (forceUninstall == false) { await toCleanup.ForEachAsync(async x => { var squirrelApps = SquirrelAwareExecutableDetector.GetAllSquirrelAwareApps(x.FullName); var args = String.Format("--squirrel-obsolete {0}", x.Name.Replace("app-", "")); if (squirrelApps.Count > 0) { // For each app, run the install command in-order and wait await squirrelApps.ForEachAsync(async exe => { using (var cts = new CancellationTokenSource()) { cts.CancelAfter(10 * 1000); try { await Utility.InvokeProcessAsync(exe, args, cts.Token); } catch (Exception ex) { this.Log().ErrorException("Coudln't run Squirrel hook, continuing: " + exe, ex); } } }, 1 /* at a time */); } }); } // Include dead folders in folders to :fire: toCleanup = di.GetDirectories() .Where(x => x.Name.ToLowerInvariant().Contains("app-")) .Where(x => x.Name != currentVersionFolder && x.Name != originalVersionFolder); // Get the current process list in an attempt to not burn // directories which have running processes var runningProcesses = UnsafeUtility.EnumerateProcesses(); // Finally, clean up the app-X.Y.Z directories await toCleanup.ForEachAsync(async x => { try { if (runningProcesses.All(p => !p.Item1.StartsWith(x.FullName, StringComparison.OrdinalIgnoreCase))) { await Utility.DeleteDirectoryOrJustGiveUp(x.FullName); } if (Directory.Exists(x.FullName)) { // NB: If we cannot clean up a directory, we need to make // sure that anyone finding it later won't attempt to run // Squirrel events on it. We'll mark it with a .dead file markAppFolderAsDead(x.FullName); } } catch (UnauthorizedAccessException ex) { this.Log().WarnException("Couldn't delete directory: " + x.FullName, ex); // NB: Same deal as above markAppFolderAsDead(x.FullName); } }); // Clean up the packages directory too var releasesFile = Utility.LocalReleaseFileForAppDir(rootAppDirectory); var entries = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesFile, Encoding.UTF8)); var pkgDir = Utility.PackageDirectoryForAppDir(rootAppDirectory); var releaseEntry = default(ReleaseEntry); foreach (var entry in entries) { if (entry.Version == currentVersion) { releaseEntry = ReleaseEntry.GenerateFromFile(Path.Combine(pkgDir, entry.Filename)); continue; } File.Delete(Path.Combine(pkgDir, entry.Filename)); } ReleaseEntry.WriteReleaseFile(new[] { releaseEntry }, releasesFile); }
public async Task <UpdateInfo> CheckForUpdate( string localReleaseFile, string updateUrlOrPath, bool ignoreDeltaUpdates = false, Action <int> progress = null, IFileDownloader urlDownloader = null) { progress = progress ?? (_ => { }); var localReleases = Enumerable.Empty <ReleaseEntry>(); bool shouldInitialize = false; try { localReleases = LoadLocalReleases(localReleaseFile); } catch (Exception ex) { // Something has gone pear-shaped, let's start from scratch this.Log().WarnException("Failed to load local releases, starting from scratch", ex); shouldInitialize = true; } if (shouldInitialize) { await initializeClientAppDirectory(); } string releaseFile; // Fetch the remote RELEASES file, whether it's a local dir or an // HTTP URL if (Utility.IsHttpUrl(updateUrlOrPath)) { this.Log().Info("Downloading RELEASES file from {0}", updateUrlOrPath); try { var data = await urlDownloader.DownloadUrl(String.Format("{0}/{1}", updateUrlOrPath, "RELEASES")); releaseFile = Encoding.UTF8.GetString(data); } catch (WebException ex) { this.Log().InfoException("Download resulted in WebException (returning blank release list)", ex); releaseFile = String.Empty; } progress(33); } else { this.Log().Info("Reading RELEASES file from {0}", updateUrlOrPath); if (!Directory.Exists(updateUrlOrPath)) { var message = String.Format( "The directory {0} does not exist, something is probably broken with your application", updateUrlOrPath); throw new Exception(message); } var fi = new FileInfo(Path.Combine(updateUrlOrPath, "RELEASES")); if (!fi.Exists) { var message = String.Format( "The file {0} does not exist, something is probably broken with your application", fi.FullName); this.Log().Warn(message); var packages = (new DirectoryInfo(updateUrlOrPath)).GetFiles("*.nupkg"); if (packages.Length == 0) { throw new Exception(message); } // NB: Create a new RELEASES file since we've got a directory of packages ReleaseEntry.WriteReleaseFile( packages.Select(x => ReleaseEntry.GenerateFromFile(x.FullName)), fi.FullName); } releaseFile = File.ReadAllText(fi.FullName, Encoding.UTF8); progress(33); } var ret = default(UpdateInfo); var remoteReleases = ReleaseEntry.ParseReleaseFile(releaseFile); progress(66); if (remoteReleases.Any()) { ret = determineUpdateInfo(localReleases, remoteReleases, ignoreDeltaUpdates); } progress(100); return(ret); }
public async Task <UpdateInfo> CheckForUpdate( string localReleaseFile, string updateUrlOrPath, bool ignoreDeltaUpdates = false, Action <int> progress = null, IFileDownloader urlDownloader = null, bool startOverIfNone = false) { progress = progress ?? (_ => { }); var localReleases = Enumerable.Empty <ReleaseEntry>(); bool shouldInitialize = false; try { localReleases = Utility.LoadLocalReleases(localReleaseFile); } catch (Exception ex) { // Something has gone pear-shaped, let's start from scratch this.Log().WarnException("Failed to load local releases, starting from scratch", ex); shouldInitialize = true; } restart: if (shouldInitialize) { await initializeClientAppDirectory(); } string releaseFile; var latestLocalRelease = localReleases.Count() > 0 ? localReleases.MaxBy(x => x.Version).First() : default(ReleaseEntry); // Fetch the remote RELEASES file, whether it's a local dir or an // HTTP URL if (Utility.IsHttpUrl(updateUrlOrPath)) { if (updateUrlOrPath.EndsWith("/")) { updateUrlOrPath = updateUrlOrPath.Substring(0, updateUrlOrPath.Length - 1); } this.Log().Info("Downloading RELEASES file from {0}", updateUrlOrPath); int retries = 3; retry: try { var uri = Utility.AppendPathToUri(new Uri(updateUrlOrPath), "RELEASES"); if (latestLocalRelease != null) { uri = Utility.AddQueryParamsToUri(uri, new Dictionary <string, string> { { "id", latestLocalRelease.PackageName }, { "localVersion", latestLocalRelease.Version.ToString() }, { "arch", Environment.Is64BitOperatingSystem ? "amd64" : "x86" } }); } var data = await urlDownloader.DownloadUrl(uri.ToString()); releaseFile = Encoding.UTF8.GetString(data); } catch (WebException ex) { this.Log().InfoException("Download resulted in WebException (returning blank release list)", ex); if (retries <= 0) { throw; } retries--; goto retry; } progress(33); } else { this.Log().Info("Reading RELEASES file from {0}", updateUrlOrPath); if (!Directory.Exists(updateUrlOrPath)) { var message = String.Format( "The directory {0} does not exist, something is probably broken with your application", updateUrlOrPath); throw new Exception(message); } var fi = new FileInfo(Path.Combine(updateUrlOrPath, "RELEASES")); if (!fi.Exists) { var message = String.Format( "The file {0} does not exist, something is probably broken with your application", fi.FullName); this.Log().Warn(message); var packages = (new DirectoryInfo(updateUrlOrPath)).GetFiles("*.nupkg"); if (packages.Length == 0) { throw new Exception(message); } // NB: Create a new RELEASES file since we've got a directory of packages ReleaseEntry.WriteReleaseFile( packages.Select(x => ReleaseEntry.GenerateFromFile(x.FullName)), fi.FullName); } releaseFile = File.ReadAllText(fi.FullName, Encoding.UTF8); progress(33); } var ret = default(UpdateInfo); var remoteReleases = ReleaseEntry.ParseReleaseFile(releaseFile); progress(66); if (!remoteReleases.Any()) { throw new Exception("Remote release File is empty or corrupted"); } ret = determineUpdateInfo(localReleases, remoteReleases, ignoreDeltaUpdates); progress(100); if (startOverIfNone && !ret.ReleasesToApply.Any()) { // User has apparently re-run the installer for the version already installed. // Assume the intent is to repair a broken installation. // These rather awkward steps cause it to erase the installed-version directory // and re-create it, much like a first-time install (though it won't run the app // with the special arguments for first-time). shouldInitialize = true; localReleases = Enumerable.Empty <ReleaseEntry>(); goto restart; } return(ret); }
// NB: Once we uninstall the old version of the app, we try to schedule // it to be deleted at next reboot. Unfortunately, depending on whether // the user has admin permissions, this can fail. So as a failsafe, // before we try to apply any update, we assume previous versions in the // directory are "dead" (i.e. already uninstalled, but not deleted), and // we blow them away. This is to make sure that we don't attempt to run // an uninstaller on an already-uninstalled version. async Task cleanDeadVersions(Version originalVersion, Version currentVersion, bool forceUninstall = false) { if (currentVersion == null) { return; } var di = new DirectoryInfo(rootAppDirectory); if (!di.Exists) { return; } this.Log().Info("cleanDeadVersions: for version {0}", currentVersion); string originalVersionFolder = null; if (originalVersion != null) { originalVersionFolder = getDirectoryForRelease(originalVersion).Name; this.Log().Info("cleanDeadVersions: exclude folder {0}", originalVersionFolder); } string currentVersionFolder = null; if (currentVersion != null) { currentVersionFolder = getDirectoryForRelease(currentVersion).Name; this.Log().Info("cleanDeadVersions: exclude folder {0}", currentVersionFolder); } // NB: If we try to access a directory that has already been // scheduled for deletion by MoveFileEx it throws what seems like // NT's only error code, ERROR_ACCESS_DENIED. Squelch errors that // come from here. var toCleanup = di.GetDirectories() .Where(x => x.Name.ToLowerInvariant().Contains("app-")) .Where(x => x.Name != currentVersionFolder && x.Name != originalVersionFolder); if (forceUninstall == false) { await toCleanup.ForEachAsync(async x => { var squirrelApps = SquirrelAwareExecutableDetector.GetAllSquirrelAwareApps(x.FullName); var args = String.Format("--squirrel-obsolete {0}", x.Name.Replace("app-", "")); if (squirrelApps.Count > 0) { // For each app, run the install command in-order and wait await squirrelApps.ForEachAsync(exe => Utility.InvokeProcessAsync(exe, args), 1 /* at a time */); } }); } // Finally, clean up the app-X.Y.Z directories await toCleanup.ForEachAsync(async x => { try { await Utility.DeleteDirectoryWithFallbackToNextReboot(x.FullName); } catch (UnauthorizedAccessException ex) { this.Log().WarnException("Couldn't delete directory: " + x.FullName, ex); } }); // Clean up the packages directory too var releasesFile = Utility.LocalReleaseFileForAppDir(rootAppDirectory); var entries = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesFile, Encoding.UTF8)); var pkgDir = Utility.PackageDirectoryForAppDir(rootAppDirectory); var releaseEntry = default(ReleaseEntry); foreach (var entry in entries) { if (entry.Version == currentVersion) { releaseEntry = ReleaseEntry.GenerateFromFile(Path.Combine(pkgDir, entry.Filename)); continue; } File.Delete(Path.Combine(pkgDir, entry.Filename)); } ReleaseEntry.WriteReleaseFile(new[] { releaseEntry }, releasesFile); }