CancelIoEx can cancel I/O on console input, which is kind of nice


Today's Little Program asks you to type something, but gives you only two seconds to do it. This is not interesting in and of itself, but it shows you how to cancel console I/O. There is no motivation for this exercise because Little Programs come with little to no motivation.

Okay, fine, here's the motivation.

We have a GUI application that has a debug console. When the user exits the application, we cannot shut down cleanly because the debug console is stuck on a read from stdin. We want to unstick the thread gently. We don't want to use Generate­Console­Ctrl­Event with CTRL_C_EVENT because that will send the event to all processes using the same console, but we don't want other processes to be affected.

Okay, now our Little Program.

#include <windows.h>
#include <stdio.h> // horrors! mixing C and C++!

DWORD CALLBACK ThreadProc(void *)
{
 Sleep(2000);
 CancelIoEx(GetStdHandle(STD_INPUT_HANDLE), nullptr);
 return 0;
}

int __cdecl wmain(int, wchar_t **)
{
 DWORD scratch;
 HANDLE h = CreateThread(nullptr, 0, ThreadProc,
                         nullptr, 0, &scratch);
 if (h) {
  printf("type something\n");
  char buffer[80];
  if (fgets(buffer, 80, stdin) != nullptr) {
   printf("you typed %s", buffer);
  } else if (feof(stdin)) {
   printf("end-of-file reached\n");
  } else if (ferror(stdin)) {
   printf("error occurred\n");
  }
 }
 return 0;
}

If you type something within two seconds, it is reported back to you, but if you take too long, then the Cancel­Io­Ex cancels the console read, and you get an error back.

If you want to continue, you'll have to clearerr(stdin), but if you just want to unstick the code that is performing the read (so that you can get the program to exit cleanly), then leaving stdin in an error state is probably better.

(If you had used Read­File instead of fgets, the read would have failed with error code ERROR_OPERATION_ABORTED, as documented by Cancel­Io­Ex.)

