5

this:

Directory.Delete(dir, true);

is not synchronous.

On the immediately proceeding line you can still manipulate/read the directory.

For example, this:

Directory.Delete(destinationDir, true);
Directory.CreateDirectory(destinationDir);
Thread.Sleep(1000);

results in the folder not existing. The delete runs async, CreateDirectory doesn't create because it already exists, then delete actually fires and removes the directory.

Is there an IO API that will give me consistency?

Answer involving Thread.Sleep will invoke Zalgo. I want a real solution please.

Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231
  • 3
    How about looping `while(Directory.Exists(destinationDir));` before `Directory.Create()`? – Visual Vincent Apr 03 '16 at 10:37
  • I dont see anything async about [`Directory.Delete`](http://referencesource.microsoft.com/#mscorlib/system/io/directory.cs,80e8dc4729bc4b3f). – Yuval Itzchakov Apr 03 '16 at 10:51
  • @VisualVincent I'm not looking for workarounds, surely NTFS/.NET can't be so broken that there isnt a real solution – Andrew Bullock Apr 03 '16 at 10:53
  • @YuvalItzchakov me neither, but it behaves asynchronously. try it – Andrew Bullock Apr 03 '16 at 10:54
  • @Yuval : Well it's executed by the system, which I guess doesn't block anything in order to not freeze the OS/stop operations completely. – Visual Vincent Apr 03 '16 at 10:54
  • It's not broken, but I don't think there is a reason for the system to block the deletion, so it hasn't been implemented. And if some method like that exists, I guess you have to go down to C/C++ level. – Visual Vincent Apr 03 '16 at 10:56
  • 3
    @VisualVincent kernel32.dll [`DeleteFile`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363915(v=vs.85).aspx) seems to explain why: *The DeleteFile function **marks a file for deletion on close**. Therefore, the **file deletion does not occur until the last handle to the file is closed.** Subsequent calls to CreateFile to open the file fail with ERROR_ACCESS_DENIED.* – Yuval Itzchakov Apr 03 '16 at 10:57
  • @AndrewBullock Do you have any open handles on the file while attempting to delete the directory? – Yuval Itzchakov Apr 03 '16 at 11:00
  • @Yuval : Perhaps `CreateDirectory` creates a new handle faster than the directory can be marked for deletion? Thus the handle is closed after it's been "recreated" and then the folder gets deleted? (Or it just throws an invisible exception) - According to the documentation for `RemoveFolder` it's the same with waiting for the last handle to close. – Visual Vincent Apr 03 '16 at 11:04
  • @VisualVincent Could be. I'm no WinAPI expert, but it's seems to be doing something similar to what you're describing. – Yuval Itzchakov Apr 03 '16 at 11:10
  • @Yuval : I guess one could perform a C++ test calling `RemoveDirectory` first, then `CreateDirectory` in the next line and see what happens. – Visual Vincent Apr 03 '16 at 11:13
  • this shouldn't happen unless you're holding a handle in some way - which isn't easy to achieve normally and via typical C# handling. I.e. op should be and feel pretty synchronous (it isn't, WInAPI functions underneath are all 'marking' only). One way to 'lock yourself out' is to enumerate the dir (w/ something in it) - and try a Delete while inside of the loop (timing wise, could be another thread). – NSGaga-mostly-inactive Apr 03 '16 at 11:23
  • 1
    @Yuval : After testing this in C++ I can confirm that _that is not_ the case. I called `RemovedDirectory` then `CreateDirectory` right after. Both functions succeeds and when I check the folder it has a new creation and latest modified date. – Visual Vincent Apr 03 '16 at 11:43
  • It could be due to the recursion used when deleting the files. All the files are deleted first, then the directory last. This could be the problem, @AndrewBullock, to why your directory gets deleted: your `Directory.CreateDirectory()` method seems to be called _before_ it has gotten to the point that the directory gets marked for deletion by `Directory.DeleteDirectory()` _(meaning a workaround is pretty much all you've got left, either by looping or creating your own deletion method)_. – Visual Vincent Apr 03 '16 at 11:45
  • @YuvalItzchakov : You can see my test below. :) – Visual Vincent Apr 03 '16 at 12:18

