Issue 35568: Expose the C raise() function in the signal module, for use on Windows (original) (raw)

Suppose we want to test how a program responds to control-C. We'll want to write a test that delivers a SIGINT to our process at a time of our choosing, and then checks what happens. For example, asyncio and Trio both have tests like this, and Trio even does a similar thing at run-time to avoid dropping signals in an edge case [1].

So, how can we deliver a signal to our process?

On POSIX platforms, you can use os.kill(os.getpid(), signal.SIGINT), and that works fine. But on Windows, things are much more complicated: https://github.com/python/cpython/pull/11274#issuecomment-449543725

The simplest solution is to use the raise() function. On POSIX, raise(signum) is just a shorthand for kill(getpid(), signum). But, that's only on POSIX. On Windows, kill() doesn't even exist... but raise() does. In fact raise() is specified in C89, so every C runtime has to provide raise(), no matter what OS it runs on.

So, you might think, that's ok, if we need to generate synthetic signals on Windows then we'll just use ctypes/cffi to access raise(). But, Windows has multiple C runtime libraries (for example: regular and debug), and you have to load raise() from the same library that Python is linked against. And I don't know of any way for a Python script to figure out which runtime it's linked against. (I know how to detect whether the interpreter is configured in debug mode, but that's not necessarily the same as being linked against the debug CRT.) So on the one platform where you really need to use raise(), there's AFAICT no reliable way to get access to it.

This would all be much simpler if the signal module wrapped the raise() function, so that we could just do 'signal.raise_(signal.SIGINT)'. We should do that.


[1] Specifically, consider the following case (I'll use asyncio terminology for simplicity): (1) the user calls loop.add_signal_handler(...) to register a custom signal handler. (2) a signal arrives, and is written to the wakeup pipe. (3) but, before the loop reads the wakeup pipe, the user calls loop.remove_signal_handler(...) to remove the custom handler and restore the original signal settings. (4) now the loop reads the wakeup pipe, and discovers that a signal has arrived, that it no longer knows how to handle. Now what? In this case trio uses raise() to redeliver the signal, so that the new signal handler has a chance to run.

That implementation will require some more of our usual macros around the call itself (for GIL and invalid values), and maybe clinicisation, as well as more through tests, which are probably going to be the hardest part.

Nathaniel - were you planning to do this? Or should we put the "easy (C)" keyword on it and wait for some sprints?

I'd like to see it in 3.8, but don't know if I'll get to it, and it'd be a good onramp issue for someone who wants to get into cpython development. So, let's put the keyword on it for now, and see what happens...