0

I am creating a directory and setting up a FileSystemWatcher on it. Then I create a file. All this is done in the form constructor. In a button event handler I delete the directory created above. Sometimes it throws a IOException: The directory is not empty. After this I cannot even access the child directory in explorer. I keep getting Access is Denied Errors. This directory gets deleted after my process exits. AFAIK FileSystemWatcher should not a lock a directory.


        string alphaBeta = @"Alpha\Beta";
        public Form1()
        {
            InitializeComponent();
            Directory.CreateDirectory(alphaBeta);
            FileSystemWatcher watcher = new FileSystemWatcher()
            {
                Path = alphaBeta,
                Filter = "*.dat",
                NotifyFilter = NotifyFilters.FileName
            };
            watcher.EnableRaisingEvents = true;
            File.WriteAllText(alphaBeta + @"\Gamma.dat", "Delta");
        }

        private void btnDelete_Click(object sender, EventArgs e)
        {
            Directory.Delete("Alpha", true);//Recursively Delete
        }

How do I properly delete the directory without getting the directory stuck?

UPDATE: Above is minimal reproducible example

My actual scenario involves a addin loaded in explorer.exe that monitors a config directory for changes. It hooks into create and rename events of FSW.

The delete code runs in the uninstaller. Uninstaller is not supposed to kill explorer. We thought FileSystemWatcher will not lock the Folder and will silently stop monitoring changes once the directory is deleted. But this doesn't seem to be the case.

AEonAX
  • 501
  • 12
  • 24
  • 2
    From my experience, the directory is most probably locked up by `FileSystemWatcher` . Please try doing a `Dispose` on the `watcher` instance before attempting to do a `Directory.Delete`. – Sau001 Oct 24 '19 at 10:11
  • 1
    I've just tested this and can reproduce the issue. It looks like this is _definitely_ the full code necessary to reproduce the problem. Maybe people should try the code out before trying to show off how clever they are. – Martin Oct 24 '19 at 10:12
  • @Martin maybe you should think about what that code does - it creates an orphaned FileSystemWatcher. How does that behave? Lock the folder perhaps? Thus preventing deletions? So you have to stop it, but you can't - it's not stored *anywhere*. It will keep its locks until it's GCd – Panagiotis Kanavos Oct 24 '19 at 10:12
  • @PanagiotisKanavos Maybe this is the necessary example - the _minimum code required to reproduce the fault_ as is needed for any question on SO. – Martin Oct 24 '19 at 10:13
  • @Martin that's not enough - the code has bugs already, that may or may not cause the problem - in this case they even prevent fixing the issue – Panagiotis Kanavos Oct 24 '19 at 10:14
  • @PanagiotisKanavos Which bugs does this program have? The OP has clearly demonstrated the issue in their code and asked why the issue is occurring. The solution then becomes obvious: the `FileSystemWatcher` must be disposed before the resources it references can be deleted. – Martin Oct 24 '19 at 10:15
  • @AEonAX what are you trying to do? This code won't watch for any file events. The code is trying to delete the *folder* that's being watched, and its parent as well, which causes the error. Are you trying to monitor *file* deletions perhaps? Or perhaps you should monitor the *parent* folder? – Panagiotis Kanavos Oct 24 '19 at 10:15
  • 1
    This happens because under the hood `FileSystemWatcher` calls the `ReadDirectoryChangesW()` Windows API function, which itself must be passed a handle to a directory that was opened via a call to the `CreateFileA()` Windows API function. The handle to the directory will be kept open until the `FileSystemWatcher` is disposed, thus preventing the directory from being deleted (since it will be "in use"). – Matthew Watson Oct 24 '19 at 10:25
  • @PanagiotisKanavos thats the minimal code required to reproduce the issue. My Actual scenario is I have a application that monitors a config directory for changes. And a Uninstaller that tries to delete the config directory. Due to some reasons the Application should not be closed during installation – AEonAX Oct 24 '19 at 13:10
  • @AEonAX you should ask about those reasons in that case. It's quite likely the problem is already solved - ClickOnce *and* .NET Core single-file exes for example store different versions' files in different directories, under app-specific folders in `c:\ProgramData`, a well-known location. So do UWP applications, although they use a different path. It's far easier to roll back changes in case of error when you isolate the changes too. – Panagiotis Kanavos Oct 24 '19 at 13:15
  • @AEonAX .NET Old's user-specific settings are saved in different files per version too, with a migration step each time a version gets upgraded. You could do the same and change the FSW's target path once installation is complete – Panagiotis Kanavos Oct 24 '19 at 13:16
  • @AEonAX even better- .NET Core's config system is actually a .NET Standard 2.0 package, which means you can use it even in .NET 4.x apps - file watchers and all. Instead of creating your own config system with change detection, you can "borrow" a standard one and keep using the same code when you upgrade to Core. I already use Microsoft.Extensions.Configuration in .NET 4.x projects for this reason – Panagiotis Kanavos Oct 24 '19 at 13:18
  • @AEonAX you can use one "main" settings file that points to the other's locations. Once you install the new files in a new folder, you can use `File.Replace()` to swap the files in a single operation, forcing the main file to reload the locations and the new files. Deleting old settings locations could run from the installer or the config code itself, once it loads the new config – Panagiotis Kanavos Oct 24 '19 at 13:22
  • @PanagiotisKanavos we already have a config management system. we currently cannot migrate to .NET configs implementation. But as it looks from the answers and the comments, it looks like we will have to disable the FSWs first then delete the directories. – AEonAX Oct 24 '19 at 13:23
  • The config Folder contains many files/folders/templates and one specfic FSW monitored folder – AEonAX Oct 24 '19 at 13:24
  • @AEonAX I'd still use different folders per version, just for peace of mind and the assurance that even if power goes down, or my code crashes in the middle of the updates (far more likely), the app can recover – Panagiotis Kanavos Oct 24 '19 at 13:29

