19

What would be the cleanest way to await for a file to be created by an external application?

    async Task doSomethingWithFile(string filepath)
    {
        // 1. await for path exists
        // 2. Do something with file
    }
mishik
  • 9,973
  • 9
  • 45
  • 67
Liel
  • 2,407
  • 4
  • 20
  • 39

4 Answers4

25

So the first key point is that you can use a FileSystemWatcher to be notified when a file system event changes at a particular path. If you, for example, want to be notified when a file is created at a particular location you can find out.

Next, we can create a method that uses a TaskCompletionSource to trigger the completion of a task when the file system watcher triggers the relevant event.

public static Task WhenFileCreated(string path)
{
    if (File.Exists(path))
        return Task.FromResult(true);

    var tcs = new TaskCompletionSource<bool>();
    FileSystemWatcher watcher = new FileSystemWatcher(Path.GetDirectoryName(path));

    FileSystemEventHandler createdHandler = null;
    RenamedEventHandler renamedHandler = null;
    createdHandler = (s, e) =>
    {
        if (e.Name == Path.GetFileName(path))
        {
            tcs.TrySetResult(true);
            watcher.Created -= createdHandler;
            watcher.Dispose();
        }
    };

    renamedHandler = (s, e) =>
    {
        if (e.Name == Path.GetFileName(path))
        {
            tcs.TrySetResult(true);
            watcher.Renamed -= renamedHandler;
            watcher.Dispose();
        }
    };

    watcher.Created += createdHandler;
    watcher.Renamed += renamedHandler;

    watcher.EnableRaisingEvents = true;

    return tcs.Task;
}

Note that this first checks if the file exists, to allow it to exit right away if applicable. It also uses both the created and renamed handlers as either option could allow the file to exist at some point in the future. The FileSystemWatcher also only watches directories, so it's important to get the directory of the specified path and then check the filename of each affected file in the event handler.

Also note that the code removes the event handlers when it's done.

This allows us to write:

