public static UpdateInfo Create(ReleaseEntry currentVersion, IEnumerable<ReleaseEntry> availableReleases, string packageDirectory) { Contract.Requires(availableReleases != null); Contract.Requires(!String.IsNullOrEmpty(packageDirectory)); var latestFull = availableReleases.MaxBy(x => x.Version).FirstOrDefault(x => !x.IsDelta); if (latestFull == null) { throw new Exception("There should always be at least one full release"); } if (currentVersion == null) { return new UpdateInfo(null, new[] { latestFull }, packageDirectory); } if (currentVersion.Version == latestFull.Version) { return new UpdateInfo(currentVersion, Enumerable.Empty<ReleaseEntry>(), packageDirectory); } var newerThanUs = availableReleases .Where(x => x.Version > currentVersion.Version) .OrderBy(v => v.Version); var deltasSize = newerThanUs.Where(x => x.IsDelta).Sum(x => x.Filesize); return (deltasSize < latestFull.Filesize && deltasSize > 0) ? new UpdateInfo(currentVersion, newerThanUs.Where(x => x.IsDelta).ToArray(), packageDirectory) : new UpdateInfo(currentVersion, new[] { latestFull }, packageDirectory); }
void checksumPackage(ReleaseEntry downloadedRelease) { var targetPackage = new FileInfo( Path.Combine(rootAppDirectory, "packages", downloadedRelease.Filename)); if (!targetPackage.Exists) { this.Log().Error("File {0} should exist but doesn't", targetPackage.FullName); throw new Exception("Checksummed file doesn't exist: " + targetPackage.FullName); } if (targetPackage.Length != downloadedRelease.Filesize) { this.Log().Error("File Length should be {0}, is {1}", downloadedRelease.Filesize, targetPackage.Length); targetPackage.Delete(); throw new Exception("Checksummed file size doesn't match: " + targetPackage.FullName); } using (var file = targetPackage.OpenRead()) { var hash = Utility.CalculateStreamSHA1(file); if (!hash.Equals(downloadedRelease.SHA1,StringComparison.OrdinalIgnoreCase)) { this.Log().Error("File SHA1 should be {0}, is {1}", downloadedRelease.SHA1, hash); targetPackage.Delete(); throw new Exception("Checksum doesn't match: " + targetPackage.FullName); } } }
private void OnNewVersionAvailable(ReleaseEntry release) { _logger.Info("New version {0} available!", release.Version); System.Windows.Application.Current.Dispatcher.Invoke(delegate { _newVersionAvailable.OnNext(release); }); }
Task downloadRelease(string updateBaseUrl, ReleaseEntry releaseEntry, IFileDownloader urlDownloader, string targetFile) { if (!updateBaseUrl.EndsWith("/")) { updateBaseUrl += '/'; } var sourceFileUrl = new Uri(new Uri(updateBaseUrl), releaseEntry.BaseUrl + releaseEntry.Filename).AbsoluteUri; File.Delete(targetFile); return urlDownloader.DownloadFile(sourceFileUrl, targetFile); }
protected UpdateInfo(ReleaseEntry currentlyInstalledVersion, IEnumerable<ReleaseEntry> releasesToApply, string packageDirectory) { // NB: When bootstrapping, CurrentlyInstalledVersion is null! CurrentlyInstalledVersion = currentlyInstalledVersion; ReleasesToApply = (releasesToApply ?? Enumerable.Empty<ReleaseEntry>()).ToList(); FutureReleaseEntry = ReleasesToApply.Any() ? ReleasesToApply.MaxBy(x => x.Version).FirstOrDefault() : CurrentlyInstalledVersion; this.PackageDirectory = packageDirectory; }
Task downloadRelease(string updateBaseUrl, ReleaseEntry releaseEntry, IFileDownloader urlDownloader, string targetFile, Action<int> progress) { var baseUri = Utility.EnsureTrailingSlash(new Uri(updateBaseUrl)); var releaseEntryUrl = releaseEntry.BaseUrl + releaseEntry.Filename; if (!String.IsNullOrEmpty(releaseEntry.Query)) { releaseEntryUrl += releaseEntry.Query; } var sourceFileUrl = new Uri(baseUri, releaseEntryUrl).AbsoluteUri; File.Delete(targetFile); return urlDownloader.DownloadFile(sourceFileUrl, targetFile, progress); }
Task downloadRelease(string updateBaseUrl, ReleaseEntry releaseEntry, IFileDownloader urlDownloader, string targetFile, Action<int> progress) { if (!updateBaseUrl.EndsWith("/")) { updateBaseUrl += '/'; } var releaseEntryUrl = releaseEntry.BaseUrl + releaseEntry.Filename; if (!String.IsNullOrEmpty(releaseEntry.Query)) { releaseEntryUrl += releaseEntry.Query; } var sourceFileUrl = new Uri(new Uri(updateBaseUrl), releaseEntryUrl).AbsoluteUri; File.Delete(targetFile); return urlDownloader.DownloadFile(sourceFileUrl, targetFile, progress); }
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary <string, string> baseFileListing) { // NB: There are three cases here that we'll handle: // // 1. Exists only in new => leave it alone, we'll use it directly. // 2. Exists in both old and new => write a dummy file so we know // to keep it. // 3. Exists in old but changed in new => create a delta file // // The fourth case of "Exists only in old => delete it in new" // is handled when we apply the delta package var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, ""); if (!baseFileListing.ContainsKey(relativePath)) { this.Log().Info("{0} not found in base package, marking as new", relativePath); return; } var oldData = File.ReadAllBytes(baseFileListing[relativePath]); var newData = File.ReadAllBytes(targetFile.FullName); if (bytesAreIdentical(oldData, newData)) { this.Log().Info("{0} hasn't changed, writing dummy file", relativePath); File.Create(targetFile.FullName + ".diff").Dispose(); File.Create(targetFile.FullName + ".shasum").Dispose(); targetFile.Delete(); return; } this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName); var msDelta = new MsDeltaCompression(); try { msDelta.CreateDelta(baseFileListing[relativePath], targetFile.FullName, targetFile.FullName + ".diff"); } catch (Win32Exception ex) { this.Log().Warn("We couldn't create a delta for {0}, writing full file", targetFile.Name); return; } var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum"); File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8); targetFile.Delete(); }
async Task <string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) { string tmpDir = default(string); bool shouldDeleteTmpDir = findShortTemporaryDir(out tmpDir); var fs = new PhysicalFileSystem(tmpDir); var pkg = new OptimizedZipPackage(fs, Path.Combine(updateInfo.PackageDirectory, release.Filename)); var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); // Copy all of the files out of the lib/ dirs in the NuGet package // into our target App directory. // // NB: We sort this list in order to guarantee that if a Net20 // and a Net40 version of a DLL get shipped, we always end up // with the 4.0 version. this.Log().Info("Writing files to app directory: {0}", target.FullName); var toWrite = pkg.GetLibFiles().Where(x => pathIsInFrameworkProfile(x, appFrameworkVersion)) .OrderBy(x => x.Path) .ToList(); // NB: Because of the above NB, we cannot use ForEachAsync here, we // have to copy these files in-order. Once we fix assembly resolution, // we can kill both of these NBs. await Task.Run(() => toWrite.ForEach(x => copyFileToLocation(target, x))); await pkg.GetContentFiles().ForEachAsync(x => copyFileToLocation(target, x)); if (shouldDeleteTmpDir) { await Utility.DeleteDirectory(tmpDir); } return(target.FullName); }
async Task <ReleaseEntry> createFullPackagesFromDeltas(IEnumerable <ReleaseEntry> releasesToApply, ReleaseEntry currentVersion) { Contract.Requires(releasesToApply != null); // If there are no remote releases at all, bail if (!releasesToApply.Any()) { return(null); } // If there are no deltas in our list, we're already done if (releasesToApply.All(x => !x.IsDelta)) { return(releasesToApply.MaxBy(x => x.Version).FirstOrDefault()); } if (!releasesToApply.All(x => x.IsDelta)) { throw new Exception("Cannot apply combinations of delta and full packages"); } // Smash together our base full package and the nearest delta var ret = await Task.Run(() => { var basePkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", currentVersion.Filename)); var deltaPkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", releasesToApply.First().Filename)); var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName); return(deltaBuilder.ApplyDeltaPackage(basePkg, deltaPkg, Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))); }); if (releasesToApply.Count() == 1) { return(ReleaseEntry.GenerateFromFile(ret.InputPackageFile)); } var fi = new FileInfo(ret.InputPackageFile); var entry = ReleaseEntry.GenerateFromFile(fi.OpenRead(), fi.Name); // Recursively combine the rest of them return(await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry)); }
Task downloadRelease(string updateBaseUrl, ReleaseEntry releaseEntry, IFileDownloader urlDownloader, string targetFile, Action <int> progress) { if (!updateBaseUrl.EndsWith("/")) { updateBaseUrl += '/'; } var releaseEntryUrl = releaseEntry.BaseUrl + releaseEntry.Filename; if (!String.IsNullOrEmpty(releaseEntry.Query)) { releaseEntryUrl += releaseEntry.Query; } var sourceFileUrl = new Uri(new Uri(updateBaseUrl), releaseEntryUrl).AbsoluteUri; File.Delete(targetFile); return(urlDownloader.DownloadFile(sourceFileUrl, targetFile, progress)); }
async Task <string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) { var pkg = new ZipPackage(Path.Combine(updateInfo.PackageDirectory, release.Filename)); var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); // Copy all of the files out of the lib/ dirs in the NuGet package // into our target App directory. // // NB: We sort this list in order to guarantee that if a Net20 // and a Net40 version of a DLL get shipped, we always end up // with the 4.0 version. this.Log().Info("Writing files to app directory: {0}", target.FullName); var toWrite = pkg.GetLibFiles().Where(x => pathIsInFrameworkProfile(x, appFrameworkVersion)) .OrderBy(x => x.Path) .ToList(); // NB: Because of the above NB, we cannot use ForEachAsync here, we // have to copy these files in-order. Once we fix assembly resolution, // we can kill both of these NBs. await Task.Run(() => toWrite.ForEach(x => copyFileToLocation(target, x))); await pkg.GetContentFiles().ForEachAsync(x => copyFileToLocation(target, x)); var newCurrentVersion = updateInfo.FutureReleaseEntry.Version; // Perform post-install; clean up the previous version by asking it // which shortcuts to install, and nuking them. Then, run the app's // post install and set up shortcuts. this.ErrorIfThrows(() => runPostInstallAndCleanup(newCurrentVersion, updateInfo.IsBootstrapping)); return(target.FullName); }
bool isPackageOk(ReleaseEntry downloadedRelease) { var targetPackage = new FileInfo( Path.Combine(rootAppDirectory, "packages", downloadedRelease.Filename)); if (!targetPackage.Exists) { return(false); } if (targetPackage.Length != downloadedRelease.Filesize) { return(false); } using (var file = targetPackage.OpenRead()) { var hash = Utility.CalculateStreamSHA1(file); return(hash.Equals(downloadedRelease.SHA1, StringComparison.OrdinalIgnoreCase)); } }
private void verifyPatchedFile(string relativeFilePath, string inputFile, string tempTargetFile) { ReleaseEntry entry = ReleaseEntry.ParseReleaseEntry(File.ReadAllText(Regex.Replace(inputFile, @"\.(bs)?diff$", ".shasum"), Encoding.UTF8)); ReleaseEntry entry2 = ReleaseEntry.GenerateFromFile(tempTargetFile, null); if (entry.Filesize != entry2.Filesize) { this.Log <DeltaPackageBuilder>().Warn <string, long, long>("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath, entry.Filesize, entry2.Filesize); ChecksumFailedException exception1 = new ChecksumFailedException(); exception1.Filename = relativeFilePath; throw exception1; } if (entry.SHA1 != entry2.SHA1) { this.Log <DeltaPackageBuilder>().Warn <string, string, string>("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath, entry.SHA1, entry2.SHA1); ChecksumFailedException exception2 = new ChecksumFailedException(); exception2.Filename = relativeFilePath; throw exception2; } }
async Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) { var pkg = new ZipPackage(Path.Combine(updateInfo.PackageDirectory, release.Filename)); var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); // Copy all of the files out of the lib/ dirs in the NuGet package // into our target App directory. // // NB: We sort this list in order to guarantee that if a Net20 // and a Net40 version of a DLL get shipped, we always end up // with the 4.0 version. this.Log().Info("Writing files to app directory: {0}", target.FullName); var toWrite = pkg.GetLibFiles().Where(x => pathIsInFrameworkProfile(x, appFrameworkVersion)) .OrderBy(x => x.Path) .ToList(); // NB: Because of the above NB, we cannot use ForEachAsync here, we // have to copy these files in-order. Once we fix assembly resolution, // we can kill both of these NBs. await Task.Run(() => toWrite.ForEach(x => copyFileToLocation(target, x))); await pkg.GetContentFiles().ForEachAsync(x => copyFileToLocation(target, x)); var newCurrentVersion = updateInfo.FutureReleaseEntry.Version; // Perform post-install; clean up the previous version by asking it // which shortcuts to install, and nuking them. Then, run the app's // post install and set up shortcuts. this.ErrorIfThrows(() => runPostInstallAndCleanup(newCurrentVersion, updateInfo.IsBootstrapping)); return target.FullName; }
Task <string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release, Action <int> progressCallback) { return(Task.Run(async() => { var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); try { this.Log().Info("Writing files to app directory: {0}", target.FullName); await ReleasePackage.ExtractZipForInstall( Path.Combine(updateInfo.PackageDirectory, release.Filename), target.FullName, rootAppDirectory, progressCallback); } catch (Exception ex) { try { //Don't leave that empty directory there: its existence will prevent the user from even using the previous version. await Utility.DeleteDirectory(target.FullName); } catch (Exception error) { //This is going to be one very stuck user... this.Log().ErrorException("Failed to cleanup new directory after failed install: " + target.FullName, error); } throw ex; } return target.FullName; })); }
Task <string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) { return(Task.Run(async() => { var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); this.Log().Info("Writing files to app directory: {0}", target.FullName); await ReleasePackage.ExtractZipForInstall( Path.Combine(updateInfo.PackageDirectory, release.Filename), target.FullName); return target.FullName; })); }
public void UpdateUninstallRegisty() { var releaseContent = File.ReadAllText(Path.Combine(rootAppDirectory, "packages", "RELEASES"), Encoding.UTF8); var releases = ReleaseEntry.ParseReleaseFile(releaseContent); var latest = releases.Where(x => !x.IsDelta).OrderByDescending(x => x.Version).First(); var pkgPath = Path.Combine(rootAppDirectory, "packages", latest.Filename); var zp = new ZipPackage(pkgPath); var installInfoFile = Path.Combine(this.rootAppDirectory, ".installInfo.json"); var installInfoResult = Utility.GetInstallInfo(installInfoFile); if (installInfoResult.Success) { var verion = installInfoResult.InstallVersion as string; var productCode = installInfoResult.ProductCode as string; var appName = installInfoResult.AppName as string; var arch = installInfoResult.Arch == "x86" ? "x86" : "x64"; var key = GetUninstallRegKey(productCode, appName, arch); if (key != null) { try { key.SetValue("DisplayVersion", zp.Version.ToString()); key.Close(); this.Log().Info($"UpdateUninstallRegisty: DisplayVersion updated to {zp.Version.ToString()}"); } catch (Exception ex) { this.Log().ErrorException("UpdateUninstallRegisty: Failed to write current version to registry.", ex); } } else { this.Log().Error("UpdateUninstallRegisty: Registry key not found or no write access. Unable to update current version."); } } else { this.Log().Error("UpdateUninstallRegisty: Unable to update current version without installInfo."); } }
bool isReleaseExplicitlyHttp(ReleaseEntry x) { return x.BaseUrl != null && Uri.IsWellFormedUriString(x.BaseUrl, UriKind.Absolute); }
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary <string, string> baseFileListing) { // NB: There are three cases here that we'll handle: // // 1. Exists only in new => leave it alone, we'll use it directly. // 2. Exists in both old and new => write a dummy file so we know // to keep it. // 3. Exists in old but changed in new => create a delta file // // The fourth case of "Exists only in old => delete it in new" // is handled when we apply the delta package var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, ""); if (!baseFileListing.ContainsKey(relativePath)) { this.Log().Info("{0} not found in base package, marking as new", relativePath); return; } var oldData = File.ReadAllBytes(baseFileListing[relativePath]); var newData = File.ReadAllBytes(targetFile.FullName); if (bytesAreIdentical(oldData, newData)) { this.Log().Info("{0} hasn't changed, writing dummy file", relativePath); File.Create(targetFile.FullName + ".diff").Dispose(); File.Create(targetFile.FullName + ".shasum").Dispose(); targetFile.Delete(); return; } this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName); var msDelta = new MsDeltaCompression(); if (targetFile.Extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) || targetFile.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) || targetFile.Extension.Equals(".node", StringComparison.OrdinalIgnoreCase)) { try { msDelta.CreateDelta(baseFileListing[relativePath], targetFile.FullName, targetFile.FullName + ".diff"); goto exit; } catch (Exception) { this.Log().Warn("We couldn't create a delta for {0}, attempting to create bsdiff", targetFile.Name); } } var of = default(FileStream); try { of = File.Create(targetFile.FullName + ".bsdiff"); BinaryPatchUtility.Create(oldData, newData, of); // NB: Create a dummy corrupt .diff file so that older // versions which don't understand bsdiff will fail out // until they get upgraded, instead of seeing the missing // file and just removing it. File.WriteAllText(targetFile.FullName + ".diff", "1"); } catch (Exception ex) { this.Log().WarnException(String.Format("We really couldn't create a delta for {0}", targetFile.Name), ex); return; } finally { if (of != null) { of.Dispose(); } } exit: var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum"); File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8); targetFile.Delete(); }
async Task <string> installFromDelta(UpdateInfo updateInfo) { IEnumerable <ReleaseEntry> releasesToApply = updateInfo.ReleasesToApply; ReleaseEntry currentVersion = updateInfo.CurrentlyInstalledVersion; var packageDirectory = updateInfo.PackageDirectory; Contract.Requires(releasesToApply != null); // If there are no remote releases at all, bail if (!releasesToApply.Any()) { return(null); } // If there are no deltas in our list, install the last full one if (releasesToApply.All(x => !x.IsDelta)) { return(await installPackageToAppDir(updateInfo, releasesToApply.MaxBy(x => x.Version).FirstOrDefault())); } if (!releasesToApply.All(x => x.IsDelta)) { throw new Exception("Cannot apply combinations of delta and full packages"); } string finalOutputPath = getDirectoryForRelease(releasesToApply.Last().Version).ToString(); string localAppDirectory = Directory.GetParent(this.rootAppDirectory).FullName; string workingPath; using (Utility.WithTempDirectory(out workingPath, localAppDirectory)) { var opts = new ExtractionOptions() { ExtractFullPath = true, Overwrite = true, PreserveFileTime = true }; //Extract base file to working folder this.Log().Info("Extracting base file to working folder {0}", currentVersion.Filename); using (var za = ZipArchive.Open(Path.Combine(rootAppDirectory, "packages", currentVersion.Filename))) using (var reader = za.ExtractAllEntries()) { reader.WriteAllToDirectory(workingPath, opts); } //Apply each incremental release foreach (var delta in releasesToApply) { this.Log().Info("Applying delta release {0} to working folder", delta.Filename); var deltaPkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", delta.Filename)); var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName); deltaBuilder.ApplyDeltaPackageToWorkingDirectory(workingPath, deltaPkg); } //Save the final thing into a local full package var lastDeltaPkgFullPath = Path.Combine(rootAppDirectory, "packages", releasesToApply.Last().Filename); var fullPackageOutput = Regex.Replace(lastDeltaPkgFullPath, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); this.Log().Info("Repacking into full package: {0}", fullPackageOutput); using (var za = ZipArchive.Create()) using (var tgt = File.OpenWrite(fullPackageOutput)) { za.DeflateCompressionLevel = CompressionLevel.BestSpeed; za.AddAllFromDirectory(workingPath); za.SaveTo(tgt); } //Convert this from NuGet package format to raw folder format ReleasePackage.ConvertNuGetWorkingDirectoryForInstall(workingPath, finalOutputPath, rootAppDirectory); } return(finalOutputPath); }
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); }
Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) { return Task.Run(async () => { var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); this.Log().Info("Writing files to app directory: {0}", target.FullName); await ReleasePackage.ExtractZipForInstall( Path.Combine(updateInfo.PackageDirectory, release.Filename), target.FullName); return target.FullName; }); }
// 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 <RegistryKey> CreateUninstallerRegistryEntry(string uninstallCmd, string quietSwitch) { var releaseContent = File.ReadAllText(Path.Combine(rootAppDirectory, "packages", "RELEASES"), Encoding.UTF8); var releases = ReleaseEntry.ParseReleaseFile(releaseContent); var latest = releases.Where(x => !x.IsDelta).OrderByDescending(x => x.Version).First(); // Download the icon and PNG => ICO it. If this doesn't work, who cares var pkgPath = Path.Combine(rootAppDirectory, "packages", latest.Filename); var zp = new ZipPackage(pkgPath); var targetPng = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".png"); var targetIco = Path.Combine(rootAppDirectory, "app.ico"); // NB: Sometimes the Uninstall key doesn't exist using (var parentKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default) .CreateSubKey("Uninstall", RegistryKeyPermissionCheck.ReadWriteSubTree)) {; } var key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default) .CreateSubKey(uninstallRegSubKey + "\\" + applicationName, RegistryKeyPermissionCheck.ReadWriteSubTree); if (zp.IconUrl != null && !File.Exists(targetIco)) { try { using (var wc = Utility.CreateWebClient()) { await wc.DownloadFileTaskAsync(zp.IconUrl, targetPng); using (var fs = new FileStream(targetIco, FileMode.Create)) { if (zp.IconUrl.AbsolutePath.EndsWith("ico")) { var bytes = File.ReadAllBytes(targetPng); fs.Write(bytes, 0, bytes.Length); } else { using (var bmp = (Bitmap)Image.FromFile(targetPng)) using (var ico = Icon.FromHandle(bmp.GetHicon())) { ico.Save(fs); } } key.SetValue("DisplayIcon", targetIco, RegistryValueKind.String); } } } catch (Exception ex) { this.Log().InfoException("Couldn't write uninstall icon, don't care", ex); } finally { File.Delete(targetPng); } } var stringsToWrite = new[] { new { Key = "DisplayName", Value = zp.Title ?? zp.Description ?? zp.Summary }, new { Key = "DisplayVersion", Value = zp.Version.ToString() }, new { Key = "InstallDate", Value = DateTime.Now.ToString("yyyyMMdd") }, new { Key = "InstallLocation", Value = rootAppDirectory }, new { Key = "Publisher", Value = String.Join(",", zp.Authors) }, new { Key = "QuietUninstallString", Value = String.Format("{0} {1}", uninstallCmd, quietSwitch) }, new { Key = "UninstallString", Value = uninstallCmd }, new { Key = "URLUpdateInfo", Value = zp.ProjectUrl != null?zp.ProjectUrl.ToString() : "", } }; var dwordsToWrite = new[] { new { Key = "EstimatedSize", Value = (int)((new FileInfo(pkgPath)).Length / 1024) }, new { Key = "NoModify", Value = 1 }, new { Key = "NoRepair", Value = 1 }, new { Key = "Language", Value = 0x0409 }, }; foreach (var kvp in stringsToWrite) { key.SetValue(kvp.Key, kvp.Value, RegistryValueKind.String); } foreach (var kvp in dwordsToWrite) { key.SetValue(kvp.Key, kvp.Value, RegistryValueKind.DWord); } return(key); }
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); }
static long GetTotalUncompressedSize(UpdateInfo updateInfo, ReleaseEntry release) { long totalSize = 0; using (var stream = File.OpenRead(Path.Combine(updateInfo.PackageDirectory, release.Filename))) { var zipIn = new ZipInputStream(stream); var zipEntry = zipIn.GetNextEntry(); while (zipEntry != null) { var pathParts = zipEntry.Name.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var foundLib = false; foreach (var part in pathParts) { if (part.Equals("lib", StringComparison.InvariantCulture)) { foundLib = true; } } if (!foundLib) { zipEntry = zipIn.GetNextEntry(); continue; } totalSize += zipEntry.Size; zipEntry = zipIn.GetNextEntry(); } } return totalSize; }
Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release, Action<int> progress) { return Task.Run(async () => { var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); var totalSize = GetTotalUncompressedSize(updateInfo, release); var currentExtractedSize = 0L; using (var stream = File.OpenRead(Path.Combine(updateInfo.PackageDirectory, release.Filename))) { // progress.Report(new InstallerStep("Extracting")); var zipIn = new ZipInputStream(stream); var zipEntry = zipIn.GetNextEntry(); while (zipEntry != null) { var entryName = zipEntry.Name; var pathParts = entryName.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var foundLib = false; foreach (var part in pathParts) { if (part.Equals("lib", StringComparison.InvariantCulture)) { foundLib = true; } } if (!foundLib) { zipEntry = zipIn.GetNextEntry(); continue; } var buffer = new byte[4096]; var fullZipToPath = Path.Combine(target.FullName, entryName.Replace('/', Path.DirectorySeparatorChar)); var directoryName = zipEntry.IsFile ? Path.GetDirectoryName(fullZipToPath) : fullZipToPath; if (!string.IsNullOrEmpty(directoryName)) Directory.CreateDirectory(directoryName); using (var streamWriter = File.Create(fullZipToPath)) { currentExtractedSize += zipEntry.Size; StreamUtils.Copy(zipIn, streamWriter, buffer); progress((int)((double) currentExtractedSize * 100.0 / (double) totalSize)); } zipEntry = zipIn.GetNextEntry(); } } this.Log().Info("Writing files to app directory: {0}", target.FullName); // Move all of the files out of the lib/ dirs in the NuGet package // into our target App directory. // // NB: We sort this list in order to guarantee that if a Net20 // and a Net40 version of a DLL get shipped, we always end up // with the 4.0 version. var libDir = target.GetDirectories().First(x => x.Name.Equals("lib", StringComparison.OrdinalIgnoreCase)); var toMove = libDir.GetDirectories().OrderBy(x => x.Name); toMove.ForEach(ld => { ld.GetDirectories() .ForEachAsync(subdir => subdir.MoveTo(subdir.FullName.Replace(ld.FullName, target.FullName))) .Wait(); ld.GetFiles() .ForEachAsync(file => { var tgt = Path.Combine(target.FullName, file.Name); this.Log().Info("Moving file {0} to {1}", file.FullName, tgt); if (File.Exists(tgt)) Utility.DeleteFileHarder(tgt, true); file.MoveTo(tgt); }) .Wait(); }); await Utility.DeleteDirectory(libDir.FullName); return target.FullName; }); }
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile, Action <int> progress = null) { Contract.Requires(deltaPackage != null); Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile)); if (progress == null) { progress = i => { } } ; string workingPath; string deltaPath; using (Utility.WithTempDirectory(out deltaPath, localAppDirectory)) using (Utility.WithTempDirectory(out workingPath, localAppDirectory)) { var fz = new FastZip(); fz.ExtractZip(deltaPackage.InputPackageFile, deltaPath, null); // The percentages here are somewhat arbitrary and reflect typical Bloom file sizes. // Probably not appropriate to push upstream. // If you are thinking about improving this using FastZip's Progress event, check with JohnT. // I experimented with it and found that the PercentCompleted which it reports is useless, // toggling between 0 and 100% repeatedly...possibly it applies to a particular file? // I also found that it is VERY expensive to turn on progress reporting while unzipping; // seems to slow the process down to the point of making it take two to three times as long. // This may of course improve in a later version of the library. progress(10); fz.ExtractZip(basePackage.InputPackageFile, workingPath, null); progress(35); var pathsVisited = new List <string>(); var deltaPathRelativePaths = new DirectoryInfo(deltaPath).GetAllFilesRecursively() .Select(x => x.FullName.Replace(deltaPath + Path.DirectorySeparatorChar, "")) .ToArray(); // Apply all of the .diff files deltaPathRelativePaths .Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase)) .Where(x => !x.EndsWith(".shasum", StringComparison.InvariantCultureIgnoreCase)) .Where(x => !x.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase) || !deltaPathRelativePaths.Contains(x.Replace(".diff", ".bsdiff"))) .ForEach(file => { pathsVisited.Add(Regex.Replace(file, @"\.(bs)?diff$", "").ToLowerInvariant()); applyDiffToFile(deltaPath, file, workingPath); }); // Delete all of the files that were in the old package but // not in the new one. new DirectoryInfo(workingPath).GetAllFilesRecursively() .Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant()) .Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x)) .ForEach(x => { this.Log().Info("{0} was in old package but not in new one, deleting", x); File.Delete(Path.Combine(workingPath, x)); }); // Update all the files that aren't in 'lib' with the delta // package's versions (i.e. the nuspec file, etc etc). deltaPathRelativePaths .Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase)) .ForEach(x => { this.Log().Info("Updating metadata file: {0}", x); File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true); }); progress(50); this.Log().Info("Repacking into full package: {0}", outputFile); fz.CreateZip(outputFile, workingPath, true, null); progress(100); } return(new ReleasePackage(outputFile)); } void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary <string, string> baseFileListing) { // NB: There are three cases here that we'll handle: // // 1. Exists only in new => leave it alone, we'll use it directly. // 2. Exists in both old and new => write a dummy file so we know // to keep it. // 3. Exists in old but changed in new => create a delta file // // The fourth case of "Exists only in old => delete it in new" // is handled when we apply the delta package var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, ""); if (!baseFileListing.ContainsKey(relativePath)) { this.Log().Info("{0} not found in base package, marking as new", relativePath); return; } var oldData = File.ReadAllBytes(baseFileListing[relativePath]); var newData = File.ReadAllBytes(targetFile.FullName); if (bytesAreIdentical(oldData, newData)) { this.Log().Info("{0} hasn't changed, writing dummy file", relativePath); File.Create(targetFile.FullName + ".diff").Dispose(); File.Create(targetFile.FullName + ".shasum").Dispose(); targetFile.Delete(); return; } this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName); var msDelta = new MsDeltaCompression(); try { msDelta.CreateDelta(baseFileListing[relativePath], targetFile.FullName, targetFile.FullName + ".diff"); } catch (Win32Exception) { this.Log().Warn("We couldn't create a delta for {0}, attempting to create bsdiff", targetFile.Name); var of = default(FileStream); try { of = File.Create(targetFile.FullName + ".bsdiff"); BinaryPatchUtility.Create(oldData, newData, of); // NB: Create a dummy corrupt .diff file so that older // versions which don't understand bsdiff will fail out // until they get upgraded, instead of seeing the missing // file and just removing it. File.WriteAllText(targetFile.FullName + ".diff", "1"); } catch (Exception ex) { this.Log().WarnException(String.Format("We really couldn't create a delta for {0}", targetFile.Name), ex); return; } finally { if (of != null) { of.Dispose(); } } } var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum"); File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8); targetFile.Delete(); } void applyDiffToFile(string deltaPath, string relativeFilePath, string workingDirectory) { var inputFile = Path.Combine(deltaPath, relativeFilePath); var finalTarget = Path.Combine(workingDirectory, Regex.Replace(relativeFilePath, @"\.(bs)?diff$", "")); var tempTargetFile = default(string); Utility.WithTempFile(out tempTargetFile, localAppDirectory); try { // NB: Zero-length diffs indicate the file hasn't actually changed if (new FileInfo(inputFile).Length == 0) { this.Log().Info("{0} exists unchanged, skipping", relativeFilePath); return; } if (relativeFilePath.EndsWith(".bsdiff", StringComparison.InvariantCultureIgnoreCase)) { using (var of = File.OpenWrite(tempTargetFile)) using (var inf = File.OpenRead(finalTarget)) { this.Log().Info("Applying BSDiff to {0}", relativeFilePath); BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of); } verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile); } else if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) { this.Log().Info("Applying MSDiff to {0}", relativeFilePath); var msDelta = new MsDeltaCompression(); msDelta.ApplyDelta(inputFile, finalTarget, tempTargetFile); verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile); } else { using (var of = File.OpenWrite(tempTargetFile)) using (var inf = File.OpenRead(inputFile)) { this.Log().Info("Adding new file: {0}", relativeFilePath); inf.CopyTo(of); } } if (File.Exists(finalTarget)) { File.Delete(finalTarget); } var targetPath = Directory.GetParent(finalTarget); if (!targetPath.Exists) { targetPath.Create(); } File.Move(tempTargetFile, finalTarget); } finally { if (File.Exists(tempTargetFile)) { Utility.DeleteFileHarder(tempTargetFile, true); } } } void verifyPatchedFile(string relativeFilePath, string inputFile, string tempTargetFile) { var shaFile = Regex.Replace(inputFile, @"\.(bs)?diff$", ".shasum"); var expectedReleaseEntry = ReleaseEntry.ParseReleaseEntry(File.ReadAllText(shaFile, Encoding.UTF8)); var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile); if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) { this.Log().Warn("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath, expectedReleaseEntry.Filesize, actualReleaseEntry.Filesize); throw new ChecksumFailedException() { Filename = relativeFilePath }; } if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) { this.Log().Warn("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath, expectedReleaseEntry.SHA1, actualReleaseEntry.SHA1); throw new ChecksumFailedException() { Filename = relativeFilePath }; } } bool bytesAreIdentical(byte[] oldData, byte[] newData) { if (oldData == null || newData == null) { return(oldData == newData); } if (oldData.LongLength != newData.LongLength) { return(false); } for (long i = 0; i < newData.LongLength; i++) { if (oldData[i] != newData[i]) { return(false); } } return(true); } }
async Task <ReleaseEntry> createFullPackagesFromDeltas(IEnumerable <ReleaseEntry> releasesToApply, ReleaseEntry currentVersion, ApplyReleasesProgress progress) { Contract.Requires(releasesToApply != null); progress = progress ?? new ApplyReleasesProgress(releasesToApply.Count(), x => { }); // If there are no remote releases at all, bail if (!releasesToApply.Any()) { return(null); } // If there are no deltas in our list, we're already done if (releasesToApply.All(x => !x.IsDelta)) { return(releasesToApply.MaxBy(x => x.Version).FirstOrDefault()); } if (!releasesToApply.All(x => x.IsDelta)) { throw new Exception("Cannot apply combinations of delta and full packages"); } // Progress calculation is "complex" here. We need to known how many releases, and then give each release a similar amount of // progress. For example, when applying 5 releases: // // release 1: 00 => 20 // release 2: 20 => 40 // release 3: 40 => 60 // release 4: 60 => 80 // release 5: 80 => 100 // // Smash together our base full package and the nearest delta var ret = await Task.Run(() => { var basePkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", currentVersion.Filename)); var deltaPkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", releasesToApply.First().Filename)); var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName); return(deltaBuilder.ApplyDeltaPackage(basePkg, deltaPkg, Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant), x => progress.ReportReleaseProgress(x))); }); progress.FinishRelease(); if (releasesToApply.Count() == 1) { return(ReleaseEntry.GenerateFromFile(ret.InputPackageFile)); } var fi = new FileInfo(ret.InputPackageFile); var entry = ReleaseEntry.GenerateFromFile(fi.OpenRead(), fi.Name); // Recursively combine the rest of them return(await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry, progress)); }
private static string GetExePath(string rootAppDirectory, string exeName, ReleaseEntry thisRelease) { var releaseDir = Utility.AppDirForRelease(rootAppDirectory, thisRelease); var exePath = Path.Combine(releaseDir, exeName); if (!File.Exists(exePath)) { var matches = Directory.GetFiles(releaseDir, exeName, SearchOption.AllDirectories); exePath = matches.FirstOrDefault() ?? exePath; } return(exePath); }
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); }
// 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); }
Task <string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release, Action <int> progress) { return(Task.Run(async() => { var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); var totalSize = GetTotalUncompressedSize(updateInfo, release); var currentExtractedSize = 0L; using (var stream = File.OpenRead(Path.Combine(updateInfo.PackageDirectory, release.Filename))) { // progress.Report(new InstallerStep("Extracting")); var zipIn = new ZipInputStream(stream); var zipEntry = zipIn.GetNextEntry(); while (zipEntry != null) { var entryName = zipEntry.Name; var pathParts = entryName.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var foundLib = false; foreach (var part in pathParts) { if (part.Equals("lib", StringComparison.InvariantCulture)) { foundLib = true; } } if (!foundLib) { zipEntry = zipIn.GetNextEntry(); continue; } var buffer = new byte[4096]; var fullZipToPath = Path.Combine(target.FullName, entryName.Replace('/', Path.DirectorySeparatorChar)); var directoryName = zipEntry.IsFile ? Path.GetDirectoryName(fullZipToPath) : fullZipToPath; if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } using (var streamWriter = File.Create(fullZipToPath)) { currentExtractedSize += zipEntry.Size; StreamUtils.Copy(zipIn, streamWriter, buffer); progress((int)((double)currentExtractedSize * 100.0 / (double)totalSize)); } zipEntry = zipIn.GetNextEntry(); } } this.Log().Info("Writing files to app directory: {0}", target.FullName); // Move all of the files out of the lib/ dirs in the NuGet package // into our target App directory. // // NB: We sort this list in order to guarantee that if a Net20 // and a Net40 version of a DLL get shipped, we always end up // with the 4.0 version. var libDir = target.GetDirectories().First(x => x.Name.Equals("lib", StringComparison.OrdinalIgnoreCase)); var toMove = libDir.GetDirectories().OrderBy(x => x.Name); toMove.ForEach(ld => { ld.GetDirectories() .ForEachAsync(subdir => subdir.MoveTo(subdir.FullName.Replace(ld.FullName, target.FullName))) .Wait(); ld.GetFiles() .ForEachAsync(file => { var tgt = Path.Combine(target.FullName, file.Name); this.Log().Info("Moving file {0} to {1}", file.FullName, tgt); if (File.Exists(tgt)) { Utility.DeleteFileHarder(tgt, true); } file.MoveTo(tgt); }) .Wait(); }); await Utility.DeleteDirectory(libDir.FullName); return target.FullName; })); }
Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) { return Task.Run(async () => { var zipper = new FastZip(); var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); this.Log().Info("Writing files to app directory: {0}", target.FullName); zipper.ExtractZip( Path.Combine(updateInfo.PackageDirectory, release.Filename), target.FullName, FastZip.Overwrite.Always, (o) => true, null, @"lib", true); // Move all of the files out of the lib/ dirs in the NuGet package // into our target App directory. // // NB: We sort this list in order to guarantee that if a Net20 // and a Net40 version of a DLL get shipped, we always end up // with the 4.0 version. var libDir = target.GetDirectories().First(x => x.Name.Equals("lib", StringComparison.OrdinalIgnoreCase)); var toMove = libDir.GetDirectories().OrderBy(x => x.Name); toMove.ForEach(ld => { ld.GetDirectories() .ForEachAsync(subdir => subdir.MoveTo(subdir.FullName.Replace(ld.FullName, target.FullName))) .Wait(); ld.GetFiles() .ForEachAsync(file => { var tgt = Path.Combine(target.FullName, file.Name); this.Log().Info("Moving file {0} to {1}", file.FullName, tgt); file.MoveTo(tgt); }) .Wait(); }); await Utility.DeleteDirectory(libDir.FullName); return target.FullName; }); }
bool isReleaseExplicitlyHttp(ReleaseEntry x) { return(x.BaseUrl != null && Uri.IsWellFormedUriString(x.BaseUrl, UriKind.Absolute)); }
async Task<string> installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) { string tmpDir = default(string); bool shouldDeleteTmpDir = findShortTemporaryDir(out tmpDir); var fs = new PhysicalFileSystem(tmpDir); var pkg = new OptimizedZipPackage(fs, Path.Combine(updateInfo.PackageDirectory, release.Filename)); var target = getDirectoryForRelease(release.Version); // NB: This might happen if we got killed partially through applying the release if (target.Exists) { this.Log().Warn("Found partially applied release folder, killing it: " + target.FullName); await Utility.DeleteDirectory(target.FullName); } target.Create(); // Copy all of the files out of the lib/ dirs in the NuGet package // into our target App directory. // // NB: We sort this list in order to guarantee that if a Net20 // and a Net40 version of a DLL get shipped, we always end up // with the 4.0 version. this.Log().Info("Writing files to app directory: {0}", target.FullName); var toWrite = pkg.GetLibFiles().Where(x => pathIsInFrameworkProfile(x, appFrameworkVersion)) .OrderBy(x => x.Path) .ToList(); // NB: Because of the above NB, we cannot use ForEachAsync here, we // have to copy these files in-order. Once we fix assembly resolution, // we can kill both of these NBs. await Task.Run(() => toWrite.ForEach(x => copyFileToLocation(target, x))); await pkg.GetContentFiles().ForEachAsync(x => copyFileToLocation(target, x)); if (shouldDeleteTmpDir) { await Utility.DeleteDirectory(tmpDir); } return target.FullName; }
internal async Task <List <ReleaseEntry> > updateLocalReleasesFile() { return(await Task.Run(() => ReleaseEntry.BuildReleasesFile(Utility.PackageDirectoryForAppDir(rootAppDirectory)))); }
private void UpdateLogic() { SquirrelAwareApp.HandleEvents( onInitialInstall: v => OnInitialInstall(ref _UpdateManager), onAppUpdate: v => OnAppUpdate(ref _UpdateManager), onAppUninstall: v => OnAppUninstall(ref _UpdateManager)); try { Squirrel.ReleaseEntry releaseEntry = null; Task.Run(async() => { try { _UpdateManager = await UpdateManager.GitHubUpdateManager(UpdatePath, null, null, null, CmdArgsStatus.PreRelease); //var updates = await _UpdateManager.CheckForUpdate(); //var lastVersion = updates?.ReleasesToApply?.OrderBy(x => x.Version).LastOrDefault(); releaseEntry = await _UpdateManager.UpdateApp(); } catch (Exception e) { Logger.WriteLog(e); try { if (_UpdateManager != null) { _UpdateManager.Dispose(); } } catch (Exception ex) { Logger.WriteLog(ex); } } }).Wait(); string updateInfo = String.Empty; try { if (releaseEntry != null) { updateInfo = releaseEntry.BaseUrl + Environment.NewLine; updateInfo += releaseEntry.EntryAsString + Environment.NewLine; updateInfo += releaseEntry.PackageName + Environment.NewLine; updateInfo += releaseEntry.Version + Environment.NewLine; updateInfo += "IsDelta: " + releaseEntry.IsDelta + Environment.NewLine; } else { Logger.WriteLog("No Updates Found"); } if (updateInfo.Length > 0) { Logger.WriteLog(updateInfo); } } catch (Exception ex3) { Logger.WriteLog(ex3); } } catch (Exception ex) { Logger.WriteLog(ex); } }
async Task<ReleaseEntry> createFullPackagesFromDeltas(IEnumerable<ReleaseEntry> releasesToApply, ReleaseEntry currentVersion) { Contract.Requires(releasesToApply != null); // If there are no remote releases at all, bail if (!releasesToApply.Any()) { return null; } // If there are no deltas in our list, we're already done if (releasesToApply.All(x => !x.IsDelta)) { return releasesToApply.MaxBy(x => x.Version).FirstOrDefault(); } if (!releasesToApply.All(x => x.IsDelta)) { throw new Exception("Cannot apply combinations of delta and full packages"); } // Smash together our base full package and the nearest delta var ret = await Task.Run(() => { var basePkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", currentVersion.Filename)); var deltaPkg = new ReleasePackage(Path.Combine(rootAppDirectory, "packages", releasesToApply.First().Filename)); var deltaBuilder = new DeltaPackageBuilder(Directory.GetParent(this.rootAppDirectory).FullName); return deltaBuilder.ApplyDeltaPackage(basePkg, deltaPkg, Regex.Replace(deltaPkg.InputPackageFile, @"-delta.nupkg$", ".nupkg", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)); }); if (releasesToApply.Count() == 1) { return ReleaseEntry.GenerateFromFile(ret.InputPackageFile); } var fi = new FileInfo(ret.InputPackageFile); var entry = ReleaseEntry.GenerateFromFile(fi.OpenRead(), fi.Name); // Recursively combine the rest of them return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry); }
private void UpdateLogic() { _UpdateManager = null; SquirrelAwareApp.HandleEvents( onInitialInstall: v => OnInitialInstall(ref _UpdateManager), onAppUpdate: v => OnAppUpdate(ref _UpdateManager), onAppUninstall: v => OnAppUninstall(ref _UpdateManager)); try { Squirrel.ReleaseEntry releaseEntry = null; bool newUpdateExist = false; Task.Run(async() => { try { _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.InitializingUpdater }).Forget(); using (_UpdateManager = await UpdateManager.GitHubUpdateManager(UpdatePath, null, null, null, _CheckPrerelease)) { try { _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.LookingForUpdates }).Forget(_Logger); var updateInfo = await _UpdateManager.CheckForUpdate(); if (updateInfo.ReleasesToApply.Count > 0) { newUpdateExist = true; } if (newUpdateExist) { _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.UpdateFound }).Forget(_Logger); } else { _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.NoUpdatesFound }).Forget(_Logger); } if (newUpdateExist) { _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.DownloadingUpdate }).Forget(_Logger); } releaseEntry = await _UpdateManager.UpdateApp(); if (newUpdateExist) { _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.UpdateDownloaded }).Forget(_Logger); } } catch (Exception e) { _Logger?.WriteLog(e.ToString()); } string updateTextInfo = String.Empty; try { if (releaseEntry != null) { updateTextInfo = releaseEntry.BaseUrl + Environment.NewLine; updateTextInfo += releaseEntry.EntryAsString + Environment.NewLine; updateTextInfo += releaseEntry.PackageName + Environment.NewLine; updateTextInfo += releaseEntry.Version + Environment.NewLine; updateTextInfo += "IsDelta: " + releaseEntry.IsDelta + Environment.NewLine; } else { _Logger?.WriteLog("No Updates Found"); } if (updateTextInfo.Length > 0) { _Logger?.WriteLog(updateTextInfo); } _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.UpdatingFinished }).Forget(_Logger); if (newUpdateExist) { _UpdatingStateChanged.InvokeAsync(new UpdatingState(this) { Text = String.Empty, UpdateSate = UpdateSate.ReadyToRestart }).Forget(_Logger); } } catch (Exception ex3) { _Logger?.WriteLog(ex3.ToString()); } }//using } catch (Exception ex0) { _Logger?.WriteLog(ex0.ToString()); } }).Wait(); _UpdateManager = null; } catch (Exception ex) { _Logger?.WriteLog(ex.ToString()); } }
public static string AppDirForRelease(string rootAppDirectory, ReleaseEntry entry) { return(Path.Combine(rootAppDirectory, "app-" + entry.Version.ToString())); }
private static string ToLog(ReleaseEntry entry) { return entry == null ? "null" : entry.EntryAsString; }