python / cpython

The Python programming language
https://www.python.org
Other
63.55k stars 30.45k forks source link

Support for new Windows pseudoterminals in the subprocess module #85323

Open njsmith opened 4 years ago

njsmith commented 4 years ago
BPO 41151
Nosy @gpshead, @giampaolo, @njsmith, @eryksun, @zooba, @cs01

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields: ```python assignee = None closed_at = None created_at = labels = ['type-feature', '3.10'] title = 'Support for new Windows pseudoterminals in the subprocess module' updated_at = user = 'https://github.com/njsmith' ``` bugs.python.org fields: ```python activity = actor = 'cs01' assignee = 'none' closed = False closed_date = None closer = None components = [] creation = creator = 'njs' dependencies = [] files = [] hgrepos = [] issue_num = 41151 keywords = [] message_count = 2.0 messages = ['372526', '372536'] nosy_count = 6.0 nosy_names = ['gregory.p.smith', 'giampaolo.rodola', 'njs', 'eryksun', 'steve.dower', 'cs01'] pr_nums = [] priority = 'normal' resolution = None stage = None status = 'open' superseder = None type = 'enhancement' url = 'https://bugs.python.org/issue41151' versions = ['Python 3.10'] ```

njsmith commented 4 years ago

So Windows finally has pty support: https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/

However, the API is a bit weird. Unlike Unix, when you create a Windows pty, there's no way to directly get access to the "slave" handle. Instead, you first call CreatePseudoConsole to get a special "HPCON" object, which is similar to a Unix pty master. And then you can have to use a special CreateProcess incantation to spawn a child process that's attached to our pty.

Specifically, what you have to do is set a special entry in the "lpAttributeList", with type "PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE".

Details: https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session

Unfortunately, the subprocess module does not provide any way to pass arbitrary attributes through lpAttributeList, which means that it's currently impossible to use pty support on Windows without reimplementing the whole subprocess module.

It would be nice if the subprocess module could somehow support this.

Off the top of my head, I can think of three possible APIs:

Option 1: full support for Windows ptys: this would require wrapping CreatePseudoConsole, providing some object to represent a Windows pty, etc. This is fairly complex (especially since CreatePseudoConsole requires you to pass in some pipe handles, and the user might want to create those themselves).

Option 2: minimal support for Windows ptys: add another supported field to the subprocess module's lpAttributeList wrapper, that lets the user pass in an "HPCON" cast to a Python integer, and stuffs it into the attribute list. This would require users to do all the work to actually *get* the HPCON object, but at least it would make ptys possible to use.

Option 3: generic support for unrecognized lpAttributeList entries: add a field to the subprocess module's lpAttributeList wrapper that lets you add arbitrary entries, specified by type number + arbitrary pointer/chunk of bytes. (Similar to how Python's ioctl or setsockopt wrappers work.) Annoyingly, it doesn't seem to be enough to just pass in a buffer object, because for pseudoconsole support, you actually have to pass in an opaque "HPCON" object directly. (This is kind of weird, and might even be a bug, see: https://github.com/microsoft/terminal/issues/6705)

eryksun commented 4 years ago

However, the API is a bit weird. Unlike Unix, when you create a Windows pty, there's no way to directly get access to the "slave" handle. Instead, you first call CreatePseudoConsole to get a special "HPCON" object, which is similar to a Unix pty master. And then you can have to use a special CreateProcess incantation to spawn a child process that's attached to our pty.

As implemented in Windows 8+ (the previous implementation is very different), if a process is attached to a console session, the ConsoleHandle value in the PEB ProcessParameters is a file handle for "\Device\ConDrv\Connect", which is a connection to a console session that's hosted by conhost.exe (or openconsole.exe if using Windows Terminal). The connection handle is used implicitly with NtDeviceIoControlFile to implement API functions such as GetConsoleWindow, GetConsoleTitle, GetConsoleCP, GetConsoleAliases, GetConsoleProcessList, GenerateConsoleCtrlEvent, and so on. Console I/O, on the other hand, uses dedicated files on "\Device\ConDrv". The initial files used for standard I/O are generic "Input" and "Output", which work with any console connection. There are other ConDrv files that, when opened, attach to a particular console session and are only valid when the process is connected to that session, including "Console" (CON), "CurrentIn" (CONIN$), "CurrentOut" (CONOUT$), and "ScreenBuffer" (CreateConsoleScreenBuffer). Simple ReadFile and WriteFile use NT NtReadFile and NtWriteFile system calls. Console-specific I/O functions such as GetConsoleMode, ReadConsoleW, and WriteConsoleW use NtDeviceIoControlFile.

If a console connection is inherited, initially the ConsoleHandle value is a handle for "\Device\ConDrv\Reference", which references the console session. (If a console session isn't inherited, the ConsoleHandle value may be one of the values reserved for the creation flags CREATE_NEW_CONSOLE, CREATE_NO_WINDOW, and DETACHED_PROCESS.) The base API opens the "\Connect" connection handle relative to the reference handle. The NtCreateFile call in this case uses the reserved EaBuffer (extended attributes) argument to send the console session host the information it needs about the connecting client process. Once connected, depending on the state, the base API may close inherited console handles (i.e. handles with the NT device type FILE_DEVICE_CONSOLE) that are set in StandardInput, StandardOutput, and StandardError, in which case new generic "Input" and "Output" handles are opened relative to the connection handle.

The pseudoconsole API apparently uses PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE to explicitly pass the "\Device\ConDrv\Reference" handle to client processes, instead of inheriting the reference handle of the current process, if the current process even has a console. An HPCON appears to be just a pointer to an array of three handles. One of the handles is the "Reference" handle that client processes need. The other two handles are for the API -- one for the console-session host process (conhost.exe) and one for the signal pipe. You can see the handle value for the other end of the signal pipe passed on the conhost.exe command line as the "--signal" parameter.