@@ -187,6 +187,9 @@ def __init__(
187
187
raise ValueError (
188
188
"browserbase_project_id is required for BROWSERBASE env with existing session_id (or set BROWSERBASE_PROJECT_ID in env)."
189
189
)
190
+
191
+ # Register signal handlers for graceful shutdown
192
+ self ._register_signal_handlers ()
190
193
191
194
self ._client : Optional [httpx .AsyncClient ] = (
192
195
None # Used for server communication in BROWSERBASE
@@ -213,6 +216,61 @@ def __init__(
213
216
** self .model_client_options ,
214
217
)
215
218
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
+
216
274
def start_inference_timer (self ):
217
275
"""Start timer for tracking inference time."""
218
276
self ._inference_start_time = time .time ()
@@ -627,15 +685,12 @@ async def close(self):
627
685
self .logger .debug (
628
686
f"Attempting to end server session { self .session_id } ..."
629
687
)
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 } "
633
693
)
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
- )
639
694
except Exception as e :
640
695
# Log error but continue cleanup
641
696
self .logger .error (
@@ -684,6 +739,7 @@ async def close(self):
684
739
self .logger .error (f"Error stopping Playwright: { str (e )} " )
685
740
686
741
self ._closed = True
742
+ self .logger .debug ("All resources closed successfully" )
687
743
688
744
async def _create_session (self ):
689
745
"""
0 commit comments