mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
141 lines
3.9 KiB
Python
141 lines
3.9 KiB
Python
import asyncio
|
|
import os
|
|
import signal
|
|
import sys
|
|
from contextlib import contextmanager
|
|
from typing import Callable, Optional, cast
|
|
|
|
import click.exceptions
|
|
|
|
|
|
@contextmanager
|
|
def Runner():
|
|
if hasattr(asyncio, "Runner"):
|
|
with asyncio.Runner() as runner:
|
|
yield runner
|
|
else:
|
|
|
|
class _Runner:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
pass
|
|
|
|
def run(self, coro):
|
|
asyncio.run(coro)
|
|
|
|
yield _Runner()
|
|
|
|
|
|
async def subp_exec(
|
|
cmd: str,
|
|
*args: str,
|
|
input: Optional[str] = None,
|
|
wait: Optional[float] = None,
|
|
verbose: bool = False,
|
|
collect: bool = False,
|
|
on_stdout: Optional[Callable[[str], Optional[bool]]] = None,
|
|
) -> tuple[Optional[str], Optional[str]]:
|
|
if verbose:
|
|
cmd_str = f"+ {cmd} {' '.join(map(str, args))}"
|
|
if input:
|
|
print(cmd_str, " <\n", "\n".join(filter(None, input.splitlines())), sep="")
|
|
else:
|
|
print(cmd_str)
|
|
if wait:
|
|
await asyncio.sleep(wait)
|
|
|
|
try:
|
|
proc = await asyncio.create_subprocess_exec(
|
|
cmd,
|
|
*args,
|
|
stdin=asyncio.subprocess.PIPE if input else None,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
|
|
def signal_handler():
|
|
# make sure process exists, then terminate it
|
|
if proc.returncode is None:
|
|
proc.terminate()
|
|
|
|
loop = asyncio.get_event_loop()
|
|
loop.add_signal_handler(signal.SIGINT, signal_handler)
|
|
loop.add_signal_handler(signal.SIGTERM, signal_handler)
|
|
|
|
empty_fut: asyncio.Future = asyncio.Future()
|
|
empty_fut.set_result(None)
|
|
stdout, stderr, _ = await asyncio.gather(
|
|
monitor_stream(
|
|
cast(asyncio.StreamReader, proc.stdout),
|
|
collect=True,
|
|
display=verbose,
|
|
on_line=on_stdout,
|
|
),
|
|
monitor_stream(
|
|
cast(asyncio.StreamReader, proc.stderr),
|
|
collect=True,
|
|
display=verbose,
|
|
),
|
|
proc._feed_stdin(input.encode()) if input else empty_fut, # type: ignore[attr-defined]
|
|
)
|
|
returncode = await proc.wait()
|
|
if (
|
|
returncode is not None
|
|
and returncode != 0 # success
|
|
and returncode != 130 # user interrupt
|
|
):
|
|
sys.stdout.write(stdout.decode() if stdout else "")
|
|
sys.stderr.write(stderr.decode() if stderr else "")
|
|
raise click.exceptions.Exit(returncode)
|
|
if collect:
|
|
return (
|
|
stdout.decode() if stdout else None,
|
|
stderr.decode() if stderr else None,
|
|
)
|
|
else:
|
|
return None, None
|
|
finally:
|
|
try:
|
|
if proc.returncode is None:
|
|
try:
|
|
os.killpg(os.getpgid(proc.pid), signal.SIGINT)
|
|
except (ProcessLookupError, KeyboardInterrupt):
|
|
pass
|
|
|
|
loop.remove_signal_handler(signal.SIGINT)
|
|
loop.remove_signal_handler(signal.SIGTERM)
|
|
except UnboundLocalError:
|
|
pass
|
|
|
|
|
|
async def monitor_stream(
|
|
stream: asyncio.StreamReader,
|
|
collect: bool = False,
|
|
display: bool = False,
|
|
on_line: Optional[Callable[[str], Optional[bool]]] = None,
|
|
) -> Optional[bytearray]:
|
|
if collect:
|
|
ba = bytearray()
|
|
|
|
def handle(line: bytes):
|
|
nonlocal on_line
|
|
nonlocal display
|
|
|
|
if collect:
|
|
ba.extend(line)
|
|
if display:
|
|
sys.stdout.write(line.decode())
|
|
if on_line:
|
|
if on_line(line.decode()):
|
|
on_line = None
|
|
display = True
|
|
|
|
async for line in stream:
|
|
await asyncio.to_thread(handle, line)
|
|
if collect:
|
|
return ba
|
|
else:
|
|
return None
|