Skip to content

Commit 932e745

Browse files
revert
1 parent 246ba1e commit 932e745

File tree

2 files changed

+63
-49
lines changed

2 files changed

+63
-49
lines changed

stagehand/main.py

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -391,9 +391,9 @@ async def init(self):
391391
self.logger.debug("Initializing Stagehand...")
392392
self.logger.debug(f"Environment: {self.env}")
393393

394-
# Always initialize playwright in a thread to avoid event loop conflicts
394+
# Always initialize playwright with timeout to avoid hanging
395395
# This ensures compatibility with strict event loop environments like Langgraph
396-
self._playwright = await self._init_playwright_in_thread()
396+
self._playwright = await self._init_playwright_with_timeout()
397397

398398
if self.env == "BROWSERBASE":
399399
# Create session if we don't have one
@@ -452,42 +452,38 @@ async def init(self):
452452

453453
self._initialized = True
454454

455-
async def _init_playwright_in_thread(self):
455+
async def _init_playwright_with_timeout(self):
456456
"""
457-
Initialize playwright using asyncio.to_thread to avoid blocking the main event loop.
457+
Initialize playwright with a timeout to avoid hanging in strict event loop environments.
458458
459-
This method runs the potentially blocking async_playwright().start() in a thread
460-
to avoid conflicts with strict event loop environments like Langgraph.
459+
This method adds a timeout to the regular async_playwright().start() to prevent
460+
hanging in environments like Langgraph that restrict blocking operations.
461461
"""
462-
self.logger.debug("Starting playwright initialization in background thread...")
463-
464-
def _start_playwright_blocking():
465-
"""Start playwright in a blocking manner - will be run in a thread."""
466-
import asyncio
467-
from playwright.async_api import async_playwright
468-
469-
# Create a new event loop for this thread
470-
loop = asyncio.new_event_loop()
471-
asyncio.set_event_loop(loop)
472-
473-
try:
474-
# Run the async playwright start in this loop
475-
return loop.run_until_complete(async_playwright().start())
476-
finally:
477-
loop.close()
462+
self.logger.debug("Starting playwright initialization with timeout...")
478463

479464
try:
480-
# Use asyncio.to_thread to run the blocking initialization
481-
playwright_instance = await asyncio.to_thread(_start_playwright_blocking)
465+
# Use asyncio.wait_for to add a timeout to prevent hanging
466+
# If the environment doesn't allow blocking operations, this will fail fast
467+
playwright_instance = await asyncio.wait_for(
468+
async_playwright().start(),
469+
timeout=30.0 # 30 second timeout
470+
)
482471

483-
self.logger.debug("Playwright initialized successfully in background thread")
472+
self.logger.debug("Playwright initialized successfully")
484473
return playwright_instance
485474

475+
except asyncio.TimeoutError:
476+
self.logger.error("Playwright initialization timed out")
477+
raise RuntimeError(
478+
"Playwright initialization timed out after 30 seconds. This may indicate "
479+
"your environment has strict event loop restrictions. If using Langgraph, "
480+
"consider using the --allow-blocking flag."
481+
)
486482
except Exception as e:
487-
self.logger.error(f"Failed to initialize playwright in background thread: {e}")
483+
self.logger.error(f"Failed to initialize playwright: {e}")
488484
raise RuntimeError(
489-
"Failed to initialize Playwright in background thread. This may indicate a "
490-
"deeper compatibility issue with your environment."
485+
"Failed to initialize Playwright. This may indicate your environment "
486+
"has restrictions on subprocess creation or event loop operations."
491487
) from e
492488

493489
def agent(self, **kwargs) -> Agent:

tests/unit/test_client_initialization.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -205,61 +205,79 @@ async def mock_create_session():
205205
await client._create_session()
206206

207207
@pytest.mark.asyncio
208-
@mock.patch("asyncio.to_thread")
209-
async def test_init_playwright_in_thread(self, mock_to_thread):
210-
"""Test that playwright initialization works properly using asyncio.to_thread."""
208+
@mock.patch("stagehand.main.async_playwright")
209+
async def test_init_playwright_with_timeout(self, mock_async_playwright):
210+
"""Test that playwright initialization works properly with timeout."""
211211
# Create a mock playwright instance
212212
mock_playwright_instance = mock.AsyncMock()
213213
mock_playwright_instance.stop = mock.AsyncMock()
214214
mock_playwright_instance.chromium = mock.MagicMock()
215215
mock_playwright_instance.firefox = mock.MagicMock()
216216
mock_playwright_instance.webkit = mock.MagicMock()
217217

