Cake: DeleteDirectory throws exception if directory doesn't exist

Created on 18 Oct 2017  路  6Comments  路  Source: cake-build/cake

What You Are Seeing?

This line;

DeleteDirectory(directory, new DeleteDirectorySettings {
  Recursive = true,
  Force = true
});

generates this error if directory does not exist.

An error occurred when executing task 'demo-of-delete-issue'.
Error: One or more errors occurred.
        The directory 'c:\path\to\directory\that\doesn't\exist' does not exist.

What is Expected?

Since the desired state is that the directory does not exist, the fact that there is nothing to delete means we are already in the desired state and should not throw an exception. (or provide an option to suppress the exception in the event of non-existance.)

What version of Cake are you using?

0.23.0

Are you running on a 32 or 64 bit system?

64bit

What environment are you running on? Windows? Linux? Mac?

Windows 8.1

Are you running on a CI Server? If so, which one?

This script will be run on Team City (although the example above was achieved at a powershell prompt)

How Did You Get This To Happen? (Steps to Reproduce)

Task("demo-of-delete-issue")
    .Does(()=>
    {        
        var directory = "c:\\path\\to\\directory\\that\\doesn't\\exist";   
        DeleteDirectory(directory, new DeleteDirectorySettings {
            Recursive = true,
            Force = true
        });
    });

var target = Argument("target", "demo-of-delete-issue");
RunTarget(target);

Output Log

