Async wait for file to be created

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!!!");
}

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)));
    }
}

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.