2023-09-04 19:08:09 +02:00
|
|
|
"""Module inspired by subprocess which allows for asynchronous
|
|
|
|
multiprocessing"""
|
|
|
|
|
2023-06-06 11:31:30 +02:00
|
|
|
import os
|
2023-06-15 16:39:08 +02:00
|
|
|
import structio
|
2023-08-23 21:56:25 +02:00
|
|
|
import platform
|
2023-06-06 11:31:30 +02:00
|
|
|
import subprocess
|
2023-06-19 17:34:44 +02:00
|
|
|
from subprocess import CalledProcessError, CompletedProcess, DEVNULL, PIPE
|
2023-06-15 16:39:08 +02:00
|
|
|
from structio.io import FileStream
|
2024-02-23 13:11:14 +01:00
|
|
|
|
2023-08-23 21:56:25 +02:00
|
|
|
if platform.system() == "Windows":
|
|
|
|
# Windows doesn't really support non-blocking file
|
2023-09-04 19:08:09 +02:00
|
|
|
# descriptors (except sockets), so we just use threads
|
2023-08-23 21:56:25 +02:00
|
|
|
from structio.io.files import AsyncFile as FileStream
|
2023-05-18 18:20:50 +02:00
|
|
|
|
|
|
|
|
2023-06-06 11:31:30 +02:00
|
|
|
class Popen:
|
2023-05-18 18:20:50 +02:00
|
|
|
"""
|
2023-06-06 11:31:30 +02:00
|
|
|
Wrapper around subprocess.Popen, but async
|
2023-05-18 18:20:50 +02:00
|
|
|
"""
|
|
|
|
|
2023-06-06 11:31:30 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
if "universal_newlines" in kwargs:
|
|
|
|
# Not sure why? But everyone else is doing it so :shrug:
|
|
|
|
raise RuntimeError("universal_newlines is not supported")
|
|
|
|
if stdin := kwargs.get("stdin"):
|
2023-06-15 16:39:08 +02:00
|
|
|
if stdin not in {PIPE, DEVNULL}:
|
|
|
|
# Curio mentions stuff breaking if the child process
|
|
|
|
# is passed a stdin fd that is set to non-blocking mode
|
2023-08-23 21:56:25 +02:00
|
|
|
if hasattr(os, "set_blocking"):
|
|
|
|
os.set_blocking(stdin.fileno(), True)
|
2023-06-06 11:31:30 +02:00
|
|
|
# Delegate to Popen's constructor
|
2023-06-15 16:39:08 +02:00
|
|
|
self._process: subprocess.Popen = subprocess.Popen(*args, **kwargs)
|
|
|
|
self.stdin = None
|
|
|
|
self.stdout = None
|
|
|
|
self.stderr = None
|
2023-06-12 11:42:07 +02:00
|
|
|
if self._process.stdin:
|
2023-06-15 16:39:08 +02:00
|
|
|
self.stdin = FileStream(self._process.stdin)
|
|
|
|
if self._process.stdout:
|
|
|
|
self.stdout = FileStream(self._process.stdout)
|
|
|
|
if self._process.stderr:
|
|
|
|
self.stderr = FileStream(self._process.stderr)
|
|
|
|
|
|
|
|
async def wait(self):
|
|
|
|
status = self._process.poll()
|
|
|
|
if status is None:
|
2023-06-19 17:34:44 +02:00
|
|
|
status = await structio.thread.run_in_worker(
|
|
|
|
self._process.wait, cancellable=True
|
|
|
|
)
|
2023-06-15 16:39:08 +02:00
|
|
|
return status
|
|
|
|
|
|
|
|
async def communicate(self, input=b"") -> tuple[bytes, bytes]:
|
|
|
|
async with structio.create_pool() as pool:
|
|
|
|
stdout = pool.spawn(self.stdout.readall) if self.stdout else None
|
|
|
|
stderr = pool.spawn(self.stderr.readall) if self.stderr else None
|
|
|
|
if input:
|
|
|
|
await self.stdin.write(input)
|
|
|
|
await self.stdin.close()
|
|
|
|
# Awaiting a task object waits for its completion and
|
|
|
|
# returns its return value!
|
|
|
|
out = b""
|
|
|
|
err = b""
|
|
|
|
if stdout:
|
|
|
|
out = await stdout
|
|
|
|
if stderr:
|
|
|
|
err = await stderr
|
|
|
|
return out, err
|
|
|
|
|
|
|
|
async def __aenter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
async def __aexit__(self, *args):
|
|
|
|
if self.stdin:
|
|
|
|
await self.stdin.close()
|
|
|
|
if self.stdout:
|
|
|
|
await self.stdout.close()
|
|
|
|
if self.stderr:
|
|
|
|
await self.stderr.close()
|
|
|
|
await self.wait()
|
2023-06-06 11:31:30 +02:00
|
|
|
|
|
|
|
def __getattr__(self, item):
|
|
|
|
# Delegate to internal process object
|
|
|
|
return getattr(self._process, item)
|
|
|
|
|
2023-06-15 16:39:08 +02:00
|
|
|
|
2023-06-19 17:34:44 +02:00
|
|
|
async def run(
|
|
|
|
args, *, stdin=None, input=None, stdout=None, stderr=None, shell=False, check=False
|
|
|
|
):
|
2023-06-15 16:39:08 +02:00
|
|
|
"""
|
|
|
|
Async version of subprocess.run()
|
|
|
|
"""
|
|
|
|
|
|
|
|
if input:
|
|
|
|
stdin = subprocess.PIPE
|
2023-06-19 17:34:44 +02:00
|
|
|
async with Popen(
|
|
|
|
args, stdin=stdin, stdout=stdout, stderr=stderr, shell=shell
|
|
|
|
) as process:
|
2023-06-15 16:39:08 +02:00
|
|
|
try:
|
|
|
|
stdout, stderr = await process.communicate(input)
|
|
|
|
except:
|
|
|
|
process.kill()
|
|
|
|
raise
|
|
|
|
|
|
|
|
status = process.poll()
|
|
|
|
if check and status:
|
|
|
|
raise CalledProcessError(status, process.args, output=stdout, stderr=stderr)
|
|
|
|
return CompletedProcess(process.args, status, stdout, stderr)
|
|
|
|
|
|
|
|
|
|
|
|
async def check_output(args, *, stdin=None, stderr=None, shell=False, input=None):
|
|
|
|
"""
|
|
|
|
Async version of subprocess.check_output
|
|
|
|
"""
|
|
|
|
|
2023-06-19 17:34:44 +02:00
|
|
|
out = await run(
|
|
|
|
args,
|
|
|
|
stdout=PIPE,
|
|
|
|
stdin=stdin,
|
|
|
|
stderr=stderr,
|
|
|
|
shell=shell,
|
|
|
|
check=True,
|
|
|
|
input=input,
|
|
|
|
)
|
2023-06-15 16:39:08 +02:00
|
|
|
return out.stdout
|