Skip to content

Commit c342c53

Browse files
committed
improve script running and timeout handing for browser feature
1 parent c9d448a commit c342c53

2 files changed

Lines changed: 486 additions & 38 deletions

File tree

llms/extensions/browser/__init__.py

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import asyncio
2+
import contextlib
23
import json
34
import os
45
import shutil
6+
import tempfile
57
import time
68
from collections import deque
79
from pathlib import Path
@@ -13,6 +15,7 @@
1315
"AGENT_BROWSER_USER_AGENT",
1416
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36",
1517
)
18+
AGENT_BROWSER_TIMEOUT = int(os.getenv("AGENT_BROWSER_TIMEOUT", 30))
1619
DEBUG_LOG = deque(maxlen=200)
1720
DEBUG_LOG_COUNTER = 0
1821

@@ -103,7 +106,7 @@ def _complete_debug_log(log: dict[str, Any], result: dict):
103106
ctx.dbg(f"{rc}: {stderr}")
104107
return log
105108

106-
async def run_browser_cmd_async(*args, timeout=30, env=None, record=True):
109+
async def run_browser_cmd_async(*args, timeout=AGENT_BROWSER_TIMEOUT, env=None, record=True):
107110
"""Run agent-browser command asynchronously."""
108111
cmd = ["agent-browser"] + list(args)
109112
cmd_str = " ".join(cmd)
@@ -176,16 +179,21 @@ async def clear_debug_log(request):
176179

177180
ctx.add_delete("/browser/debug-log", clear_debug_log)
178181

179-
async def get_screenshot(req):
180-
"""Capture and return current screenshot."""
181-
browser_dir = os.path.join(ctx.get_user_path(user=ctx.get_username(req)), "browser")
182-
screenshot_path = os.path.join(browser_dir, "screenshot.png")
183-
snapshot_path = os.path.join(browser_dir, "snapshot.json")
182+
async def run_snapshot(req):
183+
"""Run snapshot command."""
184+
ctx.dbg("Running screenshot + snapshot")
185+
try:
186+
browser_dir = os.path.join(ctx.get_user_path(user=ctx.get_username(req)), "browser")
187+
screenshot_path = os.path.join(browser_dir, "screenshot.png")
188+
snapshot_path = os.path.join(browser_dir, "snapshot.json")
184189

185-
screenshot_result, snapshot_result = await asyncio.gather(
186-
run_browser_cmd_async("screenshot", screenshot_path, record=False),
187-
run_browser_cmd_async("snapshot", "-i", "--json", snapshot_path, record=False),
188-
)
190+
screenshot_result, snapshot_result = await asyncio.gather(
191+
run_browser_cmd_async("screenshot", screenshot_path, record=False),
192+
run_browser_cmd_async("snapshot", "-i", "--json", snapshot_path, record=False),
193+
)
194+
except Exception as e:
195+
ctx.err("Failed to run snapshot", e)
196+
return False, None, None
189197

190198
success = snapshot_result["success"]
191199
if success and snapshot_result.get("stdout", "").strip():
@@ -200,6 +208,12 @@ async def get_screenshot(req):
200208
ctx.err("Failed to parse snapshot JSON\n" + snapshot_result["stdout"], e)
201209
success = False
202210

211+
return success, screenshot_path, snapshot_path
212+
213+
async def get_screenshot(req):
214+
"""Capture and return current screenshot."""
215+
success, screenshot_path, snapshot_path = await run_snapshot(req)
216+
203217
if success and os.path.exists(screenshot_path):
204218
return web.FileResponse(screenshot_path, headers={"Content-Type": "image/png", "Cache-Control": "no-cache"})
205219

@@ -229,7 +243,9 @@ async def get_snapshot(req):
229243
if result.get("stdout", "").strip():
230244
try:
231245
parsed = json.loads(result["stdout"])
232-
parsed.update(await get_status_object())
246+
ret = await get_status_object()
247+
if ret:
248+
parsed.update(ret)
233249
Path(snapshot_path).write_text(json.dumps(parsed))
234250
except Exception as e:
235251
ctx.err("Failed to parse snapshot JSON\n" + result["stdout"], e)
@@ -261,15 +277,15 @@ async def browser_open(req):
261277
"AGENT_BROWSER_PROFILE": get_profile_dir(req),
262278
"AGENT_BROWSER_USER_AGENT": AGENT_BROWSER_USER_AGENT,
263279
}
264-
result = await run_browser_cmd_async("open", url, timeout=60, env=_browser_env)
280+
result = await run_browser_cmd_async("open", url, timeout=AGENT_BROWSER_TIMEOUT, env=_browser_env)
265281
ctx.log(
266282
f"browser_open: Open result: success={result['success']}, stdout={result.get('stdout', '')[:100]}, stderr={result.get('stderr', '')[:100]}"
267283
)
268284
if not result["success"]:
269285
return web.json_response({"success": False, "error": result.get("stderr", "Failed to open URL")})
270286

271287
# Wait for page to fully load
272-
wait_result = await run_browser_cmd_async("wait", "--load", "networkidle", timeout=60)
288+
wait_result = await run_browser_cmd_async("wait", "--load", "networkidle", timeout=AGENT_BROWSER_TIMEOUT)
273289
ctx.log(f"browser_open: Wait result: success={wait_result['success']}")
274290

