2
2
3
3
import contextlib
4
4
import os
5
+ import signal
5
6
import subprocess
6
7
import sys
7
8
import warnings
9
+ from collections .abc import Awaitable
8
10
from contextlib import ExitStack
9
11
from functools import partial
10
12
from typing import (
30
32
from ._util import NoPublicConstructor , final
31
33
32
34
if TYPE_CHECKING :
33
- import signal
34
35
from collections .abc import Awaitable , Callable , Iterable , Mapping , Sequence
35
36
from io import TextIOWrapper
36
37
@@ -441,24 +442,45 @@ async def _windows_deliver_cancel(p: Process) -> None: # noqa: RUF029
441
442
)
442
443
443
444
444
- async def _posix_deliver_cancel (p : Process ) -> None :
445
- try :
446
- p .terminate ()
447
- await trio .sleep (5 )
448
- warnings .warn (
449
- RuntimeWarning (
450
- f"process { p !r} ignored SIGTERM for 5 seconds. "
451
- "(Maybe you should pass a custom deliver_cancel?) "
452
- "Trying SIGKILL." ,
453
- ),
454
- stacklevel = 1 ,
455
- )
456
- p .kill ()
457
- except OSError as exc :
458
- warnings .warn (
459
- RuntimeWarning (f"tried to kill process { p !r} , but failed with: { exc !r} " ),
460
- stacklevel = 1 ,
445
+ def _get_posix_deliver_cancel (
446
+ process_group : int | None ,
447
+ ) -> Callable [[Process ], Awaitable [None ]]:
448
+ async def _posix_deliver_cancel (p : Process ) -> None :
449
+ should_deliver_to_pg = (
450
+ sys .platform != "win32"
451
+ and process_group is not None
452
+ and os .getpgrp () != os .getpgid (p .pid )
461
453
)
454
+ try :
455
+ # TODO: should Process#terminate do this special logic
456
+ if sys .platform != "win32" and should_deliver_to_pg :
457
+ os .killpg (os .getpgid (p .pid ), signal .SIGTERM )
458
+ else :
459
+ p .terminate ()
460
+
461
+ await trio .sleep (5 )
462
+ warnings .warn (
463
+ RuntimeWarning (
464
+ f"process { p !r} ignored SIGTERM for 5 seconds. "
465
+ "(Maybe you should pass a custom deliver_cancel?) "
466
+ "Trying SIGKILL." ,
467
+ ),
468
+ stacklevel = 1 ,
469
+ )
470
+
471
+ if sys .platform != "win32" and should_deliver_to_pg :
472
+ os .killpg (os .getpgid (p .pid ), signal .SIGKILL )
473
+ else :
474
+ p .kill ()
475
+ except OSError as exc :
476
+ warnings .warn (
477
+ RuntimeWarning (
478
+ f"tried to kill process { p !r} , but failed with: { exc !r} "
479
+ ),
480
+ stacklevel = 1 ,
481
+ )
482
+
483
+ return _posix_deliver_cancel
462
484
463
485
464
486
# Use a private name, so we can declare platform-specific stubs below.
@@ -472,6 +494,7 @@ async def _run_process(
472
494
check : bool = True ,
473
495
deliver_cancel : Callable [[Process ], Awaitable [object ]] | None = None ,
474
496
task_status : TaskStatus [Process ] = trio .TASK_STATUS_IGNORED ,
497
+ shell : bool = False ,
475
498
** options : object ,
476
499
) -> subprocess .CompletedProcess [bytes ]:
477
500
"""Run ``command`` in a subprocess and wait for it to complete.
@@ -689,6 +712,9 @@ async def my_deliver_cancel(process):
689
712
"stderr=subprocess.PIPE is only valid with nursery.start, "
690
713
"since that's the only way to access the pipe" ,
691
714
)
715
+
716
+ options ["shell" ] = shell
717
+
692
718
if isinstance (stdin , (bytes , bytearray , memoryview )):
693
719
input_ = stdin
694
720
options ["stdin" ] = subprocess .PIPE
@@ -708,12 +734,36 @@ async def my_deliver_cancel(process):
708
734
raise ValueError ("can't specify both stderr and capture_stderr" )
709
735
options ["stderr" ] = subprocess .PIPE
710
736
737
+ # ensure things can be killed including a shell's child processes
738
+ if shell and sys .platform != "win32" and "process_group" not in options :
739
+ options ["process_group" ] = 0
740
+
711
741
if deliver_cancel is None :
712
742
if os .name == "nt" :
713
743
deliver_cancel = _windows_deliver_cancel
714
744
else :
715
745
assert os .name == "posix"
716
- deliver_cancel = _posix_deliver_cancel
746
+ deliver_cancel = _get_posix_deliver_cancel (options .get ("process_group" )) # type: ignore[arg-type]
747
+
748
+ if (
749
+ sys .platform != "win32"
750
+ and options .get ("process_group" ) is not None
751
+ and sys .version_info < (3 , 11 )
752
+ ):
753
+ # backport the argument to Python versions prior to 3.11
754
+ preexec_fn = options .get ("preexec_fn" )
755
+ process_group = options .pop ("process_group" )
756
+
757
+ def new_preexecfn () -> object : # pragma: no cover
758
+ assert sys .platform != "win32"
759
+ os .setpgid (0 , process_group ) # type: ignore[arg-type]
760
+
761
+ if callable (preexec_fn ):
762
+ return preexec_fn ()
763
+ else :
764
+ return None
765
+
766
+ options ["preexec_fn" ] = new_preexecfn
717
767
718
768
stdout_chunks : list [bytes | bytearray ] = []
719
769
stderr_chunks : list [bytes | bytearray ] = []
0 commit comments