11import asyncio
2+ import contextlib
23import json
34import os
45import shutil
6+ import tempfile
57import time
68from collections import deque
79from pathlib import Path
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 ))
1619DEBUG_LOG = deque (maxlen = 200 )
1720DEBUG_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