diff --git a/Directory.Build.props b/Directory.Build.props index a48c60e..210237e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ By hook or by crook, perform operations on files and directories. If they are in use by a process, kill the process. - 1.2.1 + 1.3.0 https://github.com/domsleee/forceops https://github.com/domsleee/forceops git diff --git a/ForceOps.Test/src/ProgramTest.cs b/ForceOps.Test/src/ProgramTest.cs index 5b9ae6b..3caa82a 100644 --- a/ForceOps.Test/src/ProgramTest.cs +++ b/ForceOps.Test/src/ProgramTest.cs @@ -40,6 +40,21 @@ public void SuccessfulChildDoesntThrowException() testContext.relaunchAsElevatedMock.Verify(t => t.RelaunchAsElevated(It.IsAny>(), It.IsAny()), Times.Once()); } + [Fact] + public void RetryDelayAndMaxRetriesWork() + { + using var launchedProcess = LaunchProcessInDirectory(tempDirectoryPath); + var testContext = new TestContext(); + testContext.relaunchAsElevatedMock.Setup(t => t.RelaunchAsElevated(It.IsAny>(), It.IsAny())).Returns(0); + testContext.forceOpsContext.processKiller = new Mock().Object; + + var forceOps = new ForceOps(new[] { "delete", tempDirectoryPath, "--retry-delay", "33", "--max-retries", "8" }, testContext.forceOpsContext); + Assert.Equal(0, forceOps.Run()); + + testContext.relaunchAsElevatedMock.Verify(t => t.RelaunchAsElevated(It.IsAny>(), It.IsAny()), Times.Once()); + Assert.Contains("Beginning retry 1/8 in 33ms.", testContext.fakeLoggerFactory.GetAllLogsString()); + } + [Fact] public void ExceptionThrownIfAlreadyElevated() { diff --git a/ForceOps/src/ForceOps.cs b/ForceOps/src/ForceOps.cs index 0829afa..de177cf 100644 --- a/ForceOps/src/ForceOps.cs +++ b/ForceOps/src/ForceOps.cs @@ -61,26 +61,32 @@ Command CreateDeleteCommand() Arity = ArgumentArity.OneOrMore }; - var forceOption = new Option(new[] { "-f", "--force" }, "Ignore nonexistent files and arguments"); - var disableElevate = new Option(new[] { "-e", "--disable-elevate" }, "Do not attempt to elevate if the file can't be deleted"); + var forceOption = new Option(new[] { "-f", "--force" }, "Ignore nonexistent files and arguments."); + var disableElevate = new Option(new[] { "-e", "--disable-elevate" }, "Do not attempt to elevate if the file can't be deleted."); + var retryDelay = new Option(new[] { "-d", "--retry-delay" }, () => 500, "Delay when retrying to delete a file, after deleting processes holding a lock. (default: 500)"); + var maxRetries = new Option(new[] { "-n", "--max-retries" }, () => 5, "Number of retries when deleting a locked file. (default: 5)"); var deleteCommand = new Command("delete", "Delete files or a directories recursively.") { filesToDeleteArgument, forceOption, - disableElevate + disableElevate, + retryDelay, + maxRetries }; deleteCommand.AddAlias("rm"); deleteCommand.AddAlias("remove"); - deleteCommand.SetHandler(DeleteCommand, filesToDeleteArgument, forceOption, disableElevate); + deleteCommand.SetHandler(DeleteCommand, filesToDeleteArgument, forceOption, disableElevate, retryDelay, maxRetries); return deleteCommand; } - void DeleteCommand(string[] filesOrDirectoriesToDelete, bool force, bool disableElevate) + void DeleteCommand(string[] filesOrDirectoriesToDelete, bool force, bool disableElevate, int retryDelay, int maxRetries) { RunWithRelaunchAsElevated(() => { + forceOpsContext.maxRetries = maxRetries; + forceOpsContext.retryDelay = TimeSpan.FromMilliseconds(retryDelay); var deleter = new FileAndDirectoryDeleter(forceOpsContext); filesOrDirectoriesToDelete = filesOrDirectoriesToDelete.Select(file => DirectoryUtils.CombineWithCWDAndGetAbsolutePath(file)).ToArray(); foreach (var file in filesOrDirectoriesToDelete)