2 Answers2

3

The problem is caused because the FSW keeps a handle to the directory open as long as events are enabled. The source code shows that disabling events also closes the handle. To delete the directory, the FSW must be disabled.

The second problem though is that FileSystemWatcher watcher is defined as a variable inside the Form1() constructor, which means it's orphaned and immediatelly available for garbage collection before the form is even displayed. The garbage collector runs infrequently though, which means there's no guarantee it will run before Delete is clicked. Since the watcher isn't stored anywhere, iIt's no longer possible to disable it.

At the very least, the FSW needs to be stored in a field and events disabled before deletion. We should also ensure the watcher is disposed when the form itself gets disposed :

public class Form1
{
    FileSystemWatcher _watcher;

    public Form1()
    {
        ...
        _watcher=CreateDormantWatcher(path,pattern);
        _watcher.EnableRaisingEvents=true ;
    }

    private void btnDelete_Click(object sender, EventArgs e)
    {
        _watcher.EnableRaisingEvents =false;
        Directory.Delete("Alpha", true);//Recursively Delete
    }

    protected override void Dispose (bool disposing)
    {
        base.Dispose(disposing);
        if (disposing)
        {
             _watcher.Dispose();
        }
        _watcher=watcher;
    }

    FileSystemWatcher CreateDormantWatcher(string path,string pattern)
    {
        //Don't store to the field until the FSW is 
        //already configured
        var watcher=new FileSystemWatcher()
        {
            Path = path,
            Filter = "pattern,
            NotifyFilter = NotifyFilters.FileName
        };
        watcher.Changed += OnChanged;
        watcher.Created += OnCreated;
        watcher.Deleted += OnChanged;
        watcher.Renamed += OnRenamed;
        return watcher;
    }

Easy fix: Add it as a component

Perhaps a better idea though, would be to add the FileSystemWatcher on the form as a component. FileSystemWatcher inherits from Component, which means that placing it on the form add its creation and configuration code in InitializeComponents(). It will also get disposed when all other components get disposed.

If we do that, we'd just have to toggle EnableRaisingEvents when appropriate.

Assuming the path and pattern are set as properties, and the component's name is the imaginative FileSystemWatcher1 this brings the code down to :

