Skip to content

Commit 12b16c6

Browse files
committed
Merge branch 'main' into miguel/stg-469-abstract-browser-and-api-functionality-from-client
2 parents 66becd5 + 640c790 commit 12b16c6

File tree

1 file changed

+70
-8
lines changed

1 file changed

+70
-8
lines changed

stagehand/client.py

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import asyncio
22
import os
3+
import shutil
4+
import tempfile
5+
import signal
6+
import sys
37
import time
48
from pathlib import Path
59
from 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

Comments
 (0)