Каким будет самый чистый способ await
для файла, который будет создан внешним приложением?
async Task doSomethingWithFile(string filepath)
{
// 1. await for path exists
// 2. Do something with file
}
Каким будет самый чистый способ await
для файла, который будет создан внешним приложением?
async Task doSomethingWithFile(string filepath)
{
// 1. await for path exists
// 2. Do something with file
}
Итак, первым ключевым моментом является то, что вы можете использовать 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!!!");
}
Полное решение с использованием пользовательского оператора 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)));
}
}
Это более многофункциональная версия решения 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
: для файла, который собирается удалить.
Вот как я это сделаю:
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