3 Answers3

2

As mentioned by others it appears that the .net Framework doesn't appear to run this synchronously. Testing it in PowerShell shows that the .Net calls do no wait so something like this would create a similar outcome:

Remove-Item -Recurse -Force "C:\tempfolder"
New-Item -ItemType Directory "C:\tempfolder"

Using a file watcher (also mentioned previously) will ensure that the directory deletion is completed before the creation is done:

var path = @"C:\tempfolder";
var watcher = new FileSystemWatcher(path);
watcher.Deleted += (sender, args) => Directory.CreateDirectory(args.FullPath);
Directory.Delete(path, true);

No real surprises there but at least it's a working solution that doesn't involve calling the C++ API's from Managed Code.

Joe Swan
  • 200
  • 5
1

Here is a link of someone with the same issue, there solution of first renaming /moving the directory may work for you.

Otherwise you could use the a FileWatcher to react to the directory deletion, but that feels like overkill.

Community
  • 1
  • 1
1

After doing some testing in C++ it seems that the native Windows functions for removing files/directories does block. It seems the problem is on the .NET side when it comes to the deletion function not being blocked, as Directory.CreateDirectory() appears to be called before Directory.Delete() is finished.

This is what I tried in a Win32 Console Application:

printf("Press enter to begin...");
while(getchar() != '\n');

LPCSTR DeletePath = "C:\\test\\DeleteMe"; //The directory to delete.
_SHFILEOPSTRUCTA* fileopt = new _SHFILEOPSTRUCTA();

fileopt->hwnd = NULL;        //No window handle.
fileopt->wFunc = FO_DELETE;  //Delete mode.
fileopt->pFrom = DeletePath; //The directory to delete.
fileopt->pTo = NULL;         //No target directory (this is only used when moving, copying, etc.).
fileopt->fFlags = FOF_NO_UI; //Display no UI dialogs.

int Success = SHFileOperationA(fileopt); //Remove the entire directory and all it's contents.
bool Success2 = CreateDirectoryA(DeletePath, NULL); //Create a new directory.

LPCSTR ReturnedValue = "False"; //I'm no C++ guru, so please don't hate. :)
LPCSTR ReturnedValue2 = "False";
if(Success == 0) { ReturnedValue = "True"; } //The SHFileOperation() returns 0 if it succeeds.
if(Success2 == true) { ReturnedValue2 = "True"; }

//Print the result of SHFileOperation().
printf("Returned value: ");
printf(ReturnedValue);
printf("\n");

//Print the result of CreateDirectory().
printf("Returned value 2: ");
printf(ReturnedValue2);
printf("\n");

//Continue.
printf("Press enter to exit...");
while(getchar() != '\n');

After pressing ENTER the first time there is a small delay before the result is shown, and when looking at the folder afterwards it's empty with a new creation and last modified date - meaning that it has been deleted and recreated in the correct order.

So in order to achieve what you want I guess you could try to create your own method which invokes SHFileOperation() instead, as the problem seems to be that the Directory.Delete() method performs the iteration itself in .NET code (see the Reference Source).


--- EDIT ---

After testing in C#, this seems to work! The only problem is that the very first time (since the application started) you call the P/Invoked SHFileOperation() function it will return a value of 2, which is equivalent to ERROR_FILE_NOT_FOUND. But if you execute it again it will return 0 (success).

NativeMethods.cs:

Imports required:

using System;
using System.Runtime.InteropServices;

Rest of the code:

