John Vert’s been griping about this issue to me for literally 14 years now.
I do a NET USE * \\MYSERVER\SERVERSSHARE from the CMD.EXE prompt and the console hangs. No amount of hitting CTRL-C will get it back until that silly application decides to give up.
Why on earth is this? Why can’t I just control-C and have my application stop?
It turns out that this issue comes because of a bunch of different behaviors that combine to give a less than optimal user experience.
The first is how CTRL-C is implemented in console applications. When an application calls SetConsoleCtrlHandler, then the console subsystem remembers the callback address. When the user hits CTRL-C on the console, then the console subsystem creates a brand new thread in the user’s application, and calls into the user’s specified Ctrl-C handler. If there are multiple processes in the console window (which happens when CMD.EXE launches a process), then the console subsystem calls them in the order that they were registered, and doesn’t stop until one of the handlers returns TRUE (indicating that the handler’s dealt with the signal).
If an app doesn’t call SetConsoleCtrlHandler, then CTRL-C is redirected to a handler that calls ExitProcess.
Now CMD.EXE has a CTRL-C handler, but NET.EXE (the external command executed for NET USE) doesn’t. So the system calls ExitProcess on NET.EXE when you hit CTRL-C. So far so good.
But there’s a problem. You see, the main thread of NET.EXE is blocked calling WNetAddConnection2 API. That API in turn is blocked issuing a synchronous IOCTL into the network filesystem, and the IOCTL’s blocked waiting on DNS name resolution. And since ExitProcess guarantees that the process has cleaned up before it actually removes the process, it has to wait until that IOCTL completes.
That seems silly, why doesn’t NT have a mechanism to cancel this outstanding I/O? Well it does. That’s what the CancelIo API’s all about. It takes a file handle and cancels all outstanding I/O’s for that handle
But if you look at the documentation for CancelIo carefully, it clearly says that CancelIo only cancels I/O’s that were initiated on the thread that called CanceIo. And remember – the console subsystem created a brand new thread to execute the control C handler. There aren’t any I/O’s outstanding on that thread, all of the I/O’s in the application are outstanding on the main thread of the application.
And that thread’s blocked on a synchronous IOCTL call into the network filesystem. Which won’t complete until it’s done doing its work. And that might take a while.
The really horrible thing is that there isn’t any good solution to the problem. The I/O system has really good reasons for implementing I/O cancellation the way it does, they’re deeply embedded in the design of I/O completion. The WNetAddConnection2 API is a synchronous system API (for ease of use), so it issues synchronous I/O’s to its driver. And they can’t add a console control C handler into the WNetAddConnection2 handler because if they did, it would override the intentions of the application – what if the application had indicated to the system that it NEVER wanted to be terminated by CTRL-C? If WNetAddConnection2 somehow managed to cancel its I/O when the user hit CTRL-C, then it would cause the application to malfunction.
This problem could be handled by CMD.EXE, except CMD.EXE doesn’t get control when the user hits CTRL-C, since it’s not the foreground process (NET.EXE is).
So you wait. And wait. And every time John runs into me in the hall, he asks me when I’m going to fix CTRL-C.