2

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.

Kumputer
  • 588
  • 1
  • 6
  • 22
  • [What's wrong with using Thread.Abort()](https://stackoverflow.com/questions/1559255/whats-wrong-with-using-thread-abort). Non-cooperative cancellation is just not supported in .NET. Your options are either to investigate the issue and solve the actual problem that causes the indefinite hanging, or run the untrusted code on another process that you can kill at any time. – Theodor Zoulias Feb 01 '21 at 19:07
  • I suppose spinning up a whole separate process could happen, but that would be challenging to cleanly move the data we need between processes and definitely add to the code complexity that we definitely don't need right now. The actual cause is also being investigated, but that part is outside my wheelhouse, so I'm focused on what I am able to control and mitigate. – Kumputer Feb 01 '21 at 19:26
  • You may find this interesting: [The case for Thread.Abort / Thread.Suspend](https://github.com/dotnet/runtime/issues/11369) *"Thread.Abort for production scenarios is not going to come back to .NET Core. I am sorry."* jkotas commented Sep 14, 2020 – Theodor Zoulias Feb 01 '21 at 20:10
  • Hanging in FileStream is very weird and unexpected, you should investigate the real reason. Anyway you could try my RunWithAbort method, code here: https://stackoverflow.com/a/55709530/403671 – Simon Mourier Feb 01 '21 at 23:51

1 Answers1

4

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);
    }
}
Charlieface
  • 52,284
  • 6
  • 19
  • 43