diff --git a/src/Squirrel/ReleasePackage.cs b/src/Squirrel/ReleasePackage.cs index 643fadfcb..1a611ee1a 100644 --- a/src/Squirrel/ReleasePackage.cs +++ b/src/Squirrel/ReleasePackage.cs @@ -160,34 +160,42 @@ public string CreateReleasePackage(string outputFile, string packagesRootDir = n // We must do this, or PathTooLongException may be thrown for some unicode entry names. public static void ExtractZipDecoded(string zipFilePath, string outFolder, string directoryFilter = null) { - var zf = new ZipFile(zipFilePath); + Utility.Retry(() => + { + var zf = new ZipFile(zipFilePath); - foreach (ZipEntry zipEntry in zf) { - if (!zipEntry.IsFile) continue; + foreach(ZipEntry zipEntry in zf) + { + if(!zipEntry.IsFile) + continue; - var entryFileName = Uri.UnescapeDataString(zipEntry.Name); + var entryFileName = Uri.UnescapeDataString(zipEntry.Name); - var buffer = new byte[4096]; - var zipStream = zf.GetInputStream(zipEntry); - - var fullZipToPath = Path.Combine(outFolder, entryFileName); - var directoryName = Path.GetDirectoryName(fullZipToPath); - var directoryFilter_ = new NameFilter(directoryFilter); - if (directoryFilter_.IsMatch(directoryName)) { - if (directoryName.Length > 0) { - Directory.CreateDirectory(directoryName); - } + var buffer = new byte[4096]; + var zipStream = zf.GetInputStream(zipEntry); - using (FileStream streamWriter = File.Create(fullZipToPath)) { - StreamUtils.Copy(zipStream, streamWriter, buffer); + var fullZipToPath = Path.Combine(outFolder, entryFileName); + var directoryName = Path.GetDirectoryName(fullZipToPath); + var directoryFilter_ = new NameFilter(directoryFilter); + if(directoryFilter_.IsMatch(directoryName)) + { + if(directoryName.Length > 0) + { + Directory.CreateDirectory(directoryName); + } + + using(FileStream streamWriter = File.Create(fullZipToPath)) + { + StreamUtils.Copy(zipStream, streamWriter, buffer); + } } } - } - zf.Close(); + zf.Close(); + }); } // Create zip file with entry names %-encoded, as nupkg file does. - void createZipEncoded(string zipFilePath, string folder) + void createZipEncoded(string zipFilePath, string folder) { folder = Path.GetFullPath(folder); var offset = folder.Length + (folder.EndsWith("\\", StringComparison.OrdinalIgnoreCase) ? 1 : 0); diff --git a/src/Squirrel/RetryUtility.cs b/src/Squirrel/RetryUtility.cs new file mode 100644 index 000000000..96a4e081e --- /dev/null +++ b/src/Squirrel/RetryUtility.cs @@ -0,0 +1,66 @@ +// From github.com/sillsdev/libpalaso (MIT x11) + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Squirrel +{ + /// + /// This utility can be used to wrap any call and retry it in a loop + /// until specific exceptions no longer occur. + /// + /// Number of retry attempts, pause time between attempts, and exception types + /// can all be specified or left to the defaults. + /// + /// This class came about as an attempt to mitigate certain IO issues, so the + /// defaults are specified along those lines. + /// + public static class RetryUtility + { + // Nothing special about these numbers. Just attempting to balance issues of user experience. + public const int kDefaultMaxRetryAttempts = 10; + public const int kDefaultRetryDelay = 200; + private static readonly ISet kDefaultExceptionTypesToRetry = new HashSet { Type.GetType("System.IO.IOException") }; + + public static void Retry(Action action, int maxRetryAttempts = kDefaultMaxRetryAttempts, int retryDelay = kDefaultRetryDelay, ISet exceptionTypesToRetry = null) + { + Retry(() => + { + action(); + return null; + }, maxRetryAttempts, retryDelay, exceptionTypesToRetry); + } + + public static T Retry(Func action, int maxRetryAttempts = kDefaultMaxRetryAttempts, int retryDelay = kDefaultRetryDelay, ISet exceptionTypesToRetry = null) + { + if (exceptionTypesToRetry == null) + exceptionTypesToRetry = kDefaultExceptionTypesToRetry; + + for (int attempt = 1; attempt <= maxRetryAttempts; attempt++) + { + try + { + var result = action(); + //Debug.WriteLine("Successful after {0} attempts", attempt); + return result; + } + catch (Exception e) + { + if (exceptionTypesToRetry.Contains(e.GetType())) + { + if (attempt == maxRetryAttempts) + { + //Debug.WriteLine("Failed after {0} attempts", attempt); + throw; + } + Thread.Sleep(retryDelay); + continue; + } + throw; + } + } + return default(T); + } + } +} diff --git a/src/Squirrel/UpdateManager.ApplyReleases.cs b/src/Squirrel/UpdateManager.ApplyReleases.cs index c5c105d66..35a6fcf6e 100644 --- a/src/Squirrel/UpdateManager.ApplyReleases.cs +++ b/src/Squirrel/UpdateManager.ApplyReleases.cs @@ -62,7 +62,7 @@ public async Task ApplyReleases(UpdateInfo updateInfo, bool silentInstal return getDirectoryForRelease(updateInfo.CurrentlyInstalledVersion.Version).FullName; } - var ret = await this.ErrorIfThrows(() => installPackageToAppDir(updateInfo, release), + var ret = await this.ErrorIfThrows(() => installPackageToAppDir(updateInfo, release), "Failed to install package to app dir"); progress(84); @@ -125,7 +125,7 @@ public async Task FullUninstall() if (squirrelAwareApps.Count > 0) { await squirrelAwareApps.ForEachAsync(async exe => { - using (var cts = new CancellationTokenSource()) { + using (var cts = new CancellationTokenSource()) { cts.CancelAfter(10 * 1000); try { @@ -139,7 +139,7 @@ await squirrelAwareApps.ForEachAsync(async exe => { allApps.ForEach(x => RemoveShortcutsForExecutable(x.Name, ShortcutLocation.StartMenu | ShortcutLocation.Desktop)); } - // NB: Some people attempt to uninstall apps while + // NB: Some people attempt to uninstall apps while // they're still running. I cannot even. var toKill = allApps .SelectMany(x => Process.GetProcessesByName(x.Name.Replace(".exe", ""))) @@ -161,8 +161,8 @@ await squirrelAwareApps.ForEachAsync(async exe => { await this.ErrorIfThrows(() => Utility.DeleteDirectoryOrJustGiveUp(rootAppDirectory), "Failed to delete app directory: " + rootAppDirectory); - // NB: We drop this file here so that --checkInstall will ignore - // this folder - if we don't do this, users who "accidentally" run as + // NB: We drop this file here so that --checkInstall will ignore + // this folder - if we don't do this, users who "accidentally" run as // administrator will find the app reinstalling itself on every // reboot if (!Directory.Exists(rootAppDirectory)) { @@ -331,7 +331,9 @@ Task installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) toMove.ForEach(ld => { ld.GetDirectories() - .ForEachAsync(subdir => subdir.MoveTo(subdir.FullName.Replace(ld.FullName, target.FullName))) + .ForEachAsync(subdir => + Utility.Retry(() => + subdir.MoveTo(subdir.FullName.Replace(ld.FullName, target.FullName)), retries:100)) .Wait(); ld.GetFiles() @@ -339,7 +341,7 @@ Task installPackageToAppDir(UpdateInfo updateInfo, ReleaseEntry release) 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); + Utility.Retry(() => file.MoveTo(tgt), retries: 100); }) .Wait(); }); @@ -405,7 +407,7 @@ void executeSelfUpdate(SemanticVersion currentVersion) return; } - // If we're running in the context of Update.exe, we can't + // If we're running in the context of Update.exe, we can't // update ourselves. Instead, ask the new Update.exe to do it // once we exit var us = Assembly.GetEntryAssembly(); @@ -434,7 +436,7 @@ async Task invokePostInstall(SemanticVersion currentVersion, bool isInitialInsta // For each app, run the install command in-order and wait if (!firstRunOnly) await squirrelApps.ForEachAsync(async exe => { - using (var cts = new CancellationTokenSource()) { + using (var cts = new CancellationTokenSource()) { cts.CancelAfter(30 * 1000); // Bloom --allUsers can take quite a while installing Andika try { @@ -445,7 +447,7 @@ async Task invokePostInstall(SemanticVersion currentVersion, bool isInitialInsta } }, 1 /* at a time */); - // If this is the first run, we run the apps with first-run and + // If this is the first run, we run the apps with first-run and // *don't* wait for them, since they're probably the main EXE if (squirrelApps.Count == 0) { this.Log().Warn("No apps are marked as Squirrel-aware! Going to run them all"); @@ -536,7 +538,7 @@ void updateLink(ShellLink shortcut, string newAppPath) shortcut.WorkingDirectory = newAppPath; shortcut.Target = target; - // NB: If the executable was in a previous version but not in this + // NB: If the executable was in a previous version but not in this // one, we should disappear this pin. if (!File.Exists(target)) { shortcut.Dispose(); @@ -621,7 +623,7 @@ async Task cleanDeadVersions(SemanticVersion originalVersion, SemanticVersion cu this.Log().Info("cleanDeadVersions: exclude folder {0}", currentVersionFolder); } - // NB: If we try to access a directory that has already been + // 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. @@ -638,7 +640,7 @@ await toCleanup.ForEachAsync(async x => { 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()) { + using (var cts = new CancellationTokenSource()) { cts.CancelAfter(10 * 1000); try { @@ -663,7 +665,7 @@ await toCleanup.ForEachAsync(async x => { await Utility.DeleteDirectoryOrJustGiveUp(x.FullName); if (Directory.Exists(x.FullName)) { - // NB: If we cannot clean up a directory, we need to make + // 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); diff --git a/src/Squirrel/Utility.cs b/src/Squirrel/Utility.cs index c6a7ee1dd..46e2f6774 100644 --- a/src/Squirrel/Utility.cs +++ b/src/Squirrel/Utility.cs @@ -24,7 +24,7 @@ static class Utility { public static string RemoveByteOrderMarkerIfPresent(string content) { - return string.IsNullOrEmpty(content) ? + return string.IsNullOrEmpty(content) ? string.Empty : RemoveByteOrderMarkerIfPresent(Encoding.UTF8.GetBytes(content)); } @@ -331,9 +331,13 @@ public static async Task DeleteDirectory(string directoryPath) Log().Warn(message, ex); } - var fileOperations = files.ForEachAsync(file => { - File.SetAttributes(file, FileAttributes.Normal); - File.Delete(file); + var fileOperations = files.ForEachAsync(file => + { + RetryUtility.Retry(() => + { + File.SetAttributes(file, FileAttributes.Normal); + File.Delete(file); + }); }); var directoryOperations = @@ -344,8 +348,12 @@ public static async Task DeleteDirectory(string directoryPath) Log().Debug("Now deleting folder: {0}", directoryPath); File.SetAttributes(directoryPath, FileAttributes.Normal); - try { - Directory.Delete(directoryPath, false); + try + { + RetryUtility.Retry(() => + { + Directory.Delete(directoryPath, false); + }); } catch (Exception ex) { var message = String.Format("DeleteDirectory: could not delete - {0}", directoryPath); Log().ErrorException(message, ex); @@ -362,7 +370,7 @@ public static string AppDirForVersion(string rootAppDirectory, SemanticVersion v return Path.Combine(rootAppDirectory, "app-" + version.ToString()); } - public static string PackageDirectoryForAppDir(string rootAppDirectory) + public static string PackageDirectoryForAppDir(string rootAppDirectory) { return Path.Combine(rootAppDirectory, "packages"); } @@ -381,7 +389,7 @@ public static IEnumerable LoadLocalReleases(string localReleaseFil return ReleaseEntry.ParseReleaseFile(sr.ReadToEnd()); } } - + public static ReleaseEntry FindCurrentVersion(IEnumerable localReleases) { if (!localReleases.Any()) {