Search code examples
c#asynchronousfilestream

Open file and get FileStream asynchrounously with timeout


For real world context, I'm trying to work around a somewhat rare issue in an automated process that constructs a C# FileStream like so:

using (var file = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    ...

This process goes through this line thousands of times per day, and on some rare occasions, hangs indefinitely somewhere in the FileStream constructor. I have some suspicion that the cause of the hang could be due to some users' usage of an alternate file system process that runs within Windows as a service inside a specified path, but regardless, I'd like to be able to work around whatever the issue may be by opening the FileStream asynchronously and aborting after a reasonable timeout period. I see that there is documentation for sync FileStream Read/Write, but I cannot find anything for initially acquiring the FileStream object opening the file. I have tried wrapping the FileStream open in a separate thread as well as an async task, and I am able to detect if the operation has hung, which is good, but I am unable to abort the stuck thread if this happens. In particular, Thread.Abort is no longer supported on .Net, and CancellationToken doesn't seem to work for me either. All the API's expect the running thread to terminate gracefully, but I of course have no control over what happens in .Net libraries.


Solution

  • It is a fundamental problem in the CreateFile API in Windows, that the actual file-opening is run synchronously, and has no timeout.

    There does exist CancelSynchronousIo, to use this you would need pass it an actual thread handle, ergo you need to run the file-opening on another thread, not a Task. I wouldn't really recommend this in production code. But if you wanted to go down this road, you could do as follows:

    (Some of the code is lifted from this answer, and modified for await)

    public async static Task<FileStream> OpenFileAsync(string fileName, FileMode mode, int timeout)
    // you could add more FileStream params here if you want.
    {
        FileStream stream = null;
        uint threadId = 0;
        var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var thread = new Thread(() =>    //create a thread
            {
                try
                {
                    Thread.BeginThreadAffinity();    // we need to lock onto a native thread
                    Interlocked.Exchange(ref hThread, GetCurrentThreadId());
                    Interlocked.Exchange(ref stream, new FileStream(fileName, mode));
                    completion.SetResult();
                }
                catch(Exception ex)
                {
                    completion.SetException(ex);
                }
                finally
                {
                    Thread.EndThreadAffinity();
                }
            });
        thread.Start();
    
        if(await Task.WhenAny(completion.Task, Task.Delay(timeout)) == completion.Task)   //this returns false on timeout
        {
            await completion.Task; //unwrap exception
            return Interlocked.Read(ref stream);
        }
    
        // otherwise cancel the IO and throw
        CancelIo(Interlocked.Read(ref hThread));
        Interlocked.Read(ref stream)?.Dispose();
        throw new TimeoutException();
    }
    
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern uint GetCurrentThreadId();
    
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern SafeHandle OpenThread(uint desiredAccess, bool inheritHandle, uint threadId);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr handle);
    
    [DllImport("Kernel32.dll", SetLastError = true)]
    private static extern int CancelSynchronousIo(IntPtr threadHandle);
    
    private static void CancelIo(uint threadId)
    {
        var threadHandle = IntPtr.Zero
        try
        {
            threadHandle = OpenThread(0x1, false, threadId);     // THREAD_TERMINATE
            CancelSynchronousIo(threadHandle);
        }
        finally
        {
            if(threadHandle != IntPtr.Zero)
                CloseHandle(threadHandle);
        }
    }