public static async Task Foo()
{
    await WhenFileCreated(@"C:\Temp\test.txt");
    Console.WriteLine("It's aliiiiiive!!!");
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • 1
    @AlexS No. You could check for that at the start of the method to handle that case though with just one line of code. – Servy Jul 01 '13 at 16:06
  • 2
    What if a filename's edited? Also, he wants to wait for a specific file name. – It'sNotALie. Jul 01 '13 at 16:13
  • 1
    `FileSystemWatcher` + task pattern = the way to go, +1 – ken2k Jul 01 '13 at 16:21
  • 4
    Quite a few problems with this: `fsw` is never disposed, it will error if the file is deleted and then is made again, he passes the file name and not the directory path... -1. – It'sNotALie. Jul 01 '13 at 16:26
  • 2
    Thanks for fixing it. Just the small issue: still not disposed. – It'sNotALie. Jul 01 '13 at 16:28
  • 1
    @newStackExchangeInstance You didn't refresh the page before commenting. – Servy Jul 01 '13 at 16:34
  • 3
    Doesn't this have a race condition? If the file is created between the `File.Exists()` check and `EnableRaisingEvents = true`, then the `Task` will never complete, I think. – svick Jul 01 '13 at 21:34
  • 1
    @svick Yeah, that does seem to be a possibility. – Servy Jul 02 '13 at 06:06
  • 2
    Be aware that the FileSystemWatcher will raise events on a different thread than where you came from, so you may lose your context. See http://www.roufa.com/articles/async-await-through-the-looking-glass/ for more details. – roufamatic Dec 17 '13 at 20:09
  • 2
    @roufamatic Sure, but if you're in a UI context where it's important to marshal to a given context `await` will handle it for you, in this case, and if you're not, say because it's a console app, then there's no problem with being in another thread. Either way it shouldn't be a problem. – Servy Dec 17 '13 at 20:11
  • 1
    @servy I posted that because I had an issue with HttpContext.Current being null in an MVC action after the `await`. My guess was because marshalling wasn't possible due to the mechanics of the FSW -- do you believe something else was at play? – roufamatic Dec 17 '13 at 23:30
  • 1
    @roufamatic It's hard to say without knowing the details. If you had moved the entire async method out of the sync context then it doesn't have a context to marshal back to, for example, but there are other possibilities. You can always ask a new question about it if you really want to know. – Servy Dec 18 '13 at 13:50
  • 1
    Return type of the `WhenFileCreated` shall be `Task` instead of plain vanilla `Task` – Mrinal Kamboj Jan 18 '17 at 01:05
  • 1
    @MrinalKamboj No, it should not. The `bool` value isn't meaningful. It'll always be `true`, so there's no reason for a caller to ever look at it. – Servy Jan 18 '17 at 14:10
  • 1
    @It'sNotALie. take a look at my answer: retry if locked, custom delay and dispose – frhack May 11 '19 at 16:17
  • 1
    Warning: FileSystemWatcher [is not reliable](https://stackoverflow.com/questions/21000831/filesystemwatcher-pitfalls) is not exactly reliable. See [FileSystemWatcher vs. Polling](https://stackoverflow.com/questions/239988/filesystemwatcher-vs-polling-to-watch-for-file-changes). – John Wu May 14 '19 at 02:53
  • 1
    @JohnWu Good thing this is specifically looking for new file creation, rather than any changes, and isn't doing any heavy processing int he handler, so neither of those problems exist in this case. – Servy May 14 '19 at 13:16
1

A complete solution using a custom ReactiveExtension operator: WaitIf. This requires Genesis.RetryWithBackoff available via NuGet

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;


public class TestWatcher
{

    public static void Test()
    {

        FileSystemWatcher Watcher = new FileSystemWatcher("C:\\test")
        {
            EnableRaisingEvents = true,
        };

        var Created = Observable
            .FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(h => Watcher.Created += h, h => Watcher.Created -= h)
            .Select(e => e.EventArgs.FullPath);
        var CreatedAndNotLocked = Created.WaitIf(IsFileLocked,100, attempt =>TimeSpan.FromMilliseconds(100), Scheduler.Default);
        var FirstCreatedAndNotLocked = CreatedAndNotLocked.Take(1)
            .Finally(Watcher.Dispose);
        var task = FirstCreatedAndNotLocked.GetAwaiter().ToTask();
        task.Wait();
        Console.WriteLine(task.Result);

    }

    public bool IsFileLocked(string filePath)
    {
        var ret = false;
        try
        {
            using (File.Open(filePath, FileMode.Open)) { }
        }
        catch (IOException e)
        {
            var errorCode = Marshal.GetHRForException(e) & ((1 << 16) - 1);
            ret = errorCode == 32 || errorCode == 33;
        }
        return ret;
    }
}



public static class ObservableExtensions
{


    public class NotReadyException : Exception
    {
        public NotReadyException (string message) : base(message)
        {
        }
    }

    public static IObservable<T> WaitIf<T>(
      this IObservable<T> @this,
      Func<T, bool> predicate,
      int? retryCount = null,
      Func<int, TimeSpan> strategy = null,
      Func<Exception, bool> retryOnError = null,
      IScheduler scheduler = null)
    {
        scheduler = scheduler ?? DefaultScheduler.Instance;
        return @this.SelectMany(f =>
        Observable.Defer(() =>
           Observable.FromAsync<bool>(() => Task.Run<bool>(() => predicate.Invoke(f)),scheduler)
           .SelectMany(b => b ? Observable.Throw<T>(new NotReadyException(f + " not ready")) :
                           Observable.Return(f)
        ).RetryWithBackoff(retryCount, strategy, retryOnError, scheduler)));
    }
}
frhack
  • 4,862
  • 2
  • 28
  • 25
1

This is a more feature-rich version of Servy's solution. It permits watching for specific file-system states and events, to cover different scenarios. It is also cancellable by both a timeout and a CancellationToken.

[Flags]
public enum WatchFileType
{
    Created = 1,
    Deleted = 2,
    Changed = 4,
    Renamed = 8,
    Exists = 16,
    ExistsNotEmpty = 32,
    NotExists = 64,
}

public static Task<WatchFileType> WatchFile(string filePath,
    WatchFileType watchTypes,
    int timeout = Timeout.Infinite,
    CancellationToken cancellationToken = default)
{
    var tcs = new TaskCompletionSource<WatchFileType>();
    var fileName = Path.GetFileName(filePath);
    var folderPath = Path.GetDirectoryName(filePath);
    var fsw = new FileSystemWatcher(folderPath);
    fsw.Filter = fileName;

    if (watchTypes.HasFlag(WatchFileType.Created)) fsw.Created += Handler;
    if (watchTypes.HasFlag(WatchFileType.Deleted)) fsw.Deleted += Handler;
    if (watchTypes.HasFlag(WatchFileType.Changed)) fsw.Changed += Handler;
    if (watchTypes.HasFlag(WatchFileType.Renamed)) fsw.Renamed += Handler;

    void Handler(object sender, FileSystemEventArgs e)
    {
        WatchFileType result;
        switch (e.ChangeType)
        {
            case WatcherChangeTypes.Created: result = WatchFileType.Created; break;
            case WatcherChangeTypes.Deleted: result = WatchFileType.Deleted; break;
            case WatcherChangeTypes.Changed: result = WatchFileType.Changed; break;
            case WatcherChangeTypes.Renamed: result = WatchFileType.Renamed; break;
            default: throw new NotImplementedException(e.ChangeType.ToString());
        }
        fsw.Dispose();
        tcs.TrySetResult(result);
    }

    fsw.Error += (object sender, ErrorEventArgs e) =>
    {
        fsw.Dispose();
        tcs.TrySetException(e.GetException());
    };

    CancellationTokenRegistration cancellationTokenReg = default;

    fsw.Disposed += (object sender, EventArgs e) =>
    {
        cancellationTokenReg.Dispose();
    };

    fsw.EnableRaisingEvents = true;

    var fileInfo = new FileInfo(filePath);
    if (watchTypes.HasFlag(WatchFileType.Exists) && fileInfo.Exists)
    {
        fsw.Dispose();
        tcs.TrySetResult(WatchFileType.Exists);
    }
    if (watchTypes.HasFlag(WatchFileType.ExistsNotEmpty)
        && fileInfo.Exists && fileInfo.Length > 0)
    {
        fsw.Dispose();
        tcs.TrySetResult(WatchFileType.ExistsNotEmpty);
    }
    if (watchTypes.HasFlag(WatchFileType.NotExists) && !fileInfo.Exists)
    {
        fsw.Dispose();
        tcs.TrySetResult(WatchFileType.NotExists);
    }

    if (cancellationToken.CanBeCanceled)
    {
        cancellationTokenReg = cancellationToken.Register(() =>
        {
            fsw.Dispose();
            tcs.TrySetCanceled(cancellationToken);
        });
    }

    if (tcs.Task.IsCompleted || timeout == Timeout.Infinite)
    {
        return tcs.Task;
    }

    // Handle timeout
    var cts = new CancellationTokenSource();
    var delayTask = Task.Delay(timeout, cts.Token);
    return Task.WhenAny(tcs.Task, delayTask).ContinueWith(_ =>
    {
        cts.Cancel();
        if (tcs.Task.IsCompleted) return tcs.Task;
        fsw.Dispose();
        return Task.FromCanceled<WatchFileType>(cts.Token);
    }, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}

Usage example:

var result = await WatchFile(@"..\..\_Test.txt",
    WatchFileType.Exists | WatchFileType.Created, 5000);

In this example the result will normally be either WatchFileType.Exists or WatchFileType.Created. In the exceptional case where the file does not exist and is not created for 5000 milliseconds, a TaskCanceledException will be thrown.

Scenarios
WatchFileType.Exists | WatchFileType.Created: for a file that is created in one go.
WatchFileType.ExistsNotEmpty | WatchFileType.Changed: for a file that is first created empty and then filled with data.
WatchFileType.NotExists | WatchFileType.Deleted: for a file that is about to be deleted.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
-3

This is how I'd do it:

await Task.Run(() => {while(!File.Exists(@"yourpath.extension")){} return;});
//do all the processing

You could also package it into a method:

public static Task WaitForFileAsync(string path)
{
    if (File.Exists(path)) return Task.FromResult<object>(null);
    var tcs = new TaskCompletionSource<object>();
    FileSystemWatcher watcher = new FileSystemWatcher(Path.GetDirectoryName(path));
    watcher.Created += (s, e) => 
    { 
        if (e.FullPath.Equals(path))
        { 
            tcs.TrySetResult(null);
            if (watcher != null)
            {
                watcher.EnableRaisingEvents = false;
                watcher.Dispose();
            }
        } 
    };
    watcher.Renamed += (s, e) =>
    {
        if (e.FullPath.Equals(path))
        {
            tcs.TrySetResult(null);
            if (watcher != null)
            {
                watcher.EnableRaisingEvents = false;
                watcher.Dispose();
            }
        }
    };
    watcher.EnableRaisingEvents = true;
    return tcs.Task;
}

and then just use it as this:

await WaitForFileAsync("yourpath.extension");
//do all the processing
It'sNotALie.
  • 22,289
  • 12
  • 68
  • 103
  • 1
    @StephenCleary Well, there isn't a FileExistsAsync is there? `FileSystemWatcher` can give you the wrong path name, and doesn't always cover renames or if the file exists. This is probably the best way. – It'sNotALie. Jul 01 '13 at 16:16