2
2
from __future__ import annotations
3
3
4
4
import fnmatch
5
+ import io
5
6
import logging
6
7
import os
7
8
import shutil
@@ -240,12 +241,19 @@ def __exit__(
240
241
241
242
@staticmethod
242
243
def get_stream_file_no (key : str ) -> Generator [int , Popen [bytes ], None ]:
243
- process = yield PIPE
244
- stream = getattr (process , key )
245
- if sys .platform == "win32" : # explicit check for mypy # pragma: win32 cover
246
- yield stream .handle
244
+ allocated_pty = _pty (key )
245
+ if allocated_pty is not None :
246
+ main_fd , child_fd = allocated_pty
247
+ yield child_fd
248
+ os .close (child_fd ) # close the child process pipe
249
+ yield main_fd
247
250
else :
248
- yield stream .name
251
+ process = yield PIPE
252
+ stream = getattr (process , key )
253
+ if sys .platform == "win32" : # explicit check for mypy # pragma: win32 cover
254
+ yield stream .handle
255
+ else :
256
+ yield stream .name
249
257
250
258
def set_out_err (self , out : SyncWrite , err : SyncWrite ) -> tuple [SyncWrite , SyncWrite ]:
251
259
prev = self ._out , self ._err
@@ -256,6 +264,56 @@ def set_out_err(self, out: SyncWrite, err: SyncWrite) -> tuple[SyncWrite, SyncWr
256
264
return prev
257
265
258
266
267
+ def _pty (key : str ) -> tuple [int , int ] | None :
268
+ """
269
+ Allocate a virtual terminal (pty) for a subprocess.
270
+
271
+ A virtual terminal allows a process to perform syscalls that fetch attributes related to the tty,
272
+ for example to determine whether to use colored output or enter interactive mode.
273
+
274
+ The termios attributes of the controlling terminal stream will be copied to the allocated pty.
275
+
276
+ :param key: The stream to copy attributes from. Either "stdout" or "stderr".
277
+ :return: (main_fd, child_fd) of an allocated pty; or None on error or if unsupported (win32).
278
+ """
279
+ if sys .platform == "win32" : # explicit check for mypy # pragma: win32 cover
280
+ return None
281
+
282
+ stream : io .TextIOWrapper = getattr (sys , key )
283
+
284
+ # when our current stream is a tty, emulate pty for the child
285
+ # to allow host streams traits to be inherited
286
+ if not stream .isatty ():
287
+ return None
288
+
289
+ try :
290
+ import fcntl
291
+ import pty
292
+ import struct
293
+ import termios
294
+ except ImportError : # pragma: no cover
295
+ return None # cannot proceed on platforms without pty support
296
+
297
+ try :
298
+ main , child = pty .openpty ()
299
+ except OSError : # could not open a tty
300
+ return None # pragma: no cover
301
+
302
+ try :
303
+ mode = termios .tcgetattr (stream )
304
+ termios .tcsetattr (child , termios .TCSANOW , mode )
305
+ except (termios .error , OSError ): # could not inherit traits
306
+ return None # pragma: no cover
307
+
308
+ # adjust sub-process terminal size
309
+ columns , lines = shutil .get_terminal_size (fallback = (- 1 , - 1 ))
310
+ if columns != - 1 and lines != - 1 :
311
+ size = struct .pack ("HHHH" , columns , lines , 0 , 0 )
312
+ fcntl .ioctl (child , termios .TIOCSWINSZ , size )
313
+
314
+ return main , child
315
+
316
+
259
317
__all__ = (
260
318
"SIG_INTERRUPT" ,
261
319
"CREATION_FLAGS" ,
0 commit comments