275291
return web.json_response(
@@ -492,28 +508,31 @@ async def run_script(req):
492508
if not os.path.exists(path):
493509
raise Exception("Script not found")
494510

495-
t0 = time.monotonic()
511+
out_fd, out_path = tempfile.mkstemp(suffix=".out")
512+
err_fd, err_path = tempfile.mkstemp(suffix=".err")
496513
try:
497514
log = _begin_debug_log(f"bash {path}")
498-
proc = await asyncio.create_subprocess_exec(
499-
"bash",
500-
path,
501-
stdout=asyncio.subprocess.PIPE,
502-
stderr=asyncio.subprocess.PIPE,
503-
env={**os.environ, "AGENT_BROWSER_SESSION": "default"},
504-
)
505-
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
515+
with os.fdopen(out_fd, "w") as out_f, os.fdopen(err_fd, "w") as err_f:
516+
proc = await asyncio.create_subprocess_exec(
517+
"bash",
518+
path,
519+
stdout=out_f,
520+
stderr=err_f,
521+
env={**os.environ, "AGENT_BROWSER_SESSION": "default"},
522+
start_new_session=True,
523+
)
524+
await asyncio.wait_for(proc.wait(), timeout=AGENT_BROWSER_TIMEOUT)
506525

507526
result = {
508527
"success": proc.returncode == 0,
509-
"stdout": stdout.decode() if stdout else "",
510-
"stderr": stderr.decode() if stderr else "",
528+
"stdout": Path(out_path).read_text(),
529+
"stderr": Path(err_path).read_text(),
511530
"returncode": proc.returncode,
512531
}
513532
_complete_debug_log(log, result)
533+
success, screenshot_path, snapshot_path = await run_snapshot(req)
514534
return web.json_response(result)
515535
except asyncio.TimeoutError:
516-
proc.kill()
517536
result = {
518537
"success": False,
519538
"error": "Script execution timed out",
@@ -522,11 +541,21 @@ async def run_script(req):
522541
"stderr": "Script execution timed out",
523542
}
524543
_complete_debug_log(log, result)
544+
try:
545+
os.killpg(os.getpgid(proc.pid), 9)
546+
except (ProcessLookupError, OSError):
547+
proc.kill()
548+
success, screenshot_path, snapshot_path = await run_snapshot(req)
525549
return web.json_response({"error": "Script execution timed out"}, status=500)
526550
except Exception as e:
527551
result = {"success": False, "error": str(e), "returncode": -1, "stdout": "", "stderr": str(e)}
528552
_complete_debug_log(log, result)
553+
success, screenshot_path, snapshot_path = await run_snapshot(req)
529554
return web.json_response({"error": str(e)}, status=500)
555+
finally:
556+
for p in (out_path, err_path):
557+
with contextlib.suppress(OSError):
558+
os.unlink(p)
530559

531560
ctx.add_post("/browser/scripts/{name}/run", run_script)
532561

@@ -537,25 +566,29 @@ async def exec_bash(req):
537566
if not content:
538567
raise Exception("content required")
539568

569+
out_fd, out_path = tempfile.mkstemp(suffix=".out")
570+
err_fd, err_path = tempfile.mkstemp(suffix=".err")
540571
try:
541572
log = _begin_debug_log(f"bash -c {content[:100]}")
542-
proc = await asyncio.create_subprocess_exec(
543-
"bash",
544-
"-c",
545-
content,
546-
stdout=asyncio.subprocess.PIPE,
547-
stderr=asyncio.subprocess.PIPE,
548-
env={**os.environ, "AGENT_BROWSER_SESSION": "default"},
549-
)
550-
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
573+
with os.fdopen(out_fd, "w") as out_f, os.fdopen(err_fd, "w") as err_f:
574+
proc = await asyncio.create_subprocess_exec(
575+
"bash",
576+
"-c",
577+
content,
578+
stdout=out_f,
579+
stderr=err_f,
580+
env={**os.environ, "AGENT_BROWSER_SESSION": "default"},
581+
)
582+
await asyncio.wait_for(proc.wait(), timeout=AGENT_BROWSER_TIMEOUT)
551583

552584
result = {
553585
"success": proc.returncode == 0,
554-
"stdout": stdout.decode() if stdout else "",
555-
"stderr": stderr.decode() if stderr else "",
586+
"stdout": Path(out_path).read_text(),
587+
"stderr": Path(err_path).read_text(),
556588
"returncode": proc.returncode,
557589
}
558590
_complete_debug_log(log, result)
591+
success, screenshot_path, snapshot_path = await run_snapshot(req)
559592
return web.json_response(result)
560593
except asyncio.TimeoutError:
561594
proc.kill()
@@ -567,11 +600,17 @@ async def exec_bash(req):
567600
"stderr": "Command execution timed out",
568601
}
569602
_complete_debug_log(log, result)
570-
raise Exception("Command execution timed out")
603+
success, screenshot_path, snapshot_path = await run_snapshot(req)
604+
raise Exception("Command execution timed out") from None
571605
except Exception as e:
572606
result = {"success": False, "error": str(e), "returncode": -1, "stdout": "", "stderr": str(e)}
573607
_complete_debug_log(log, result)
574-
raise Exception(str(e))
608+
success, screenshot_path, snapshot_path = await run_snapshot(req)
609+
raise
610+
finally:
611+
for p in (out_path, err_path):
612+
with contextlib.suppress(OSError):
613+
os.unlink(p)
575614

576615
ctx.add_post("/browser/exec", exec_bash)
577616

0 commit comments

Comments
 (0)