What would be the cleanest way to await
for a file to be created by an external application?
async Task doSomethingWithFile(string filepath)
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 WatchFile(string filePath,
WatchFileType watchTypes,
int timeout = Timeout.Infinite,
CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource();
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(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.