3
3
import os
4
4
import shutil
5
5
import tempfile
6
+ import signal
7
+ import sys
6
8
import time
7
9
from pathlib import Path
8
10
from typing import Any , Literal , Optional
@@ -45,6 +47,9 @@ class Stagehand:
45
47
46
48
# Dictionary to store one lock per session_id
47
49
_session_locks = {}
50
+
51
+ # Flag to track if cleanup has been called
52
+ _cleanup_called = False
48
53
49
54
def __init__ (
50
55
self ,
@@ -188,6 +193,9 @@ def __init__(
188
193
raise ValueError (
189
194
"browserbase_project_id is required for BROWSERBASE env with existing session_id (or set BROWSERBASE_PROJECT_ID in env)."
190
195
)
196
+
197
+ # Register signal handlers for graceful shutdown
198
+ self ._register_signal_handlers ()
191
199
192
200
self ._client : Optional [httpx .AsyncClient ] = (
193
201
None # Used for server communication in BROWSERBASE
@@ -214,6 +222,61 @@ def __init__(
214
222
** self .model_client_options ,
215
223
)
216
224
225
+ def _register_signal_handlers (self ):
226
+ """Register signal handlers for SIGINT and SIGTERM to ensure proper cleanup."""
227
+ def cleanup_handler (sig , frame ):
228
+ # Prevent multiple cleanup calls
229
+ if self .__class__ ._cleanup_called :
230
+ return
231
+
232
+ self .__class__ ._cleanup_called = True
233
+ print (f"\n [{ signal .Signals (sig ).name } ] received. Ending Browserbase session..." )
234
+
235
+ try :
236
+ # Try to get the current event loop
237
+ try :
238
+ loop = asyncio .get_running_loop ()
239
+ except RuntimeError :
240
+ # No event loop running - create one to run cleanup
241
+ print ("No event loop running, creating one for cleanup..." )
242
+ try :
243
+ asyncio .run (self ._async_cleanup ())
244
+ except Exception as e :
245
+ print (f"Error during cleanup: { str (e )} " )
246
+ finally :
247
+ sys .exit (0 )
248
+ return
249
+
250
+ # Schedule cleanup in the existing event loop
251
+ # Use call_soon_threadsafe since signal handlers run in a different thread context
252
+ def schedule_cleanup ():
253
+ task = asyncio .create_task (self ._async_cleanup ())
254
+ # Shield the task to prevent it from being cancelled
255
+ shielded = asyncio .shield (task )
256
+ # We don't need to await here since we're in call_soon_threadsafe
257
+
258
+ loop .call_soon_threadsafe (schedule_cleanup )
259
+
260
+ except Exception as e :
261
+ print (f"Error during signal cleanup: { str (e )} " )
262
+ sys .exit (1 )
263
+
264
+ # Register signal handlers
265
+ signal .signal (signal .SIGINT , cleanup_handler )
266
+ signal .signal (signal .SIGTERM , cleanup_handler )
267
+
268
+ async def _async_cleanup (self ):
269
+ """Async cleanup method called from signal handler."""
270
+ try :
271
+ await self .close ()
272
+ print (f"Session { self .session_id } ended successfully" )
273
+ except Exception as e :
274
+ print (f"Error ending Browserbase session: { str (e )} " )
275
+ finally :
276
+ # Force exit after cleanup completes (or fails)
277
+ # Use os._exit to avoid any further Python cleanup that might hang
278
+ os ._exit (0 )
279
+
217
280
def start_inference_timer (self ):
218
281
"""Start timer for tracking inference time."""
219
282
self ._inference_start_time = time .time ()
@@ -628,15 +691,12 @@ async def close(self):
628
691
self .logger .debug (
629
692
f"Attempting to end server session { self .session_id } ..."
630
693
)
631
- # Use internal client if httpx_client wasn't provided externally
632
- client_to_use = (
633
- self ._client if not self .httpx_client else self .httpx_client
694
+ # Don't use async with here as it might close the client prematurely
695
+ # The _execute method will handle the request properly
696
+ result = await self ._execute ("end" , {"sessionId" : self .session_id })
697
+ self .logger .debug (
698
+ f"Server session { self .session_id } ended successfully with result: { result } "
634
699
)
635
- async with client_to_use : # Ensure client context is managed
636
- await self ._execute ("end" , {"sessionId" : self .session_id })
637
- self .logger .debug (
638
- f"Server session { self .session_id } ended successfully"
639
- )
640
700
except Exception as e :
641
701
# Log error but continue cleanup
642
702
self .logger .error (
@@ -685,6 +745,7 @@ async def close(self):
685
745
self .logger .error (f"Error stopping Playwright: { str (e )} " )
686
746
687
747
self ._closed = True
748
+ self .logger .debug ("All resources closed successfully" )
688
749
689
750
async def _create_session (self ):
690
751
"""
0 commit comments