Skip to content

Commit f71d1b1

Browse files
dex0shubhamclaude
andauthored
fix(render): isolate Chrome --user-data-dir per worker (fixes #54) (#65)
Both CDP backends launched Chrome with --remote-debugging-port but no --user-data-dir. On a machine that already has Chrome open (normal on a desktop), the new invocation forwards to the running instance on the default profile instead of starting its own headless renderer: the CDP endpoint comes up and trivial Runtime.evaluate works, but Page.navigate never commits and Page.captureScreenshot hangs forever — every URL fails with done=0 after a ~180s timeout. This is the second of the two root causes in #54 (the first, picking a non-page CDP target, was fixed in #56). It's masked on Linux/CI because the bundled headless_shell launches fresh with no competing instance. Give each worker an isolated, throwaway profile via tempfile.mkdtemp, pass it as --user-data-dir, and remove it on teardown (including the connect-failure path in the turbo backend). A unique profile per worker also prevents parallel workers from colliding on one profile. Also document cross-platform Chrome resolution in the README (headless_shell is linux-x64 only; Windows/macOS use auto-detected system Chrome or CHROME_PATH). Verified on Windows 11 with system Chrome 149: pixelshot https://example.com now returns done=1 failed=0 with a valid tile (was a ~180s failure); temp profiles are cleaned up; existing render/chrome tests pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5756fe7 commit f71d1b1

3 files changed

Lines changed: 43 additions & 7 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ pixelshot paper.pdf -o ./tiles --dpi 200
230230
pixelshot https://github.com/StarTrail-org/PixelRAG paper.pdf -o ./tiles
231231
```
232232

233+
> **Chrome on Windows/macOS** — the bundled turbo `headless_shell` auto-installs on
234+
> **linux-x64** only. Elsewhere, `pixelshot` uses your system Chrome/Chromium (or
235+
> Playwright's Chromium), auto-detected from the standard install locations. Point it at
236+
> a specific binary with `CHROME_PATH=/path/to/chrome` if it isn't found automatically.
237+
> Each render runs in an isolated, throwaway Chrome profile, so it works even while you
238+
> have Chrome open.
239+
233240
### Embed tools (standalone)
234241

235242
Each stage runs independently, without the orchestrator:

render/src/pixelrag_render/backends/cdp.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
import io
2525
import json
2626
import logging
27+
import shutil
2728
import signal
2829
import subprocess
30+
import tempfile
2931
import time
3032
import urllib.request
3133
from pathlib import Path
@@ -314,11 +316,21 @@ async def _worker(
314316
results: list,
315317
):
316318
"""Async worker: owns a Chrome process, pulls URLs from queue."""
319+
# Isolated profile per worker. Without --user-data-dir, a launch on a machine that
320+
# already has Chrome open forwards to the running instance (default profile) instead
321+
# of starting this headless renderer — navigation/screenshot then hang forever. A
322+
# unique dir also stops parallel workers from colliding on one profile. See issue #54.
323+
user_data_dir = tempfile.mkdtemp(prefix=f"pixelshot_chrome_{port}_")
317324
proc = subprocess.Popen(
318325
# `--headless=new`: the bare `--headless` is deprecated and hangs on modern
319326
# Chrome (e.g. google-chrome 149); `=new` works on both stock Chrome and the
320327
# patched headless_shell.
321-
[chrome_path, f"--remote-debugging-port={port}", "--headless=new"]
328+
[
329+
chrome_path,
330+
f"--remote-debugging-port={port}",
331+
"--headless=new",
332+
f"--user-data-dir={user_data_dir}",
333+
]
322334
+ BROWSER_ARGS
323335
+ ["about:blank"],
324336
stdout=subprocess.DEVNULL,
@@ -388,6 +400,7 @@ async def _worker(
388400
proc.wait(timeout=5)
389401
except subprocess.TimeoutExpired:
390402
proc.kill()
403+
shutil.rmtree(user_data_dir, ignore_errors=True)
391404

392405

393406
def _derive_stems(urls: list[str], stems: list[str] | None) -> list[str]:

render/src/pixelrag_render/backends/fast_cdp.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
import json
2626
import logging
2727
import os
28+
import shutil
2829
import signal
2930
import struct
3031
import subprocess
32+
import tempfile
3133
import time
3234
import urllib.request
3335
from pathlib import Path
@@ -115,12 +117,22 @@ def _next_base_port() -> int:
115117

116118

117119
async def _launch_chrome(chrome_path: str, port: int) -> tuple:
118-
"""Launch a headless Chrome and return (websocket, proc)."""
120+
"""Launch a headless Chrome and return (websocket, proc, user_data_dir)."""
119121
import websockets
120122

123+
# Isolated profile per worker. Without --user-data-dir, a launch on a machine that
124+
# already has Chrome open forwards to the running instance (default profile) instead
125+
# of starting this headless renderer — navigation/screenshot then hang forever. A
126+
# unique dir also stops parallel workers from colliding on one profile. See issue #54.
127+
user_data_dir = tempfile.mkdtemp(prefix=f"pixelshot_chrome_{port}_")
121128
args = (
122129
# `--headless=new`: bare `--headless` is deprecated and hangs on modern Chrome.
123-
[chrome_path, f"--remote-debugging-port={port}", "--headless=new"]
130+
[
131+
chrome_path,
132+
f"--remote-debugging-port={port}",
133+
"--headless=new",
134+
f"--user-data-dir={user_data_dir}",
135+
]
124136
+ CHROME_ARGS
125137
+ ["about:blank"]
126138
)
@@ -142,19 +154,21 @@ async def _launch_chrome(chrome_path: str, port: int) -> tuple:
142154
open_timeout=10,
143155
max_size=50 * 1024 * 1024,
144156
)
145-
return ws, proc
157+
return ws, proc, user_data_dir
146158
except Exception:
147159
if attempt == 9:
148160
proc.kill()
161+
shutil.rmtree(user_data_dir, ignore_errors=True)
149162
raise ConnectionError(f"Failed to connect to Chrome on port {port}")
150163

151164

152165
class _Conn:
153166
"""Minimal CDP connection with a receive loop."""
154167

155-
def __init__(self, ws, proc):
168+
def __init__(self, ws, proc, user_data_dir=None):
156169
self._ws = ws
157170
self._proc = proc
171+
self._user_data_dir = user_data_dir
158172
self._msg_id = 0
159173
self._pending: dict[int, asyncio.Future] = {}
160174
self._event_listeners: dict[str, list] = {}
@@ -238,6 +252,8 @@ async def close(self):
238252
self._proc.wait(timeout=5)
239253
except subprocess.TimeoutExpired:
240254
self._proc.kill()
255+
if self._user_data_dir:
256+
shutil.rmtree(self._user_data_dir, ignore_errors=True)
241257

242258

243259
# ---------------------------------------------------------------------------
@@ -328,8 +344,8 @@ def _compressor_thread():
328344
base_port + n_workers - 1,
329345
)
330346
for i in range(n_workers):
331-
ws, proc = await _launch_chrome(chrome_path, base_port + i)
332-
conn = _Conn(ws, proc)
347+
ws, proc, user_data_dir = await _launch_chrome(chrome_path, base_port + i)
348+
conn = _Conn(ws, proc, user_data_dir)
333349
connections.append(conn)
334350

335351
for i, conn in enumerate(connections):

0 commit comments

Comments
 (0)