_subprocesses.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. from __future__ import annotations
  2. from collections.abc import AsyncIterable, Iterable, Mapping, Sequence
  3. from io import BytesIO
  4. from os import PathLike
  5. from subprocess import PIPE, CalledProcessError, CompletedProcess
  6. from typing import IO, Any, TypeAlias, cast
  7. from ..abc import Process
  8. from ._eventloop import get_async_backend
  9. from ._tasks import create_task_group
  10. StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]
  11. async def run_process(
  12. command: StrOrBytesPath | Sequence[StrOrBytesPath],
  13. *,
  14. input: bytes | None = None,
  15. stdin: int | IO[Any] | None = None,
  16. stdout: int | IO[Any] | None = PIPE,
  17. stderr: int | IO[Any] | None = PIPE,
  18. check: bool = True,
  19. cwd: StrOrBytesPath | None = None,
  20. env: Mapping[str, str] | None = None,
  21. startupinfo: Any = None,
  22. creationflags: int = 0,
  23. start_new_session: bool = False,
  24. pass_fds: Sequence[int] = (),
  25. user: str | int | None = None,
  26. group: str | int | None = None,
  27. extra_groups: Iterable[str | int] | None = None,
  28. umask: int = -1,
  29. ) -> CompletedProcess[bytes]:
  30. """
  31. Run an external command in a subprocess and wait until it completes.
  32. .. seealso:: :func:`subprocess.run`
  33. :param command: either a string to pass to the shell, or an iterable of strings
  34. containing the executable name or path and its arguments
  35. :param input: bytes passed to the standard input of the subprocess
  36. :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
  37. a file-like object, or `None`; ``input`` overrides this
  38. :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
  39. a file-like object, or `None`
  40. :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
  41. :data:`subprocess.STDOUT`, a file-like object, or `None`
  42. :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the
  43. process terminates with a return code other than 0
  44. :param cwd: If not ``None``, change the working directory to this before running the
  45. command
  46. :param env: if not ``None``, this mapping replaces the inherited environment
  47. variables from the parent process
  48. :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used
  49. to specify process startup parameters (Windows only)
  50. :param creationflags: flags that can be used to control the creation of the
  51. subprocess (see :class:`subprocess.Popen` for the specifics)
  52. :param start_new_session: if ``true`` the setsid() system call will be made in the
  53. child process prior to the execution of the subprocess. (POSIX only)
  54. :param pass_fds: sequence of file descriptors to keep open between the parent and
  55. child processes. (POSIX only)
  56. :param user: effective user to run the process as (Python >= 3.9, POSIX only)
  57. :param group: effective group to run the process as (Python >= 3.9, POSIX only)
  58. :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9,
  59. POSIX only)
  60. :param umask: if not negative, this umask is applied in the child process before
  61. running the given command (Python >= 3.9, POSIX only)
  62. :return: an object representing the completed process
  63. :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process
  64. exits with a nonzero return code
  65. """
  66. async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None:
  67. buffer = BytesIO()
  68. async for chunk in stream:
  69. buffer.write(chunk)
  70. stream_contents[index] = buffer.getvalue()
  71. if stdin is not None and input is not None:
  72. raise ValueError("only one of stdin and input is allowed")
  73. async with await open_process(
  74. command,
  75. stdin=PIPE if input else stdin,
  76. stdout=stdout,
  77. stderr=stderr,
  78. cwd=cwd,
  79. env=env,
  80. startupinfo=startupinfo,
  81. creationflags=creationflags,
  82. start_new_session=start_new_session,
  83. pass_fds=pass_fds,
  84. user=user,
  85. group=group,
  86. extra_groups=extra_groups,
  87. umask=umask,
  88. ) as process:
  89. stream_contents: list[bytes | None] = [None, None]
  90. async with create_task_group() as tg:
  91. if process.stdout:
  92. tg.start_soon(drain_stream, process.stdout, 0)
  93. if process.stderr:
  94. tg.start_soon(drain_stream, process.stderr, 1)
  95. if process.stdin and input:
  96. await process.stdin.send(input)
  97. await process.stdin.aclose()
  98. await process.wait()
  99. output, errors = stream_contents
  100. if check and process.returncode != 0:
  101. raise CalledProcessError(cast(int, process.returncode), command, output, errors)
  102. return CompletedProcess(command, cast(int, process.returncode), output, errors)
  103. async def open_process(
  104. command: StrOrBytesPath | Sequence[StrOrBytesPath],
  105. *,
  106. stdin: int | IO[Any] | None = PIPE,
  107. stdout: int | IO[Any] | None = PIPE,
  108. stderr: int | IO[Any] | None = PIPE,
  109. cwd: StrOrBytesPath | None = None,
  110. env: Mapping[str, str] | None = None,
  111. startupinfo: Any = None,
  112. creationflags: int = 0,
  113. start_new_session: bool = False,
  114. pass_fds: Sequence[int] = (),
  115. user: str | int | None = None,
  116. group: str | int | None = None,
  117. extra_groups: Iterable[str | int] | None = None,
  118. umask: int = -1,
  119. ) -> Process:
  120. """
  121. Start an external command in a subprocess.
  122. .. seealso:: :class:`subprocess.Popen`
  123. :param command: either a string to pass to the shell, or an iterable of strings
  124. containing the executable name or path and its arguments
  125. :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a
  126. file-like object, or ``None``
  127. :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
  128. a file-like object, or ``None``
  129. :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
  130. :data:`subprocess.STDOUT`, a file-like object, or ``None``
  131. :param cwd: If not ``None``, the working directory is changed before executing
  132. :param env: If env is not ``None``, it must be a mapping that defines the
  133. environment variables for the new process
  134. :param creationflags: flags that can be used to control the creation of the
  135. subprocess (see :class:`subprocess.Popen` for the specifics)
  136. :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used
  137. to specify process startup parameters (Windows only)
  138. :param start_new_session: if ``true`` the setsid() system call will be made in the
  139. child process prior to the execution of the subprocess. (POSIX only)
  140. :param pass_fds: sequence of file descriptors to keep open between the parent and
  141. child processes. (POSIX only)
  142. :param user: effective user to run the process as (POSIX only)
  143. :param group: effective group to run the process as (POSIX only)
  144. :param extra_groups: supplementary groups to set in the subprocess (POSIX only)
  145. :param umask: if not negative, this umask is applied in the child process before
  146. running the given command (POSIX only)
  147. :return: an asynchronous process object
  148. """
  149. kwargs: dict[str, Any] = {}
  150. if user is not None:
  151. kwargs["user"] = user
  152. if group is not None:
  153. kwargs["group"] = group
  154. if extra_groups is not None:
  155. kwargs["extra_groups"] = extra_groups
  156. if umask >= 0:
  157. kwargs["umask"] = umask
  158. return await get_async_backend().open_process(
  159. command,
  160. stdin=stdin,
  161. stdout=stdout,
  162. stderr=stderr,
  163. cwd=cwd,
  164. env=env,
  165. startupinfo=startupinfo,
  166. creationflags=creationflags,
  167. start_new_session=start_new_session,
  168. pass_fds=pass_fds,
  169. **kwargs,
  170. )