    public Form1()
    {
        InitializeComponent();
        Directory.CreateDirectory(FileSystemWatcher1.Path);
        FileSystemWatcher1.EnableRaisingEvents = true;
        File.WriteAllText(alphaBeta + @"\Gamma.dat", "Delta");
    }

    private void btnDelete_Click(object sender, EventArgs e)
    {
        FileSystemWatcher1.EnableRaisingEvents = false;
        Directory.Delete("Alpha", true);//Recursively Delete
    }
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
0

This appears to be an issue with the FileSystemWatcher. If the code for this is commented out then the Exception does not occur.

It would appear that the FileSystemWatcher is not necessarily disposed before the Directory.Delete method is called.

It would be appropriate to handle the FileSystemWatcher in a way that causes it to be disposed before deleting resources that it may have depended upon, but in your example code it's easily achieved by simply adding watcher.Dispose();:

string alphaBeta = @"Alpha\Beta";
public Form1()
{
    InitializeComponent();
    Directory.CreateDirectory(alphaBeta);
    FileSystemWatcher watcher = new FileSystemWatcher()
    {
        Path = alphaBeta,
        Filter = "*.dat",
        NotifyFilter = NotifyFilters.FileName
    };
    watcher.EnableRaisingEvents = true;
    watcher.Dispose();
    File.WriteAllText(alphaBeta + @"\Gamma.dat", "Delta");
}

private void button1_Click(object sender, EventArgs e)
{

    Directory.Delete("Alpha", true);//Recursively Delete
}
Martin
  • 16,093
  • 1
  • 29
  • 48
  • Or removing it altogether as it does *nothing* useful – Panagiotis Kanavos Oct 24 '19 at 10:15
  • If disposed immediately, then would the notification mechanism still work? – Sau001 Oct 24 '19 at 10:16
  • 1
    @PanagiotisKanavos In this _context_. This code is the OP's example of the problem, _not necessarily their actual production code_. – Martin Oct 24 '19 at 10:16
  • @Sau001 the code doesn't handle any notifications anyway. Whatever the OP's intent is, this code can't do it – Panagiotis Kanavos Oct 24 '19 at 10:17
  • @Sau001 No it would not work if Disposed immediately – Martin Oct 24 '19 at 10:17
  • It is not an FSW issue, it is the way the operating system works. It allows programs opening a handle to a directory, just like it does for a file. All handles must be closed before the directory can be removed. Another common case of this is Environment.CurrentDirectory, the default working directory for a process. More intuitive perhaps that jerking that floor mat for a process can't be allowed to work. Anti-malware and search indexers cause plenty of trouble as well. Moving the directory to the recycle bin is a decent alternative. – Hans Passant Oct 24 '19 at 10:39
  • @HansPassant I'm not sure whether to agree or not with your first statement. If the FSW is now out of scope then would it be reasonable to assume that it would still have an open handle? At the very best it's unclear because the handle may remain open until the FSW has been Disposed – Martin Oct 24 '19 at 10:42
  • @Martin Yes, until the Garbage collector has a chance to collect it. The *variable* is out of scope, not the reference type instance stored in the variable. The instances will be GD'd and disposed when the garbage collector runs and finds out they're orphaned. This isn't a C# peculiarity, that's how all garbage collected languages work, including Java and Go. – Panagiotis Kanavos Oct 24 '19 at 10:45
  • @PanagiotisKanavos Hence my statement `is now out of scope`. The FSW is awaiting GC. If the GC happened to execute before the `Delete` then this situation would not occur. – Martin Oct 24 '19 at 10:46
  • And the GC runs infrequently, typically only when there's memory pressure, because that's an expensive operation. Besides, *event handlers* count against garbage collection. The OP's code doesn't show any event handlers though, so we can only assume that either the GC didn't run or the OP simplified the example – Panagiotis Kanavos Oct 24 '19 at 10:48
  • 1
    @PanagiotisKanavos The OP has clearly simplified their example as the FSW otherwise would serve no purpose. However, the question remained: _Does the FSW stop me from deleting the directory?_. The answer is, simply, _Yes_ – Martin Oct 24 '19 at 10:50