218-
# Mock asyncio.to_thread to return our mock instance
219-
mock_to_thread.return_value = mock_playwright_instance
218+
# Mock async_playwright().start() to return our mock instance as an awaitable
219+
async def mock_start():
220+
return mock_playwright_instance
221+
222+
mock_async_playwright.return_value.start = mock_start
220223

221224
# Create a Stagehand client with LOCAL env
222225
config = StagehandConfig(env="LOCAL")
223226
client = Stagehand(config=config)
224227

225-
# Test the threaded playwright initialization
226-
result = await client._init_playwright_in_thread()
228+
# Test the playwright initialization with timeout
229+
result = await client._init_playwright_with_timeout()
227230

228231
# Verify that the playwright instance was returned
229232
assert result is mock_playwright_instance
230233

231-
# Verify that asyncio.to_thread was called
232-
mock_to_thread.assert_called_once()
233-
234234
# Verify the result has the expected attributes
235235
assert hasattr(result, 'chromium')
236236
assert hasattr(result, 'firefox')
237237
assert hasattr(result, 'webkit')
238238
assert hasattr(result, 'stop')
239239

240240
@pytest.mark.asyncio
241-
@mock.patch("asyncio.to_thread")
242-
async def test_init_playwright_in_thread_handles_exceptions(self, mock_to_thread):
243-
"""Test that threaded playwright initialization properly handles exceptions."""
244-
# Mock asyncio.to_thread to raise an exception
245-
mock_to_thread.side_effect = Exception("Test exception")
241+
@mock.patch("stagehand.main.async_playwright")
242+
async def test_init_playwright_with_timeout_handles_exceptions(self, mock_async_playwright):
243+
"""Test that playwright initialization properly handles exceptions."""
244+
# Mock async_playwright().start() to raise an exception as an awaitable
245+
async def mock_start():
246+
raise Exception("Test exception")
247+
248+
mock_async_playwright.return_value.start = mock_start
246249

247250
# Create a Stagehand client with LOCAL env
248251
config = StagehandConfig(env="LOCAL")
249252
client = Stagehand(config=config)
250253

251254
# Test that the method raises a RuntimeError with our exception message
252-
with pytest.raises(RuntimeError, match="Failed to initialize Playwright in background thread"):
253-
await client._init_playwright_in_thread()
255+
with pytest.raises(RuntimeError, match="Failed to initialize Playwright"):
256+
await client._init_playwright_with_timeout()
257+
258+
@pytest.mark.asyncio
259+
@mock.patch("stagehand.main.asyncio.wait_for")
260+
async def test_init_playwright_with_timeout_handles_timeout(self, mock_wait_for):
261+
"""Test that playwright initialization properly handles timeouts."""
262+
# Mock asyncio.wait_for to raise a TimeoutError
263+
mock_wait_for.side_effect = asyncio.TimeoutError()
264+
265+
# Create a Stagehand client with LOCAL env
266+
config = StagehandConfig(env="LOCAL")
267+
client = Stagehand(config=config)
268+
269+
# Test that the method raises a RuntimeError with timeout message
270+
with pytest.raises(RuntimeError, match="Playwright initialization timed out"):
271+
await client._init_playwright_with_timeout()
254272

255273
@pytest.mark.asyncio
256274
@mock.patch("stagehand.main.cleanup_browser_resources")
257275
@mock.patch("stagehand.main.connect_local_browser")
258-
@mock.patch.object(Stagehand, "_init_playwright_in_thread")
259-
async def test_init_uses_threaded_playwright(
276+
@mock.patch.object(Stagehand, "_init_playwright_with_timeout")
277+
async def test_init_uses_playwright_with_timeout(
260278
self, mock_init_playwright, mock_connect_local, mock_cleanup
261279
):
262-
"""Test that the main init() method uses threaded playwright initialization."""
280+
"""Test that the main init() method uses playwright initialization with timeout."""
263281
# Set up mocks
264282
mock_playwright_instance = mock.AsyncMock()
265283
mock_init_playwright.return_value = mock_playwright_instance
@@ -286,7 +304,7 @@ async def test_init_uses_threaded_playwright(
286304
# Initialize the client
287305
await client.init()
288306

289-
# Verify that threaded playwright initialization was called
307+
# Verify that playwright initialization with timeout was called
290308
mock_init_playwright.assert_called_once()
291309

292310
# Verify that the client is properly initialized

0 commit comments

Comments
 (0)