How to CancelSynchronousIo() on WaitForSingleObjec

2019-08-21 17:30发布

问题:

On Windows 10, I'm waiting for input from the console using

WaitForSingleObject( GetStdHandle(STD_INPUT_HANDLE), ... )

and to cancel this waiting using CancelSynchronousIo().

But the cancellation does nothing (returns 0 and GetLastError() is ERROR_NOT_FOUND).

Any idea what I could be doing wrong?
Should I be able to cancel this waiting for new input on stdin?

(I actually want to do this with any HANDLE whose GetFileType() is FILE_TYPE_CHAR, not only stdin, but stdin is certainly the most important use case and the simplest to test with).


Related discussions I've found:

  • Synchronous ReadFile() on stdin cannot be unblocked by CancelSynchronousIo()
  • win32: how stop ReadFile (stdin|pipe)

But unfortunately they only discuss ReadFile(), not WaitForSingleObject(). I've also tried WaitForMultipleObjects() (with just a single object in the array), same problem.

(Background: I'm trying to improve input handling in the GHC Haskell compiler runtime.)

回答1:

CancelSynchronousIo cancel I/O operations that are issued by the specified thread. more concrete it cancel IRP packets which associated with specified thread via call IoCancelIrp. if use undocumented NtCancelSynchronousIoFile (CancelSynchronousIo internally call it with IoRequestToCancel = 0) we can be more selective - cancel only i/o request which used specified IoRequestToCancel (system check that Irp->UserIosb == IoRequestToCancel and cancel only this requests)

but WaitForSingleObject this is not I/O request. this call not create any IRP which can be canceled. so - no way do this.

however if you use WaitForSingleObjectEx with bAlertable set to TRUE - you can break wait by queue apc to thread by using QueueUserAPC . also if use NtWaitForSingleObject instead WaitForSingleObjectEx we can also alert thread by using undocumented call NtAlertThread. in this case NtWaitForSingleObject will break with STATUS_ALERTED (note that WaitForSingleObjectEx which internally call NtWaitForSingleObject do special check for STATUS_ALERTED and in case this status - again run NtWaitForSingleObject - as result we can not break WaitForSingleObjectEx by call NtAlertThread, but NtWaitForSingleObject will be breaked.

so if you need break waiting for std input - create additional thread, which must call not CancelSynchronousIo (this senseless) but QueueUserAPC or NtAlertThread (only if you use NtWaitForSingleObject for wait). and input thread must wait in alertable state. so demo code can look like:

extern "C" NTSYSCALLAPI NTSTATUS NTAPI NtAlertThread(HANDLE ThreadHandle);

VOID NTAPI OnApc(ULONG_PTR Parameter)
{
    DbgPrint("OnApc(%p)\n", Parameter);
}

DWORD CALLBACK BreakWaitThread(HANDLE hThread)
{
    switch (LONG status = MessageBoxW(0, L"Use Apc(yes) or Alert(No) ?", L"BreakWaitThread", 
        MB_ICONQUESTION|MB_YESNOCANCEL|MB_DEFBUTTON3))
    {
    case IDYES:
        if (!QueueUserAPC(OnApc, hThread, 0))
        {
            DbgPrint("QueueUserAPC=%u\n", GetLastError());
        }
        break;
    case IDNO:
        if (0 > (status = NtAlertThread(hThread)))
        {
            DbgPrint("AlertThread=%x\n", status);
        }
        break;
    case IDCANCEL:
        DbgPrint("canceled\n");
        break;
    default:
        DbgPrint("MessageBox=%x\n", status);
    }

    CloseHandle(hThread);

    return 0; 
}

void ConsoleLoop(HANDLE hStdIn)
{
    ULONG NumberOfEvents, NumberOfEventsRead, n;

    INPUT_RECORD buf[8], *p;

    for (;;)
    {
        switch (ZwWaitForSingleObject(hStdIn, TRUE, 0))
        //switch (WaitForSingleObjectEx(hStdIn, INFINITE, TRUE))
        {
        case WAIT_OBJECT_0:

            while (GetNumberOfConsoleInputEvents(hStdIn, &NumberOfEvents) && NumberOfEvents)
            {
                do 
                {
                    NumberOfEventsRead = min(RTL_NUMBER_OF(buf), NumberOfEvents);

                    if (ReadConsoleInput(hStdIn, buf, NumberOfEventsRead, &NumberOfEventsRead) && NumberOfEventsRead)
                    {
                        n = NumberOfEventsRead;
                        p = buf;
                        do 
                        {
                            if (p->EventType == KEY_EVENT)
                            {
                                DbgPrint("%u(%u) %C %x %x %x\n", 
                                    p->Event.KeyEvent.bKeyDown,
                                    p->Event.KeyEvent.wRepeatCount,
                                    p->Event.KeyEvent.uChar.UnicodeChar,
                                    p->Event.KeyEvent.wVirtualKeyCode,
                                    p->Event.KeyEvent.wVirtualScanCode,
                                    p->Event.KeyEvent.dwControlKeyState);

                                if (VK_OEM_PERIOD == p->Event.KeyEvent.wVirtualKeyCode)
                                {
                                    return ;//if user type '.' return for demo
                                }
                            }
                        } while (p++, --n);
                    }
                    else
                    {
                        FlushConsoleInputBuffer(hStdIn);
                        break;
                    }

                } while (NumberOfEvents -= NumberOfEventsRead);
            }
            continue;

        case STATUS_USER_APC:
            DbgPrint("\nUSER_APC\n");
            return;
        case STATUS_ALERTED:
            DbgPrint("\nALERTED\n");
            return;
        case WAIT_FAILED :
            DbgPrint("\nWAIT_FAILED=%u\n", GetLastError());
            return;
        default:
            __debugbreak();
            return;
        }
    }
}

void SimpleDemo()
{
    if (HANDLE hCurrentThread = OpenThread(THREAD_ALERT|THREAD_SET_CONTEXT , FALSE, GetCurrentThreadId()))
    {
        ULONG dwThreadId;
        HANDLE hThread = CreateThread(0, 0, BreakWaitThread, hCurrentThread, 0, &dwThreadId);

        if (hThread)
        {
            ConsoleLoop(GetStdHandle(STD_INPUT_HANDLE));
            PostThreadMessage(dwThreadId, WM_QUIT, 0, 0);
            WaitForSingleObject(hThread, INFINITE);
            CloseHandle(hThread);
        }
        else
        {
            CloseHandle(hCurrentThread);
        }
    }
}


回答2:

Console I/O is difficult to use asynchronously, it is simply not designed for it. See IO Completion Ports (IOCP) and Asynchronous I/O through STDIN, STDOUT and STDERR for some possible workarounds.

If that is not an option for you, then you will have to either:

  1. use WaitForSingleObject() in a loop with a short timeout. Create a flag variable that your loop can look at on each iteration to break the loop if the flag is set.

  2. use WaitForMutipleObjects(), giving it 2 HANDLEs to wait on - one for the console (or whatever), and one for an event object from CreateEvent(). Then you can signal the event with SetEvent() when you want to break the wait. The return value of WaitForMutipleObjects() will tell you which HANDLE was signaled.