11import asyncio
22import os
3+ import shutil
4+ import tempfile
5+ import signal
6+ import sys
37import time
48from pathlib import Path
59from typing import Any , Literal , Optional
@@ -46,6 +50,9 @@ class Stagehand:
4650
4751 # Dictionary to store one lock per session_id
4852 _session_locks = {}
53+
54+ # Flag to track if cleanup has been called
55+ _cleanup_called = False
4956
5057 def __init__ (
5158 self ,
@@ -189,6 +196,9 @@ def __init__(
189196 raise ValueError (
190197 "browserbase_project_id is required for BROWSERBASE env with existing session_id (or set BROWSERBASE_PROJECT_ID in env)."
191198 )
199+
200+ # Register signal handlers for graceful shutdown
201+ self ._register_signal_handlers ()
192202
193203 self ._client : Optional [httpx .AsyncClient ] = (
194204 None # Used for server communication in BROWSERBASE
@@ -215,6 +225,61 @@ def __init__(
215225 ** self .model_client_options ,
216226 )
217227
228+ def _register_signal_handlers (self ):
229+ """Register signal handlers for SIGINT and SIGTERM to ensure proper cleanup."""
230+ def cleanup_handler (sig , frame ):
231+ # Prevent multiple cleanup calls
232+ if self .__class__ ._cleanup_called :
233+ return
234+
235+ self .__class__ ._cleanup_called = True
236+ print (f"\n [{ signal .Signals (sig ).name } ] received. Ending Browserbase session..." )
237+
238+ try :
239+ # Try to get the current event loop
240+ try :
241+ loop = asyncio .get_running_loop ()
242+ except RuntimeError :
243+ # No event loop running - create one to run cleanup
244+ print ("No event loop running, creating one for cleanup..." )
245+ try :
246+ asyncio .run (self ._async_cleanup ())
247+ except Exception as e :
248+ print (f"Error during cleanup: { str (e )} " )
249+ finally :
250+ sys .exit (0 )
251+ return
252+
253+ # Schedule cleanup in the existing event loop
254+ # Use call_soon_threadsafe since signal handlers run in a different thread context
255+ def schedule_cleanup ():
256+ task = asyncio .create_task (self ._async_cleanup ())
257+ # Shield the task to prevent it from being cancelled
258+ shielded = asyncio .shield (task )
259+ # We don't need to await here since we're in call_soon_threadsafe
260+
261+ loop .call_soon_threadsafe (schedule_cleanup )
262+
263+ except Exception as e :
264+ print (f"Error during signal cleanup: { str (e )} " )
265+ sys .exit (1 )
266+
267+ # Register signal handlers
268+ signal .signal (signal .SIGINT , cleanup_handler )
269+ signal .signal (signal .SIGTERM , cleanup_handler )
270+
271+ async def _async_cleanup (self ):
272+ """Async cleanup method called from signal handler."""
273+ try :
274+ await self .close ()
275+ print (f"Session { self .session_id } ended successfully" )
276+ except Exception as e :
277+ print (f"Error ending Browserbase session: { str (e )} " )
278+ finally :
279+ # Force exit after cleanup completes (or fails)
280+ # Use os._exit to avoid any further Python cleanup that might hang
281+ os ._exit (0 )
282+
218283 def start_inference_timer (self ):
219284 """Start timer for tracking inference time."""
220285 self ._inference_start_time = time .time ()
@@ -458,15 +523,12 @@ async def close(self):
458523 self .logger .debug (
459524 f"Attempting to end server session { self .session_id } ..."
460525 )
461- # Use internal client if httpx_client wasn't provided externally
462- client_to_use = (
463- self ._client if not self .httpx_client else self .httpx_client
526+ # Don't use async with here as it might close the client prematurely
527+ # The _execute method will handle the request properly
528+ result = await self ._execute ("end" , {"sessionId" : self .session_id })
529+ self .logger .debug (
530+ f"Server session { self .session_id } ended successfully with result: { result } "
464531 )
465- async with client_to_use : # Ensure client context is managed
466- await self ._execute ("end" , {"sessionId" : self .session_id })
467- self .logger .debug (
468- f"Server session { self .session_id } ended successfully"
469- )
470532 except Exception as e :
471533 # Log error but continue cleanup
472534 self .logger .error (
0 commit comments