@@ -187,6 +187,9 @@ def __init__(
187187 raise ValueError (
188188 "browserbase_project_id is required for BROWSERBASE env with existing session_id (or set BROWSERBASE_PROJECT_ID in env)."
189189 )
190+
191+ # Register signal handlers for graceful shutdown
192+ self ._register_signal_handlers ()
190193
191194 self ._client : Optional [httpx .AsyncClient ] = (
192195 None # Used for server communication in BROWSERBASE
@@ -213,6 +216,61 @@ def __init__(
213216 ** self .model_client_options ,
214217 )
215218
219+ def _register_signal_handlers (self ):
220+ """Register signal handlers for SIGINT and SIGTERM to ensure proper cleanup."""
221+ def cleanup_handler (sig , frame ):
222+ # Prevent multiple cleanup calls
223+ if self .__class__ ._cleanup_called :
224+ return
225+
226+ self .__class__ ._cleanup_called = True
227+ print (f"\n [{ signal .Signals (sig ).name } ] received. Ending Browserbase session..." )
228+
229+ try :
230+ # Try to get the current event loop
231+ try :
232+ loop = asyncio .get_running_loop ()
233+ except RuntimeError :
234+ # No event loop running - create one to run cleanup
235+ print ("No event loop running, creating one for cleanup..." )
236+ try :
237+ asyncio .run (self ._async_cleanup ())
238+ except Exception as e :
239+ print (f"Error during cleanup: { str (e )} " )
240+ finally :
241+ sys .exit (0 )
242+ return
243+
244+ # Schedule cleanup in the existing event loop
245+ # Use call_soon_threadsafe since signal handlers run in a different thread context
246+ def schedule_cleanup ():
247+ task = asyncio .create_task (self ._async_cleanup ())
248+ # Shield the task to prevent it from being cancelled
249+ shielded = asyncio .shield (task )
250+ # We don't need to await here since we're in call_soon_threadsafe
251+
252+ loop .call_soon_threadsafe (schedule_cleanup )
253+
254+ except Exception as e :
255+ print (f"Error during signal cleanup: { str (e )} " )
256+ sys .exit (1 )
257+
258+ # Register signal handlers
259+ signal .signal (signal .SIGINT , cleanup_handler )
260+ signal .signal (signal .SIGTERM , cleanup_handler )
261+
262+ async def _async_cleanup (self ):
263+ """Async cleanup method called from signal handler."""
264+ try :
265+ await self .close ()
266+ print (f"Session { self .session_id } ended successfully" )
267+ except Exception as e :
268+ print (f"Error ending Browserbase session: { str (e )} " )
269+ finally :
270+ # Force exit after cleanup completes (or fails)
271+ # Use os._exit to avoid any further Python cleanup that might hang
272+ os ._exit (0 )
273+
216274 def start_inference_timer (self ):
217275 """Start timer for tracking inference time."""
218276 self ._inference_start_time = time .time ()
@@ -627,15 +685,12 @@ async def close(self):
627685 self .logger .debug (
628686 f"Attempting to end server session { self .session_id } ..."
629687 )
630- # Use internal client if httpx_client wasn't provided externally
631- client_to_use = (
632- self ._client if not self .httpx_client else self .httpx_client
688+ # Don't use async with here as it might close the client prematurely
689+ # The _execute method will handle the request properly
690+ result = await self ._execute ("end" , {"sessionId" : self .session_id })
691+ self .logger .debug (
692+ f"Server session { self .session_id } ended successfully with result: { result } "
633693 )
634- async with client_to_use : # Ensure client context is managed
635- await self ._execute ("end" , {"sessionId" : self .session_id })
636- self .logger .debug (
637- f"Server session { self .session_id } ended successfully"
638- )
639694 except Exception as e :
640695 # Log error but continue cleanup
641696 self .logger .error (
@@ -684,6 +739,7 @@ async def close(self):
684739 self .logger .error (f"Error stopping Playwright: { str (e )} " )
685740
686741 self ._closed = True
742+ self .logger .debug ("All resources closed successfully" )
687743
688744 async def _create_session (self ):
689745 """
0 commit comments