[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern int SHFileOperation([In] ref SHFILEOPSTRUCT lpFileOp);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHFILEOPSTRUCT
{
    public IntPtr hwnd;
    public FileFuncFlags wFunc;

    [MarshalAs(UnmanagedType.LPWStr)]
    public string pFrom;

    [MarshalAs(UnmanagedType.LPWStr)]
    public string pTo;
    public FILEOP_FLAGS fFlags;

    [MarshalAs(UnmanagedType.Bool)]
    public bool fAnyOperationsAborted;
    public IntPtr hNameMappings;

    [MarshalAs(UnmanagedType.LPWStr)]
    public string lpszProgressTitle;
}

public enum FileFuncFlags : uint
{
    FO_MOVE = 0x1,
    FO_COPY = 0x2,
    FO_DELETE = 0x3,
    FO_RENAME = 0x4
}

[Flags]
public enum FILEOP_FLAGS : ushort
{
    FOF_MULTIDESTFILES = 0x1,
    FOF_CONFIRMMOUSE = 0x2,
    /// <summary>
    /// Don't create progress/report
    /// </summary>
    FOF_SILENT = 0x4,
    FOF_RENAMEONCOLLISION = 0x8,
    /// <summary>
    /// Don't prompt the user.
    /// </summary>
    FOF_NOCONFIRMATION = 0x10,
    /// <summary>
    /// Fill in SHFILEOPSTRUCT.hNameMappings.
    /// Must be freed using SHFreeNameMappings
    /// </summary>
    FOF_WANTMAPPINGHANDLE = 0x20,
    FOF_ALLOWUNDO = 0x40,
    /// <summary>
    /// On *.*, do only files
    /// </summary>
    FOF_FILESONLY = 0x80,
    /// <summary>
    /// Don't show names of files
    /// </summary>
    FOF_SIMPLEPROGRESS = 0x100,
    /// <summary>
    /// Don't confirm making any needed dirs
    /// </summary>
    FOF_NOCONFIRMMKDIR = 0x200,
    /// <summary>
    /// Don't put up error UI
    /// </summary>
    FOF_NOERRORUI = 0x400,
    /// <summary>
    /// Dont copy NT file Security Attributes
    /// </summary>
    FOF_NOCOPYSECURITYATTRIBS = 0x800,
    /// <summary>
    /// Don't recurse into directories.
    /// </summary>
    FOF_NORECURSION = 0x1000,
    /// <summary>
    /// Don't operate on connected elements.
    /// </summary>
    FOF_NO_CONNECTED_ELEMENTS = 0x2000,
    /// <summary>
    /// During delete operation, 
    /// warn if nuking instead of recycling (partially overrides FOF_NOCONFIRMATION)
    /// </summary>
    FOF_WANTNUKEWARNING = 0x4000,
    /// <summary>
    /// Treat reparse points as objects, not containers
    /// </summary>
    FOF_NORECURSEREPARSE = 0x8000
}

Some place else:

string DeletePath = "C:\\test\\DeleteMe";
NativeMethods.SHFILEOPSTRUCT fileopt = new NativeMethods.SHFILEOPSTRUCT();

fileopt.hwnd = IntPtr.Zero;
fileopt.wFunc = NativeMethods.FileFuncFlags.FO_DELETE;
fileopt.pFrom = DeletePath;
fileopt.pTo = null;
fileopt.fFlags = NativeMethods.FILEOP_FLAGS.FOF_SILENT | NativeMethods.FILEOP_FLAGS.FOF_NOCONFIRMATION |
                 NativeMethods.FILEOP_FLAGS.FOF_NOERRORUI | NativeMethods.FILEOP_FLAGS.FOF_NOCONFIRMMKDIR; //Equivalent of FOF_NO_UI.

int Success = NativeMethods.SHFileOperation(ref fileopt);
Directory.CreateDirectory(DeletePath);

MessageBox.Show("Operation returned value: " + Success.ToString(), "Test", MessageBoxButtons.OK, MessageBoxIcon.Information);

Hope this helps!

Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
  • @AndrewBullock : After testing the regular `IO.Directory.Delete()` and `IO.Directory.CreateDirectory()` like you did it seems that works too. I don't know what the problem could be for you, but it does create the new directory after the old one is deleted without any problem. – Visual Vincent Apr 03 '16 at 13:05
  • Alternatively, you might have to create your own C++ wrapper to perform those operations if the P/Invocation does not block properly. – Visual Vincent Apr 03 '16 at 13:06