22
33import contextlib
44import os
5+ import signal
56import subprocess
67import sys
78import warnings
9+ from collections .abc import Awaitable
810from contextlib import ExitStack
911from functools import partial
1012from typing import (
3032from ._util import NoPublicConstructor , final
3133
3234if TYPE_CHECKING :
33- import signal
3435 from collections .abc import Awaitable , Callable , Iterable , Mapping , Sequence
3536 from io import TextIOWrapper
3637
@@ -441,24 +442,45 @@ async def _windows_deliver_cancel(p: Process) -> None: # noqa: RUF029
441442 )
442443
443444
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 )
461453 )
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
462484
463485
464486# Use a private name, so we can declare platform-specific stubs below.
@@ -472,6 +494,7 @@ async def _run_process(
472494 check : bool = True ,
473495 deliver_cancel : Callable [[Process ], Awaitable [object ]] | None = None ,
474496 task_status : TaskStatus [Process ] = trio .TASK_STATUS_IGNORED ,
497+ shell : bool = False ,
475498 ** options : object ,
476499) -> subprocess .CompletedProcess [bytes ]:
477500 """Run ``command`` in a subprocess and wait for it to complete.
@@ -689,6 +712,9 @@ async def my_deliver_cancel(process):
689712 "stderr=subprocess.PIPE is only valid with nursery.start, "
690713 "since that's the only way to access the pipe" ,
691714 )
715+
716+ options ["shell" ] = shell
717+
692718 if isinstance (stdin , (bytes , bytearray , memoryview )):
693719 input_ = stdin
694720 options ["stdin" ] = subprocess .PIPE
@@ -708,12 +734,36 @@ async def my_deliver_cancel(process):
708734 raise ValueError ("can't specify both stderr and capture_stderr" )
709735 options ["stderr" ] = subprocess .PIPE
710736
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+
711741 if deliver_cancel is None :
712742 if os .name == "nt" :
713743 deliver_cancel = _windows_deliver_cancel
714744 else :
715745 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
717767
718768 stdout_chunks : list [bytes | bytearray ] = []
719769 stderr_chunks : list [bytes | bytearray ] = []
0 commit comments