Preparing to run build script...
Running build script...
Module directory does not exist.
Analyzing build script...
Analyzing C:/dev/...../build-scripts/test.cake...
Processing build script...
Adding assembly reference to mscorlib.dll...
Adding assembly reference to System.Core.dll...
Adding assembly reference to Cake.Core.dll...
Adding assembly reference to Cake.Common.dll...
Adding assembly reference to Cake.exe...
Adding assembly reference to System.dll...
Adding assembly reference to System.Xml.dll...
Adding assembly reference to System.Xml.Linq.dll...
Adding assembly reference to System.Data.dll...
Adding assembly reference to System.Runtime.dll...
Importing namespace Cake.Common...
Importing namespace Cake.Common.Build...
Importing namespace Cake.Common.Build.AppVeyor...
Importing namespace Cake.Common.Build.AppVeyor.Data...
Importing namespace Cake.Common.Build.Bamboo...
Importing namespace Cake.Common.Build.Bamboo.Data...
Importing namespace Cake.Common.Build.BitbucketPipelines...
Importing namespace Cake.Common.Build.BitbucketPipelines.Data...
Importing namespace Cake.Common.Build.Bitrise...
Importing namespace Cake.Common.Build.Bitrise.Data...
Importing namespace Cake.Common.Build.ContinuaCI...
Importing namespace Cake.Common.Build.ContinuaCI.Data...
Importing namespace Cake.Common.Build.GitLabCI...
Importing namespace Cake.Common.Build.GitLabCI.Data...
Importing namespace Cake.Common.Build.GoCD...
Importing namespace Cake.Common.Build.GoCD.Data...
Importing namespace Cake.Common.Build.Jenkins...
Importing namespace Cake.Common.Build.Jenkins.Data...
Importing namespace Cake.Common.Build.MyGet...
Importing namespace Cake.Common.Build.TeamCity...
Importing namespace Cake.Common.Build.TFBuild...
Importing namespace Cake.Common.Build.TFBuild.Data...
Importing namespace Cake.Common.Build.TravisCI...
Importing namespace Cake.Common.Build.TravisCI.Data...
Importing namespace Cake.Common.Diagnostics...
Importing namespace Cake.Common.IO...
Importing namespace Cake.Common.IO.Paths...
Importing namespace Cake.Common.Net...
Importing namespace Cake.Common.Security...
Importing namespace Cake.Common.Solution...
Importing namespace Cake.Common.Solution.Project...
Importing namespace Cake.Common.Solution.Project.Properties...
Importing namespace Cake.Common.Solution.Project.XmlDoc...
Importing namespace Cake.Common.Text...
Importing namespace Cake.Common.Tools...
Importing namespace Cake.Common.Tools.Cake...
Importing namespace Cake.Common.Tools.Chocolatey...
Importing namespace Cake.Common.Tools.Chocolatey.ApiKey...
Importing namespace Cake.Common.Tools.Chocolatey.Config...
Importing namespace Cake.Common.Tools.Chocolatey.Download...
Importing namespace Cake.Common.Tools.Chocolatey.Features...
Importing namespace Cake.Common.Tools.Chocolatey.Install...
Importing namespace Cake.Common.Tools.Chocolatey.New...
Importing namespace Cake.Common.Tools.Chocolatey.Pack...
Importing namespace Cake.Common.Tools.Chocolatey.Pin...
Importing namespace Cake.Common.Tools.Chocolatey.Push...
Importing namespace Cake.Common.Tools.Chocolatey.Sources...
Importing namespace Cake.Common.Tools.Chocolatey.Uninstall...
Importing namespace Cake.Common.Tools.Chocolatey.Upgrade...
Importing namespace Cake.Common.Tools.DotCover...
Importing namespace Cake.Common.Tools.DotCover.Analyse...
Importing namespace Cake.Common.Tools.DotCover.Cover...
Importing namespace Cake.Common.Tools.DotCover.Merge...
Importing namespace Cake.Common.Tools.DotCover.Report...
Importing namespace Cake.Common.Tools.DotNetCore...
Importing namespace Cake.Common.Tools.DotNetCore.Build...
Importing namespace Cake.Common.Tools.DotNetCore.Clean...
Importing namespace Cake.Common.Tools.DotNetCore.Execute...
Importing namespace Cake.Common.Tools.DotNetCore.MSBuild...
Importing namespace Cake.Common.Tools.DotNetCore.NuGet.Delete...
Importing namespace Cake.Common.Tools.DotNetCore.NuGet.Push...
Importing namespace Cake.Common.Tools.DotNetCore.Pack...
Importing namespace Cake.Common.Tools.DotNetCore.Publish...
Importing namespace Cake.Common.Tools.DotNetCore.Restore...
Importing namespace Cake.Common.Tools.DotNetCore.Run...
Importing namespace Cake.Common.Tools.DotNetCore.Test...
Importing namespace Cake.Common.Tools.DotNetCore.Tool...
Importing namespace Cake.Common.Tools.DotNetCore.VSTest...
Importing namespace Cake.Common.Tools.DupFinder...
Importing namespace Cake.Common.Tools.Fixie...
Importing namespace Cake.Common.Tools.GitLink...
Importing namespace Cake.Common.Tools.GitReleaseManager...
Importing namespace Cake.Common.Tools.GitReleaseManager.AddAssets...
Importing namespace Cake.Common.Tools.GitReleaseManager.Close...
Importing namespace Cake.Common.Tools.GitReleaseManager.Create...
Importing namespace Cake.Common.Tools.GitReleaseManager.Export...
Importing namespace Cake.Common.Tools.GitReleaseManager.Publish...
Importing namespace Cake.Common.Tools.GitReleaseNotes...
Importing namespace Cake.Common.Tools.GitVersion...
Importing namespace Cake.Common.Tools.ILMerge...
Importing namespace Cake.Common.Tools.ILRepack...
Importing namespace Cake.Common.Tools.InnoSetup...
Importing namespace Cake.Common.Tools.InspectCode...
Importing namespace Cake.Common.Tools.MSBuild...
Importing namespace Cake.Common.Tools.MSTest...
Importing namespace Cake.Common.Tools.NSIS...
Importing namespace Cake.Common.Tools.NuGet...
Importing namespace Cake.Common.Tools.NuGet.Add...
Importing namespace Cake.Common.Tools.NuGet.Init...
Importing namespace Cake.Common.Tools.NuGet.Install...
Importing namespace Cake.Common.Tools.NuGet.List...
Importing namespace Cake.Common.Tools.NuGet.Pack...
Importing namespace Cake.Common.Tools.NuGet.Push...
Importing namespace Cake.Common.Tools.NuGet.Restore...
Importing namespace Cake.Common.Tools.NuGet.SetApiKey...
Importing namespace Cake.Common.Tools.NuGet.SetProxy...
Importing namespace Cake.Common.Tools.NuGet.Sources...
Importing namespace Cake.Common.Tools.NuGet.Update...
Importing namespace Cake.Common.Tools.NUnit...
Importing namespace Cake.Common.Tools.OctopusDeploy...
Importing namespace Cake.Common.Tools.OpenCover...
Importing namespace Cake.Common.Tools.ReportGenerator...
Importing namespace Cake.Common.Tools.ReportUnit...
Importing namespace Cake.Common.Tools.Roundhouse...
Importing namespace Cake.Common.Tools.SignTool...
Importing namespace Cake.Common.Tools.SpecFlow...
Importing namespace Cake.Common.Tools.SpecFlow.StepDefinitionReport...
Importing namespace Cake.Common.Tools.SpecFlow.TestExecutionReport...
Importing namespace Cake.Common.Tools.TextTransform...
Importing namespace Cake.Common.Tools.VSTest...
Importing namespace Cake.Common.Tools.VSWhere...
Importing namespace Cake.Common.Tools.VSWhere.All...
Importing namespace Cake.Common.Tools.VSWhere.Latest...
Importing namespace Cake.Common.Tools.VSWhere.Legacy...
Importing namespace Cake.Common.Tools.VSWhere.Product...
Importing namespace Cake.Common.Tools.WiX...
Importing namespace Cake.Common.Tools.WiX.Heat...
Importing namespace Cake.Common.Tools.XBuild...
Importing namespace Cake.Common.Tools.XUnit...
Importing namespace Cake.Common.Xml...
Importing namespace Cake.Core...
Importing namespace Cake.Core.Diagnostics...
Importing namespace Cake.Core.IO...
Importing namespace Cake.Core.Scripting...
Importing namespace System...
Importing namespace System.Collections.Generic...
Importing namespace System.IO...
Importing namespace System.Linq...
Importing namespace System.Text...
Importing namespace System.Threading.Tasks...
Compiling build script...