Comments (24)
  1. Medinoc says:

    Thanks! This will allow me to get rid of my ugly hack of working on a duplicated handle and closing it from a thread.

    Yuhong Bao mode: This wouldn't be a problem if console handles with ENABLE_LINE_INPUT would wait for a full line before becoming signaled!

  2. Boris says:

    I'm not getting any interruptions. However long I take, the result is "you typed <whatever I typed>".

    (I've never done professional Win32 programming, but I used VS 2012 to open a Win32 Console Application project, pasted your code as-is with the additional include of "stdafx.h" at the top, compiled the app without any issues and ran it from cmd.exe.)

  3. mark says:

    Is CancelIO guaranteed to work on console handles or is this an implementation detail? What kernel handle type are console handles anyway? And has this changed over time?

  4. DWalker says:

    If "Little Programs" don't need motivation, then you don't need to supply motivation!  :-)  Programming techniques are often interesting in themselves, of course, or none of us would be here.

  5. Stefan Kanthak says:

    The REAL horror is but mixing the Win32 API with the C runtime!

    Is the equivalence of the stream handle "stdin" provided by the C runtime and the handle returned from GetStdHandle(STD_INPUT_HANDLE) part of the contract?

  6. Joshua says:

    @Medinoc: Good hack to get rid of. Having tried it, I can say it can hang if you have the single processor NTOSKRNL.EXE.

    @Stefan Kanthak: Yes.

  7. This doesn't work when using MSVC 12 to compile the program under Windows 7, the call to CancelIoEx() seems to be just ignored. Interestingly, it does work if input is redirected and doesn't really come from console.

  8. Rob says:

    @mark: Console handles are file handles.  msdn.microsoft.com/.../ms724485(v=vs.85).aspx

  9. Myria says:

    @Rob: They're not.  They're values that are recognized by kernel32.dll APIs and special-cased.  Consoles are actually implemented using LPCs over NT port objects.

    Raymond, did this change in more-recent Windows versions than Windows 7?

  10. Stefan Kanthak says:

    @Joshua: Really? Where?

    <msdn.microsoft.com/.../c565h7xx.aspx> only says "These streams are directed to the console (keyboard and screen) by default.": what does "directed to" mean is but left open.

    There is NO explicit statement that (the implementation of) FILE* stdin etc. use the handle returned from GetStdHandle(STD_INPUT_HANDLE) etc.

    FILE *stdin etc. might as well use handles returned from CreateFile(TEXT("CONIN$"), ...) etc.

  11. qb says:

    I think Stefan Kanthak is right and some versions of MSVC runtime could open their std handles directly instead of using GetStdHandle, and as a consequence this trick doesn't work. Same for MinGW runtime that comes with 32-bit GCC 4.8.1.

    Replacing GetStdHandle(STD_INPUT_HANDLE) with _get_osfhandle(stdin) should probably work, but I cannot test it at the moment.

  12. curious says:

    @boris. So assuming you've decided to debug the program,  What did your error checking code tell you?

  13. Wizou says:

    Yup, it doesn't seem to work under Windows 7 SP1, unfortunately :(

    I'm using VS2010 right now, and I tried using ReadFile as well (so it is not related to MSVC runtime)

  14. Rick C says:

    Odd, because it worked fine for me, Win 8.1.  But I didn't make a project, I just pasted it into a Notepad window, saved it as a .cpp file, and then ran cl from the command line.

  15. I'm wondering if the people who are saying it isn't working are running the program via Visual Studio's built-in console window as opposed to an actual command shell.  There may be a semantical difference in the kind of input handle being passed into the program from Visual Studio.

  16. JamesJohnston says:

    The C runtime's stdin/stdout files are just wrappers around GetStdHandle.  I've observed this myself in the past using an API tracing tool.  Why the big controversy?  What other stdin could the C runtime possibly be using if not the one from GetStdHandle?  ("MSVC runtime could open their std handles directly instead of using GetStdHandle" ---> what does "directly" mean, in Win32 API terms?)

    That said, if I remember from my tracing, the handles are often duplicated using DuplicateHandle.  But... "The duplicate handle refers to the same object as the original handle. Therefore, any changes to the object are reflected through both handles."

    Of course one should not assume "stdin == GetStdHandle(STD_INPUT_HANDLE)" i.e. that the returned pointers/handles compare identical.  stdin is a "FILE*" and GetStdHandle is a HANDLE.  Presumably, the "stdin" could be the return value from GetStdHandle but with additional runtime-specific information attached to it.  But, both values ultimately refer to the same object...  Raymond didn't mix anything up... the HANDLE is passed to Win32 API and the FILE* is passed to the CRT.

  17. Medinoc says:

    The thing that *should* compare identical (assuming you don't fiddle with the handles first would be this test:

    _get_osfhandle(_fileno(stdin)) == GetStdHandle(STD_INPUT_HANDLE)

  18. John Doe says:

    Once again, Raymond's blog serves as a proper extension of MSDN.  Nowhere could you figure out that CancelIoEx works on consoles.

  19. Wizou says:

    @MNGoldenEagle: I tested under W7 SP1 with gets & ReadFile, in Debug & Release, inside & outside Visual Studio 2010, and even with a simple "cl" command line build.

    That's too bad because Raymond's trick would have been a neat solution to the problem I have with the project I'm currently working on.

  20. Myria says:

    Yeah, it looks like Raymond's information is specific to Windows 8 (or 8.1?) and later.  As I said earlier, console handles, at least apparently in Windows 7 and older, are fake handles that are special-cased by various APIs in kernel32.dll.  WriteFile, etc. recognize console handles and translate them to NT port (LPC) calls.  They're not true kernel handles.  It looks like that changed in Windows 8.

    @Someone: For technical correctness, use %lu instead of %u for GetLastError(), since DWORD is a typedef for "unsigned long".  It's technically undefined behavior to use %u...not that it would ever fail with Visual C++.

  21. Someone says:

    Under Win7, CancelIoEx() fails with ERROR_INVALID_HANDLE. (Even Little Programs should at least check the return code of every function call.)

    I tried with this (and cl.exe at command line):

    #include <windows.h>

    #include <stdio.h>

    DWORD CALLBACK ThreadProc(void *)

    {

     Sleep(2000);

     if (GetStdHandle(STD_INPUT_HANDLE) == INVALID_HANDLE_VALUE) {

    fprintf(stderr, "GetStdHandle => %un", GetLastError());

    return 1;

     }

     if (!CancelIoEx(GetStdHandle(STD_INPUT_HANDLE), NULL)) {

    fprintf(stderr, "CancelioEx => %un", GetLastError());

    return 1;

     }

     return 0;

    }

    int __cdecl wmain(int, wchar_t **)

    {

     DWORD scratch;

     HANDLE h = CreateThread(NULL, 0, ThreadProc, NULL, 0, &scratch);

     if (!h) {

    fprintf(stderr, "CreateThread => %un", GetLastError());

    return 1;

     }

     DWORD bytes;

     char buffer[80];

     if (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, 80, &bytes, NULL)) {

       buffer[bytes] = '';

       printf("you typed %sn", buffer);

     } else {

       printf("ReadFile => %un", GetLastError());

     }

     return 0;

    }

  22. dirk.gently says:

    Could you write a Little Program that implements blocking keyboard reads (say, like Console.ReadKey()) but on a Windows Form?

    [I can't make any sense of that question. Windows Forms are not console apps. -Raymond]
  23. JamesJohnston says:

    @Someone:  Well, that's too bad.  I tried your program and got the same result of CancelIoEx returning ERROR_INVALID_HANDLE.  I also tried calling DuplicateHandle to duplicate the console input handle (i.e. so I wasn't using the constant value returned by GetStdHandle).  DuplicateHandle succeeds but still I get the same error result from CancelIoEx.  I'm on Win 7 SP1.

    Must be an improvement in Windows 8 or 8.1...

  24. You Did It Again says:

    I knew if I waited long enough, you would get around to solving this problem for me.  I was so tired of having to type at least one char and pressing ENTER before it would get out of reading stdin.  Thanks!!

Comments are closed.

Skip to main content