Why doesn’t CTRL-C stop NET USE?

 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.



Comments (4)

  1. Anonymous says:

    The problem really is that WNetAddConnection2 is a synchronous call that cannot be canceled. Ctrl+C handling is just a side effect.

  2. Anonymous says:

    You’re right, but it was just an example. The comments I made above apply to EVERY console application that makes synchronous API calls. Like CopyFile. Or CreateFile.

    If you like, replace NET.EXE with the internal "TYPE" command – try CTRL-C’ing a "TYPE \DFFSDSDFSDFSDFSFDSDFSFSF". It gets blocked on a CreateFile API call while trying to do a DNS resolution of the server "DFFSDSDF", and the I/O can’t be canceled because the CreateFile API is synchronous.

    There’s no way that the entire Win32 API set could have been made asynchronous, at a minimum, it would have caused MASSIVE waves of complaints on the part of our users, who would have insisted that we add synchronous versions of all the asynchronous APIs (for ease of use).

    And then we’d be back where we are today, because the application authors would all use the synchronous version of the API.

    And what about the user that uses "cat" under the Posix subsystem? That user isn’t even running a Win32 application, they’re running an app that’s calling the synchronous Posix open() API with the non existant networked resource, which again is a synchronous API call and….

    Btw, I’m using networks as examples here simply because DNS resolutions take time, and it’s easy to generate a slow operation. This can happen with ANY synchronous operation.

  3. Anonymous says:

    As a side note in the synchronous vs asynchronous API thing, I had to do some Mac programming for the first time in my life recently. I was shocked to discover that virtually all APIs that can block have asynchronous equivalents. This probably came from necessity in the cooperative multitasking days of Mac OS <10. I thought it was pretty cool though, I always thought UNIX and Windows needed just a litte more support for asynchronous operations.

  4. Anonymous says:

    Way back when, back in the very early days of this blog (actually it was the 3rd post to my blog), I…