-
-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Try to defend against interference by retrying some io operations. #24
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// From github.com/sillsdev/libpalaso (MIT x11) | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
|
||
namespace Squirrel | ||
{ | ||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
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<Type> kDefaultExceptionTypesToRetry = new HashSet<Type> { Type.GetType("System.IO.IOException") }; | ||
|
||
public static void Retry(Action action, int maxRetryAttempts = kDefaultMaxRetryAttempts, int retryDelay = kDefaultRetryDelay, ISet<Type> exceptionTypesToRetry = null) | ||
{ | ||
Retry<object>(() => | ||
{ | ||
action(); | ||
return null; | ||
}, maxRetryAttempts, retryDelay, exceptionTypesToRetry); | ||
} | ||
|
||
public static T Retry<T>(Func<T> action, int maxRetryAttempts = kDefaultMaxRetryAttempts, int retryDelay = kDefaultRetryDelay, ISet<Type> 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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,7 +62,7 @@ public async Task<string> 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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All these trailing-space deletions are distracting, especially in a repo that's mainly someone else's. |
||
"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,15 +331,17 @@ Task<string> 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(() => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, anything we've seen to suggest that other exception types should be retried? |
||
subdir.MoveTo(subdir.FullName.Replace(ld.FullName, target.FullName)), retries:100)) | ||
.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); | ||
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); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will probably work, but it means redoing the whole (100m or so?) extract on every failure. I would have been more inclined to put the Retry calls just (or also) around the directory creation and the individual file extracts.
Will the file extract operations succeed if (on a retry) the file already exists?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thought: do we know whether failure to extract/create directory will throw IOException or something else (a permission exception? a Zip utility exception?). May need to pass more exception types to Retry.