Skip to content

Commit f09623d

Browse files
authored
Merge pull request #20 from panel-extensions/fix/screenshot-browser-install
Install Chromium via `pls install-browser` (pixi/uv/pip)
2 parents 85d92a8 + f4a7e62 commit f09623d

5 files changed

Lines changed: 103 additions & 3 deletions

File tree

docs/tutorials/installation.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ terminal. By the end, `pls --version` will print the installed version.
2727
pixi add --pypi "panel-live-server[pydata]"
2828
```
2929

30+
Install the browser the `screenshot` tool needs:
31+
32+
```bash
33+
pixi run pls install-browser
34+
```
35+
3036
Find the `pls` path:
3137

3238
**macOS / Linux:**
@@ -51,6 +57,12 @@ terminal. By the end, `pls --version` will print the installed version.
5157
uv tool install "panel-live-server[pydata]"
5258
```
5359

60+
Install the browser the `screenshot` tool needs:
61+
62+
```bash
63+
pls install-browser
64+
```
65+
5466
Find the `pls` path:
5567

5668
**macOS / Linux:**
@@ -98,6 +110,12 @@ terminal. By the end, `pls --version` will print the installed version.
98110
pip install "panel-live-server[pydata]"
99111
```
100112

113+
Install the browser the `screenshot` tool needs:
114+
115+
```bash
116+
pls install-browser
117+
```
118+
101119
Find the `pls` path:
102120

103121
**macOS / Linux:**

docs/tutorials/mcp-server.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,17 @@ The server runs in an isolated uv tool environment. Install missing packages as
187187

188188
### `screenshot` fails with a Playwright error
189189

190-
The `screenshot` tool needs the Chromium browser that Playwright manages. Install it once with:
190+
The `screenshot` tool needs the Chromium browser that Playwright manages, which is
191+
not installed automatically. Install it once with:
191192

192193
```bash
193-
playwright install chromium
194+
pls install-browser
194195
```
195196

197+
This downloads Chromium into the same environment that runs `pls`. See
198+
[Installation → Enable the screenshot tool](installation.md#enable-the-screenshot-tool)
199+
for the per-installer command.
200+
196201
---
197202

198203
## What You've Learned

src/panel_live_server/cli.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,29 @@ def list_packages(
271271
typer.echo(f"{name:<{name_width}} {version}")
272272

273273

274+
@app.command(name="install-browser")
275+
def install_browser() -> None:
276+
"""Download the Chromium browser the `screenshot` MCP tool needs.
277+
278+
Playwright ships its browser binary separately from the Python package, so a
279+
`pip` or `uv` install does not fetch it automatically. Run this once after
280+
installing (pixi users get it via `pixi run postinstall`). It lands in the
281+
same environment that runs `pls`.
282+
"""
283+
from panel_live_server.screenshot import install_browser as _install_browser
284+
285+
typer.echo("Installing Chromium for the screenshot tool (one-time)...")
286+
code = _install_browser()
287+
if code == 0:
288+
typer.echo("Done — the screenshot tool is ready.")
289+
else:
290+
typer.echo(
291+
"Browser install failed. Try manually: python -m playwright install chromium",
292+
err=True,
293+
)
294+
raise typer.Exit(code)
295+
296+
274297
def main() -> None:
275298
"""Entry point for the pls command."""
276299
app()

src/panel_live_server/screenshot.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import asyncio
1414
import logging
15+
import os
16+
import sys
1517

1618
logger = logging.getLogger(__name__)
1719

@@ -20,7 +22,46 @@ class PlaywrightUnavailableError(RuntimeError):
2022
"""Raised when Playwright or its browser is not installed/launchable."""
2123

2224

23-
_INSTALL_HINT = "Playwright Chromium is not installed. Run:\n" " playwright install chromium"
25+
_INSTALL_HINT = "Playwright's Chromium browser is not installed. Run:\n pls install-browser"
26+
27+
28+
def install_browser() -> int:
29+
"""Download the headless Chromium browser the screenshot tool needs.
30+
31+
Playwright ships its browser binary separately from the Python package, so
32+
``pip``/``uv`` installs do not fetch it automatically. This shells out to
33+
``<this-interpreter> -m playwright install chromium`` so the browser always
34+
lands in the same environment that is running ``pls`` — avoiding the common
35+
trap where the binary is installed under a different interpreter.
36+
37+
Returns
38+
-------
39+
int
40+
The installer subprocess exit code (``0`` on success).
41+
"""
42+
import subprocess
43+
44+
return subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"]).returncode
45+
46+
47+
def is_browser_installed() -> bool:
48+
"""Return ``True`` if the Chromium binary Playwright needs is present.
49+
50+
This is a cheap check — it does not launch a browser. It uses Playwright's
51+
sync API, so call it from a worker thread (e.g. ``asyncio.to_thread``), not
52+
directly inside a running event loop.
53+
"""
54+
try:
55+
from playwright.sync_api import sync_playwright
56+
except ImportError:
57+
return False
58+
try:
59+
with sync_playwright() as p:
60+
path = p.chromium.executable_path
61+
return bool(path) and os.path.exists(path)
62+
except Exception:
63+
return False
64+
2465

2566
# Best-effort wait for Panel/Bokeh content to mount before capturing.
2667
_CONTENT_SELECTOR = "canvas, .bk-Row, .bk-Column, .bk, .markdown, table, img, svg"

src/panel_live_server/server.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,19 @@ async def app_lifespan(app):
262262
else:
263263
logger.warning("Panel Live Server failed to start - show tool will not work")
264264

265+
# Warn early if the screenshot browser is missing, so users find out now
266+
# rather than mid-screenshot. The check uses Playwright's sync API, so run
267+
# it in a worker thread to stay off the event loop.
268+
try:
269+
from panel_live_server.screenshot import is_browser_installed
270+
271+
if not await asyncio.to_thread(is_browser_installed):
272+
msg = "The `screenshot` tool needs Chromium, which is not installed. Run `pls install-browser` to enable it."
273+
print(f"\n {msg}\n", file=sys.stderr, flush=True) # noqa: T201
274+
logger.warning(msg)
275+
except Exception:
276+
logger.debug("Could not check screenshot browser availability", exc_info=True)
277+
265278
try:
266279
yield
267280
finally:

0 commit comments

Comments
 (0)