|
10 | 10 | from ipaddress import ip_address, IPv6Address
|
11 | 11 | from pathlib import Path
|
12 | 12 | from socket import AddressFamily, SocketKind
|
| 13 | +from subprocess import PIPE, CompletedProcess, DEVNULL, CalledProcessError |
13 | 14 | from typing import TypeVar, Callable, Union, Optional, Awaitable, Coroutine, Any, Dict, List, Tuple
|
14 | 15 |
|
15 | 16 | import sniffio
|
16 | 17 |
|
17 | 18 | from .abc import (
|
18 | 19 | Lock, Condition, Event, Semaphore, CapacityLimiter, CancelScope, TaskGroup, IPAddressType,
|
19 |
| - SocketStream, UDPSocket, ConnectedUDPSocket, IPSockAddrType, Listener, SocketListener) |
| 20 | + SocketStream, UDPSocket, ConnectedUDPSocket, IPSockAddrType, Listener, SocketListener, Process) |
20 | 21 | from .fileio import AsyncFile
|
21 | 22 | from .streams.tls import TLSStream
|
22 | 23 | from .streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
@@ -265,6 +266,81 @@ def current_default_worker_thread_limiter() -> CapacityLimiter:
|
265 | 266 | return _get_asynclib().current_default_thread_limiter()
|
266 | 267 |
|
267 | 268 |
|
| 269 | +# |
| 270 | +# Subprocesses |
| 271 | +# |
| 272 | + |
| 273 | +async def run_process(command: Union[str, typing.Sequence[str]], *, input: Optional[bytes] = None, |
| 274 | + stdout: int = PIPE, stderr: int = PIPE, |
| 275 | + check: bool = True) -> CompletedProcess: |
| 276 | + """ |
| 277 | + Run an external command in a subprocess and wait until it completes. |
| 278 | +
|
| 279 | + .. seealso:: :func:`subprocess.run` |
| 280 | +
|
| 281 | + :param command: either a string to pass to the shell, or an iterable of strings containing the |
| 282 | + executable name or path and its arguments |
| 283 | + :param input: bytes passed to the standard input of the subprocess |
| 284 | + :param stdout: either :data:`subprocess.PIPE` or :data:`subprocess.DEVNULL` |
| 285 | + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL` or |
| 286 | + :data:`subprocess.STDOUT` |
| 287 | + :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the process |
| 288 | + terminates with a return code other than 0 |
| 289 | + :return: an object representing the completed process |
| 290 | + :raises CalledProcessError: if ``check`` is ``True`` and the process exits with a nonzero |
| 291 | + return code |
| 292 | +
|
| 293 | + """ |
| 294 | + async def drain_stream(stream, index): |
| 295 | + chunks = [chunk async for chunk in stream] |
| 296 | + stream_contents[index] = b''.join(chunks) |
| 297 | + |
| 298 | + async with await open_process(command, stdin=PIPE if input else DEVNULL, stdout=stdout, |
| 299 | + stderr=stderr) as process: |
| 300 | + stream_contents = [None, None] |
| 301 | + try: |
| 302 | + async with create_task_group() as tg: |
| 303 | + if process.stdout: |
| 304 | + await tg.spawn(drain_stream, process.stdout, 0) |
| 305 | + if process.stderr: |
| 306 | + await tg.spawn(drain_stream, process.stderr, 1) |
| 307 | + if process.stdin and input: |
| 308 | + await process.stdin.send(input) |
| 309 | + await process.stdin.aclose() |
| 310 | + |
| 311 | + await process.wait() |
| 312 | + except BaseException: |
| 313 | + process.kill() |
| 314 | + raise |
| 315 | + |
| 316 | + output, errors = stream_contents |
| 317 | + if check and process.returncode != 0: |
| 318 | + raise CalledProcessError(typing.cast(int, process.returncode), command, output, errors) |
| 319 | + |
| 320 | + return CompletedProcess(command, typing.cast(int, process.returncode), output, errors) |
| 321 | + |
| 322 | + |
| 323 | +async def open_process(command: Union[str, typing.Sequence[str]], *, stdin: int = PIPE, |
| 324 | + stdout: int = PIPE, stderr: int = PIPE) -> Process: |
| 325 | + """ |
| 326 | + Start an external command in a subprocess. |
| 327 | +
|
| 328 | + .. seealso:: :class:`subprocess.Popen` |
| 329 | +
|
| 330 | + :param command: either a string to pass to the shell, or an iterable of strings containing the |
| 331 | + executable name or path and its arguments |
| 332 | + :param stdin: either :data:`subprocess.PIPE` or :data:`subprocess.DEVNULL` |
| 333 | + :param stdout: either :data:`subprocess.PIPE` or :data:`subprocess.DEVNULL` |
| 334 | + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL` or |
| 335 | + :data:`subprocess.STDOUT` |
| 336 | + :return: an asynchronous process object |
| 337 | +
|
| 338 | + """ |
| 339 | + shell = isinstance(command, str) |
| 340 | + return await _get_asynclib().open_process(command, shell=shell, stdin=stdin, stdout=stdout, |
| 341 | + stderr=stderr) |
| 342 | + |
| 343 | + |
268 | 344 | #
|
269 | 345 | # Async file I/O
|
270 | 346 | #
|
|
0 commit comments