Files
LangBot/plugins/Runner/dashscope-agent/pkg/vendor_process.py

94 lines
3.5 KiB
Python

"""Killable synchronous vendor isolation with bounded pipe backpressure.
No executor threads, inherited provider secrets or queues. A worker is reaped
before cancellation/timeout/early-close returns. Host limits concurrent runs.
"""
import asyncio
import json
import os
import sys
import tempfile
from pathlib import Path
MAX_FRAME = 1024 * 1024
MAX_TOTAL = 16 * 1024 * 1024
async def vendor_stream(payload, *, timeout, worker=None):
worker = worker or Path(__file__).with_name("vendor_worker.py")
payload = dict(payload, import_paths=sys.path)
encoded = json.dumps(payload, ensure_ascii=False).encode() + b"\n"
if len(encoded) > 16 * 1024 * 1024:
raise ValueError("Vendor request exceeds the runtime limit")
# A private parent-owned directory survives a killed upload only until reap.
with tempfile.TemporaryDirectory(prefix="langbot-vendor-") as temp:
env = {"PATH": os.defpath, "TMPDIR": temp, "HOME": temp, "PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"}
spawn = asyncio.create_task(
asyncio.create_subprocess_exec(
sys.executable,
str(worker),
cwd=temp,
env=env,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
limit=MAX_FRAME + 1,
)
)
process = None
try:
end = asyncio.get_running_loop().time() + timeout
async def wait(operation):
remaining = end - asyncio.get_running_loop().time()
async with asyncio.timeout(max(0, remaining)):
return await operation
process = await wait(asyncio.shield(spawn))
process.stdin.write(encoded)
await wait(process.stdin.drain())
process.stdin.close()
total = 0
while True:
line = await wait(process.stdout.readline())
if not line:
if await wait(process.wait()) != 0:
raise RuntimeError("Vendor worker failed")
break
total += len(line)
if len(line) > MAX_FRAME or total > MAX_TOTAL:
raise ValueError("Vendor response exceeds the runtime limit")
frame = json.loads(line)
if "error" in frame:
if frame["error"] == "timeout":
raise TimeoutError("Vendor timed out")
raise RuntimeError("Vendor request failed")
yield frame["item"]
finally:
async def reap():
proc = process or await spawn
if proc.returncode is None:
try:
proc.kill()
except ProcessLookupError:
pass
# Drain after kill: Process.wait alone can wait for a full pipe.
if proc.stdout:
while await proc.stdout.read(65536):
pass
await proc.wait()
cleanup = asyncio.create_task(reap())
cancelled = False
while not cleanup.done():
try:
await asyncio.shield(cleanup)
except asyncio.CancelledError:
cancelled = True
cleanup.result()
if cancelled:
raise asyncio.CancelledError