classification
Title: Unable to send CTRL_BREAK_EVENT
Type: behavior Stage:
Components: Windows Versions: Python 3.8, Python 3.7, Python 2.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: Ofekmeister, eryksun, paul.moore, steve.dower, tim.golden, zach.ware
Priority: normal Keywords:

Created on 2018-04-09 03:47 by Ofekmeister, last changed 2018-04-09 06:17 by eryksun.

Messages (2)
msg315108 - (view) Author: Ofek Lev (Ofekmeister) * Date: 2018-04-09 03:47
Vault (https://github.com/hashicorp/vault) requires the use of signals to trigger certain output https://www.vaultproject.io/docs/internals/telemetry.html.

The required signal isn't sent on py2.7:

>>> import os
>>> import signal
>>> import psutil
>>> p = psutil.Process([p.info for p in psutil.process_iter(attrs=['pid', 'name']) if 'vault' in p.info['name']][0]['pid'])
>>> p.exe()
'C:\\Users\\Ofek\\Desktop\\vault.exe'
>>> p.pid
15536
>>> os.kill(p.pid, signal.CTRL_BREAK_EVENT)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
WindowsError: [Error 87] The parameter is incorrect

Interestingly, on py3.6 that code works but instead produces this:

OSError: [WinError 87] The parameter is incorrect

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
SystemError: <built-in function kill> returned a result with an error set
msg315109 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2018-04-09 06:17
Unless you're willing to step through hoops using ctypes or PyWin32, sending Ctrl+Break requires already being attached to the same console as the target. Note that the target isn't necessarily a single process that's attached to the console. Sending a control event is implemented by calling WinAPI GenerateConsoleCtrlEvent, which targets process group identifiers (i.e. like Unix killpg), not process identifiers (i.e. not like Unix kill). 

FYI, to generate the event, the console host (conhost.exe) uses an LPC port to relay the request to the session Windows server (csrss.exe), which is privileged to inject a thread in each target process. This control thread starts executing at a known entry point named "CtrlRoutine" in kernelbase.dll. (Prior to Windows 7 the console is actually hosted in csrss.exe, so there's no need to relay the request, and "CtrlRoutine" is implemented in kernel32.dll instead.)

If you don't know the group ID, then the only option is to broadcast Ctrl+Break to group 0, which includes every process that's attached to the console. First ignore SIGBREAK (the C signal for CTRL_BREAK_EVENT) via signal.signal(signal.SIGBREAK, signal.SIG_IGN). Otherwise the default handler will run, which exits the current process. Then send Ctrl+Break via os.kill(0, signal.CTRL_BREAK_EVENT). 

On the other hand, if you started the process as a new group, then its ID is also the group ID. For example, p = subprocess.Popen('vault.exe', creationflags=subprocess.CREATE_NEW_PROCESS_GROUP). In this case you can   send Ctrl+Break via os.kill(p.pid, signal.CTRL_BREAK_EVENT). Windows will generate the event only in the subset of processes that are both in the target process group and currently attached to the console that originated the request.

The current implementation of os.kill() is behaving as designed and documented in Python 2.7. There is no bug in this case. However, the docs should clarify that the target when sending a console control event is a process group, not a process. They should also refer to the subprocess docs to explain how to create a new group via the CREATE_NEW_PROCESS_GROUP creation flag. Also, the line about accepting process handles should be stricken. A process handle is an integer that's indistinguishable from a process/group identifier, so the statement makes no sense, and thankfully the code doesn't try to implement anything that crazy.

In Python 3, someone decided that if GenerateConsoleCtrlEvent fails, os.kill() should also try TerminateProcess, to allow terminating the process explicitly with exit codes 0 (CTRL_C_EVENT) and 1 (CTRL_BREAK_EVENT). Of course, killing a process using a console control event is nothing at all like terminating it abruptly, so to me this looks quite strange; it's coding with a sledgehammer. Anyway, if TerminateProcess succeeds it should, but currently does not, clear the error from the failed GenerateConsoleCtrlEvent call. That's a bug.

I suggest resolving the bug by partially backing out of the change. It should only call GenerateConsoleCtrlEvent for the enum values signal.CTRL_C_EVENT and signal.CTRL_BREAK_EVENT, which I think was suggested by Steve Dower at one point. TerminateProcess can be called otherwise for 0 and 1 values. In this case, os.kill() would no longer magically switch between fundamentally different functions in a single call. An exception should always be raised if GenerateConsoleCtrlEvent fails. Possibly in 3.8, os.killpg() could be added on Windows for sending console control events to process groups, while deprecating or discouraging using os.kill() for this.
History
Date User Action Args
2018-04-09 06:17:38eryksunsetnosy: + eryksun

messages: + msg315109
versions: + Python 3.7, Python 3.8
2018-04-09 03:47:30Ofekmeistercreate