Skip to content

Commit 640c790

Browse files
authored
Merge pull request #17 from browserbase/miguel/stg-173-on-session-interrupt-send-end
interrupts enabled
2 parents aa178e7 + b191579 commit 640c790

File tree

1 file changed

+69
-8
lines changed

1 file changed

+69
-8
lines changed

stagehand/client.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os
44
import shutil
55
import tempfile
6+
import signal
7+
import sys
68
import time
79
from pathlib import Path
810
from typing import Any, Literal, Optional
@@ -45,6 +47,9 @@ class Stagehand:
4547

4648
# Dictionary to store one lock per session_id
4749
_session_locks = {}
50+
51+
# Flag to track if cleanup has been called
52+
_cleanup_called = False
4853

4954
def __init__(
5055
self,
@@ -188,6 +193,9 @@ def __init__(
188193
raise ValueError(
189194
"browserbase_project_id is required for BROWSERBASE env with existing session_id (or set BROWSERBASE_PROJECT_ID in env)."
190195
)
196+
197+
# Register signal handlers for graceful shutdown
198+
self._register_signal_handlers()
191199

192200
self._client: Optional[httpx.AsyncClient] = (
193201
None # Used for server communication in BROWSERBASE
@@ -214,6 +222,61 @@ def __init__(
214222
**self.model_client_options,
215223
)
216224

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+
217280
def start_inference_timer(self):
218281
"""Start timer for tracking inference time."""
219282
self._inference_start_time = time.time()
@@ -628,15 +691,12 @@ async def close(self):
628691
self.logger.debug(
629692
f"Attempting to end server session {self.session_id}..."
630693
)
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}"
634699
)
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-
)
640700
except Exception as e:
641701
# Log error but continue cleanup
642702
self.logger.error(
@@ -685,6 +745,7 @@ async def close(self):
685745
self.logger.error(f"Error stopping Playwright: {str(e)}")
686746

687747
self._closed = True
748+
self.logger.debug("All resources closed successfully")
688749

689750
async def _create_session(self):
690751
"""

0 commit comments

Comments
 (0)