Async ожидает создания файла

Каким будет самый чистый способ await для файла, который будет создан внешним приложением?

    async Task doSomethingWithFile(string filepath)
    {
        // 1. await for path exists
        // 2. Do something with file
    }

Ответ 1

Итак, первым ключевым моментом является то, что вы можете использовать FileSystemWatcher для уведомления, когда событие файловой системы изменяется по определенному пути. Если вы, например, хотите получать уведомления, когда файл создается в определенном месте, вы можете узнать.

Далее мы можем создать метод, который использует TaskCompletionSource для запуска завершения задачи, когда наблюдатель файловой системы запускает соответствующее событие.

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

Обратите внимание, что это сначала проверяет, существует ли файл, чтобы он мог немедленно выйти, если это применимо. Он также использует как созданные, так и переименованные обработчики, поскольку любой вариант может позволить файлу существовать в какой-то момент в будущем. FileSystemWatcher также отслеживает только каталоги, поэтому важно получить каталог указанного пути, а затем проверить имя файла каждого затронутого файла в обработчике событий.

Также обратите внимание, что код удаляет обработчики событий, когда это делается.

Это позволяет нам написать:

public static async Task Foo()
{
    await WhenFileCreated(@"C:\Temp\test.txt");
    Console.WriteLine("It aliiiiiive!!!");
}

Ответ 2

Полное решение с использованием пользовательского оператора ReactiveExtension: WaitIf. Для этого требуется Genesis.RetryWithBackoff, доступный через 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)));
    }
}

Ответ 3

Это более многофункциональная версия решения Servy. Это позволяет наблюдать за определенными состояниями и событиями файловой системы, чтобы охватить различные сценарии. Он также может быть отменен как по таймауту, так и по 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();
}

Пример использования:

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

В этом примере результатом обычно будет WatchFileType.Exists или WatchFileType.Created. В исключительном случае, когда файл не существует и не создается в течение 5000 миллисекунд, будет TaskCanceledException.

Сценарии
WatchFileType.Exists | WatchFileType.Created WatchFileType.Exists | WatchFileType.Created: для файла, который создается за один раз.
WatchFileType.ExistsNotEmpty | WatchFileType.Changed WatchFileType.ExistsNotEmpty | WatchFileType.Changed: для файла, который сначала создается пустым, а затем заполняется данными.
WatchFileType.NotExists | WatchFileType.Deleted WatchFileType.NotExists | WatchFileType.Deleted: для файла, который собирается удалить.

Ответ 4

Вот как я это сделаю:

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

Вы также можете упаковать его в метод:

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

а затем просто используйте его как:

await WaitForFileAsync("yourpath.extension");
//do all the processing