Skip to content

Commit 7579066

Browse files
committed
fix: Recheck the browser temp directory after each removal attempt
1 parent ef02218 commit 7579066

2 files changed

Lines changed: 23 additions & 13 deletions

File tree

‎src/crawlee/browsers/_playwright_browser.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ async def new_context(self, **context_options: Any) -> BrowserContext:
8888
async def _delete_temp_dir(self, _: BrowserContext | None = None) -> None:
8989
"""Remove the temporary user data directory, retrying until the browser releases its files.
9090
91-
The browser process can keep files in the directory open for a while after the context is closed, which makes
92-
the removal fail on Windows.
91+
Browser helper processes can outlive the context: they keep files in the directory open, which makes the removal
92+
fail on Windows, and they can write into the directory again after it is removed. Each attempt therefore waits
93+
before checking that the directory is gone.
9394
"""
9495
async with self._temp_dir_lock:
9596
temp_dir = self._temp_dir
@@ -100,11 +101,9 @@ async def _delete_temp_dir(self, _: BrowserContext | None = None) -> None:
100101
# One close asks for the removal twice, so the second caller finds nothing left to do.
101102
self._temp_dir = None
102103

103-
for attempt in range(self._TMP_DIR_DELETE_ATTEMPTS):
104-
if attempt:
105-
await asyncio.sleep(self._TMP_DIR_DELETE_INTERVAL.total_seconds())
106-
104+
for _attempt in range(self._TMP_DIR_DELETE_ATTEMPTS):
107105
await asyncio.to_thread(shutil.rmtree, temp_dir, ignore_errors=True)
106+
await asyncio.sleep(self._TMP_DIR_DELETE_INTERVAL.total_seconds())
108107

109108
if not temp_dir.exists():
110109
return

‎tests/unit/browsers/test_playwright_browser.py‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import annotations
22

3+
import asyncio
34
import logging
45
import shutil
56
from datetime import timedelta
67
from pathlib import Path
78
from typing import TYPE_CHECKING, Any
8-
from unittest.mock import Mock
9+
from unittest.mock import AsyncMock, Mock
910

1011
import pytest
1112
from playwright.async_api import async_playwright
@@ -48,9 +49,7 @@ async def test_delete_temp_folder_with_close_browser(playwright: Playwright) ->
4849
assert not current_temp_dir.exists()
4950

5051

51-
async def test_delete_temp_folder_when_files_are_locked(
52-
playwright: Playwright, monkeypatch: pytest.MonkeyPatch
53-
) -> None:
52+
async def test_delete_temp_folder_when_files_are_locked(monkeypatch: pytest.MonkeyPatch) -> None:
5453
"""The temp directory is removed even when the first delete attempts fail, as Windows locks the browser files."""
5554
monkeypatch.setattr(PlaywrightPersistentBrowser, '_TMP_DIR_DELETE_INTERVAL', timedelta(0))
5655

@@ -66,14 +65,26 @@ def rmtree_locked_at_first(path: Any, **kwargs: Any) -> None:
6665
rmtree.side_effect = rmtree_locked_at_first
6766
monkeypatch.setattr(shutil, 'rmtree', rmtree)
6867

69-
persist_browser = PlaywrightPersistentBrowser(
70-
playwright.chromium, user_data_dir=None, browser_launch_options={'headless': True}
71-
)
68+
# A real browser on Windows can hold the files longer than the whole retry budget, so a fake context stands in.
69+
# Like Playwright, it runs the `close` listener as a separate task.
70+
context = Mock()
71+
listener_tasks = list[asyncio.Task]()
72+
73+
async def close_context() -> None:
74+
listener = context.on.call_args.args[1]
75+
listener_tasks.append(asyncio.create_task(listener(context)))
76+
77+
context.close = close_context
78+
browser_type = Mock()
79+
browser_type.launch_persistent_context = AsyncMock(return_value=context)
80+
81+
persist_browser = PlaywrightPersistentBrowser(browser_type, user_data_dir=None, browser_launch_options={})
7282
await persist_browser.new_context()
7383
assert isinstance(persist_browser._temp_dir, Path)
7484
current_temp_dir = persist_browser._temp_dir
7585
assert current_temp_dir.exists()
7686
await persist_browser.close()
87+
await asyncio.gather(*listener_tasks)
7788
# The context's `close` event and `close` itself both ask for the removal, but only one of them retries.
7889
assert rmtree.call_count == locked_attempts + 1
7990
assert not current_temp_dir.exists()

0 commit comments

Comments
 (0)