========================================
demo-of-delete-issue
========================================
Executing task: demo-of-delete-issue
An error occurred when executing task 'demo-of-delete-issue'.
Error: System.AggregateException: One or more errors occurred. ---> System.IO.IOException: The directory 'c:/path/to/dir
ectory/that/doesn't/exist' does not exist.
   at Cake.Common.IO.DirectoryDeleter.Delete(ICakeContext context, DirectoryPath path, DeleteDirectorySettings settings)

   at Submission#0.DeleteDirectory(DirectoryPath path, DeleteDirectorySettings settings)
   at Submission#0.<<Initialize>>b__0_0()
   at Cake.Core.CakeTaskBuilderExtensions.<>c__DisplayClass8_0.<Does>b__0(ICakeContext x)
   at Cake.Core.ActionTask.<Execute>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.DefaultExecutionStrategy.<ExecuteAsync>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.CakeEngine.<ExecuteTaskAsync>d__29.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.CakeEngine.<RunTargetAsync>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Scripting.BuildScriptHost.<RunTargetAsync>d__3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.Scripting.ScriptHost.RunTarget(String target)
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.<RunSubmissionsAsync>d__9`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.CodeAnalysis.Scripting.Script`1.<RunSubmissionsAsync>d__21.MoveNext()
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at Cake.Scripting.Roslyn.RoslynScriptSession.Execute(Script script)
   at Cake.Core.Scripting.ScriptRunner.Run(IScriptHost host, FilePath scriptPath, IDictionary`2 arguments)
   at Cake.Commands.BuildCommand.Execute(CakeOptions options)
   at Cake.CakeApplication.Run(CakeOptions options)
   at Cake.Program.Main()
---> (Inner Exception #0) System.IO.IOException: The directory 'c:/path/to/directory/that/doesn't/exist' does not exist.

   at Cake.Common.IO.DirectoryDeleter.Delete(ICakeContext context, DirectoryPath path, DeleteDirectorySettings settings)

   at Submission#0.DeleteDirectory(DirectoryPath path, DeleteDirectorySettings settings)
   at Submission#0.<<Initialize>>b__0_0()
   at Cake.Core.CakeTaskBuilderExtensions.<>c__DisplayClass8_0.<Does>b__0(ICakeContext x)
   at Cake.Core.ActionTask.<Execute>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.DefaultExecutionStrategy.<ExecuteAsync>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.CakeEngine.<ExecuteTaskAsync>d__29.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.CakeEngine.<RunTargetAsync>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Scripting.BuildScriptHost.<RunTargetAsync>d__3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Cake.Core.Scripting.ScriptHost.RunTarget(String target)
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.<RunSubmissionsAsync>d__9`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.CodeAnalysis.Scripting.Script`1.<RunSubmissionsAsync>d__21.MoveNext()<---
Improvement

Most helpful comment

It occurs to me that CreateDirectory does not throw an exception if the directory already exists. So, for consistency, CreateDirectory either should throw an exception is the directory pre-exists or DeleteDirectory should not if it doesn't exist.

All 6 comments

Hi @colinangusmackay!
In my opinion this is the correct behavior. If you take a look into C# implementation of Directory delete you will see the same logic for example.

Why don't you validate if the Directory Exists?

if (!DirectoryExists(directory))
{
    DeleteDirectory(directory, new DeleteDirectorySettings {
        Recursive = true,
        Force = true
    });
}

The .NET Framework and CAKE operate in different domains. Just because .NET does a thing, does not mean CAKE has to do that thing.

The purpose of CAKE is to build and deploy software. In pursuit of that goal you get the system in to a desired state, and therefore any operation to get to that state should be idempotent. In other words, if the system is already in that state the operation does nothing, if it is not in the state then it does the job it needs to do to get to that state.

Specifically, in the case of deleting a directory, the idempotent version will do nothing if the directory already does not exist, or delete it if it does exist. It should only throw exceptions if it cannot achieve its goal (e.g. Access is denied)

While, I accept that I could use your work around (and I already do that) I should not need to do so.

Directory and file existence checks are also prone to TOCTTOU race conditions and should never be used to make a decision about a subsequent operation. Right now the only safe thing do to is catch the Directory/FileNotFoundException because that is the only atomic API that .NET provides.

But one person's trash is another's treasure; I'd hate for you to throw away the bit of information about whether the thing existed. Right now we have a perfectly suited atomic operation; the only problem is that the information is conveyed by the presence or lack of an exception. It's ugly but as far as that goes, it's as robust as it needs to be. I have methods like FileStream OpenReadIfExists(string path) to hide the ugliness while preserving the atomicity.

You could add this to your lib.cake:

bool DeleteDirectoryIfExists(string path)
{
    try
    {
        DeleteDirectory(path, new DeleteDirectorySettings {
            Recursive = true,
            Force = true
        });
        return true;
    }
    catch (IOException ex) when (GetWinErrorCode(ex) == WinErrorCode.PathNotFound)
    {    
        return false;
    }
}

WinErrorCode GetWinErrorCode(Exception ex)
{
    switch (ex)
    {   
        case null:
            throw new ArgumentNullException(nameof(ex));
        case Win32Exception win32Exception:
            return unchecked((WinErrorCode)win32Exception.NativeErrorCode);
        default:
            return unchecked((WinErrorCode)ex.HResult);
    }
}

enum WinErrorCode : ushort
{
    // Add others (but only if relevant):
    FileNotFound = 2,
    PathNotFound = 3,
    BadNetpath = 53,
    BadNetName = 67
}

It occurs to me that CreateDirectory does not throw an exception if the directory already exists. So, for consistency, CreateDirectory either should throw an exception is the directory pre-exists or DeleteDirectory should not if it doesn't exist.

I do agree with that. Especially because we also have EnsureDirectoryExists.

We want a EnsureDirectoryDoesntExist(..). Personally I want this behaviour from Cake:

  1. CreateDirectory throws if directory already exists
  2. EnsureDirectoryExists doesnt throw if directory exists, otherwise creates
  3. DeleteDirectory throws if directory doesn't exist
  4. EnsureDirectoryDoesntExist doesnt throw if directory does not exist, otherwise deletes

I agree with OP. Cake should be all about convenience. The two Ensure.. overloads is the most common operation. Simply having a wrapper around already existing .NET method is a waste of effort. We could as well call System.IO methods.

Was this page helpful?
0 / 5 - 0 ratings