dsh-browser-install: auto-install Playwright Chromium and bridge it for dsh-ego-browser
On load, ensures the Playwright browser target (default: chromium) is installed via `npx --yes playwright install chromium --with-deps` (background when missing, so profile startup is not blocked), then symlinks the installed binary at /usr/local/bin/google-chrome-stable (+ chromium). That exact name is what dsh-ego-browser's PATH scan and its bundled --no-sandbox wrapper both look for, so root/Docker containers get a working agent browser with zero manual PATH, env-var or wrapper steps. Pre-existing user binaries/wrappers are never clobbered. Ships as a dsh bundle (plain ESM, no build step); install via `dsh plugin --profile web add github:<you>/dsh-browser-install#main` and update with `dsh plugin --profile web update` (branch ref resolves the tip).
This commit is contained in:
commit
d2e2eaed8d
5 files changed
+520
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
pnpm-lock.yaml
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# dsh-browser-install
|
||||||
|
|
||||||
|
Automatically installs **Playwright Chromium** (`npx playwright install chromium --with-deps`) into the Docker container running DeepSeek Harness, and bridges the installed binary onto `PATH` so the **dsh-ego-browser** side-bar plugin finds it — no manual "find the binary / symlink it / set `EGO_LINUX_*` env vars / wrap it with `--no-sandbox`" steps for agents ever again.
|
||||||
|
|
||||||
|
## Why this plugin exists
|
||||||
|
|
||||||
|
`dsh-ego-browser` discovers its browser the following way (see `lib/index.js` → `resolveEgoEnv` and `runtime/ego-linux/src/chrome.mjs`):
|
||||||
|
|
||||||
|
1. It scans `PATH` + fixed locations for `google-chrome`, `google-chrome-stable`, `chromium`, `chromium-browser`, … and
|
||||||
|
2. on **root** hosts (docker default), it hands the found binary to a bundled `--no-sandbox` wrapper that only looks at fixed paths (`/usr/bin/*`, `/opt/google/...`, `/usr/local/bin/google-chrome-stable`, `/snap/bin/chromium`).
|
||||||
|
|
||||||
|
Playwright installs Chromium into `~/.cache/ms-playwright/chromium-<rev>/chrome-linux{,64}/chrome` — a location **invisible to both scans**. That is why agents kept having to hunt the binary down, symlink/wrap it, and set `EGO_LINUX_CHROME` / `EGO_LINUX_EXTRA_ARGS` by hand.
|
||||||
|
|
||||||
|
This plugin does exactly that bridging once, automatically:
|
||||||
|
|
||||||
|
- on load it checks the Playwright cache (idempotent, no-op when the browser is already there),
|
||||||
|
- if the browser is missing it runs `npx --yes playwright install chromium --with-deps` **in the background** (profile startup is never blocked for minutes), and
|
||||||
|
- it then symlinks the installed binary at `/usr/local/bin/google-chrome-stable` and `/usr/local/bin/chromium` — the exact names ego-browser's PATH scan *and* its bundled `--no-sandbox` wrapper look for. On this root container, the chain then resolves to: root detected → bundled `--no-sandbox` wrapper → `--no-sandbox` launch; no `DISPLAY` → automatic headless. **No env vars, no manual wrapper, no agent steps.**
|
||||||
|
|
||||||
|
Nothing is ever clobbered: a pre-existing system Chrome or a user wrapper keeps winning (the plugin only creates links where nothing of its own/an external binary exists).
|
||||||
|
|
||||||
|
## Install (from git — recommended)
|
||||||
|
|
||||||
|
Replace `<you>` with your GitHub account name:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh plugin --profile web add github:<you>/dsh-browser-install#main
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `#main` **branch ref is what makes updates work**: `pnpm update` re-resolves the branch tip (verified against pnpm 12.8.1), so after you push new commits simply run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh plugin --profile web update # all deps
|
||||||
|
dsh plugin --profile web update dsh-browser-install # just this one
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bundle changes only become active after a profile restart: `dsh --profile web`.
|
||||||
|
- No build step: the bundle ships plain ESM and declares no `prepare` script, so pnpm's default block on git-dependency build scripts never applies — nothing to `allowBuilds` in `pnpm-workspace.yaml`.
|
||||||
|
- pnpm installs git deps over HTTPS, so keep the repo public (or set up credentials on the host).
|
||||||
|
|
||||||
|
## Alternative: install from a local checkout
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh plugin --profile web add /root/workspaces/Plugin\ Development/dsh-browser-install
|
||||||
|
```
|
||||||
|
|
||||||
|
A directory install is a symlinked `file:` dependency — handy while iterating. For persistent installs and updates, prefer the git flow above.
|
||||||
|
|
||||||
|
## Restart and verify
|
||||||
|
|
||||||
|
Restart the profile, then check the plugin loaded and did its job:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh --profile web
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh --profile web --dump-config | grep -A3 browser-install
|
||||||
|
ls -la /usr/local/bin/google-chrome-stable # -> .../ms-playwright/chromium-*/chrome-linux64/chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
Then from any agent session, `ego_doctor` should report a browser binary, and `ego_space_open`/`ego_navigate` work with no environment prep.
|
||||||
|
|
||||||
|
> Non-root hosts: set `symlinkDir` to a directory on your `PATH` (e.g. `~/.local/bin`). On root only `/usr/local/bin` is needed — it also satisfies the bundled `--no-sandbox` wrapper's absolute-path list.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
All fields are optional. Override them in your profile's `cordis.patch.yml` (or in the bundle row's `config`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: browser-install
|
||||||
|
name: "dsh-browser-install"
|
||||||
|
config:
|
||||||
|
withDeps: true # run `--with-deps` (apt system deps); needs root/apt
|
||||||
|
target: chromium # chromium | chromium-headless-shell | chrome
|
||||||
|
package: playwright # npx package spec, e.g. "playwright@1.54.1"
|
||||||
|
forceInstall: false # always re-run the install on load
|
||||||
|
installOnStartup: true # install in background when the browser is missing
|
||||||
|
cacheDir: "" # empty = $PLAYWRIGHT_BROWSERS_PATH | ~/.cache/ms-playwright
|
||||||
|
symlinkDir: /usr/local/bin
|
||||||
|
symlinkNames: [google-chrome-stable, chromium]
|
||||||
|
installTimeoutMs: 600000
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `installOnStartup` | `true` | Run the Playwright install when the plugin loads **and** no matching browser is found. The install runs in the background so profile startup is not blocked. |
|
||||||
|
| `forceInstall` | `false` | Always run the install on load, even when a browser is already present (e.g. after a Playwright upgrade). |
|
||||||
|
| `package` | `playwright` | Package spec passed to `npx --yes <package> install ...`. Pin with e.g. `playwright@1.54.1`. |
|
||||||
|
| `target` | `chromium` | Which Playwright browser to ensure: `chromium` (full build), `chromium-headless-shell`, or `chrome` (Google Chrome). Default matches what ego-browser expects. |
|
||||||
|
| `withDeps` | `true` | Pass `--with-deps` so Playwright also installs the browser's system dependencies via apt (requires root; the DSH image runs as root). |
|
||||||
|
| `cacheDir` | auto | Playwright browsers cache root. Empty = `$PLAYWRIGHT_BROWSERS_PATH`, else `~/.cache/ms-playwright`. When set here, the installer child also gets `PLAYWRIGHT_BROWSERS_PATH`. |
|
||||||
|
| `symlinkDir` | `/usr/local/bin` | Directory for the PATH links; must be on the DSH process `PATH`. |
|
||||||
|
| `symlinkNames` | `[google-chrome-stable, chromium]` | Link names to create. `google-chrome-stable` is the one ego-browser's PATH scan and its `--no-sandbox` wrapper both check. |
|
||||||
|
| `installTimeoutMs` | `600000` | Max time for the background install before it is killed. |
|
||||||
|
|
||||||
|
## Behaviour notes
|
||||||
|
|
||||||
|
- **No-clobber links:** a link whose target lives inside the Playwright cache is treated as ours and refreshed to the newest installed revision; any other existing file/symlink is left untouched (and wins).
|
||||||
|
- **Lifecycle:** the background installer child is killed if the plugin is unloaded mid-install (`ctx.effect`). The PATH links are intentionally *not* removed on unload — a running browser may be using them and they are harmless, idempotent shared state.
|
||||||
|
- **Error visibility:** install failures, symlink errors, and a `symlinkDir` missing from `PATH` are all logged loudly under the `dsh-browser-install` logger scope, so an agent can read the reason instead of guessing.
|
||||||
|
- **ego-browser settings untouched:** the plugin does not modify dsh-ego-browser's config or any `EGO_LINUX_*` env vars — it only fills the gap (install + visible symlink) that makes ego-browser's existing auto-adaptation work (root → `--no-sandbox` wrapper, no DISPLAY → headless).
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dsh plugin --profile web remove dsh-browser-install
|
||||||
|
```
|
||||||
|
|
||||||
|
The symlinks are left in place (harmless; the browser keeps working for ego-browser). Remove them manually if you truly want them gone:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rm -f /usr/local/bin/google-chrome-stable /usr/local/bin/chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Pure helpers (`resolveCacheDir`, `resolveBrowserBinary`, `planSymlinks`, `applySymlinkPlan`, `buildInstallCmd`) are exported for testing; the plugin itself needs no build step (plain ESM, no TypeScript compile).
|
||||||
|
|
||||||
|
- Node ≥ 22 (the harness image ships v24).
|
||||||
|
- Requires DSH ≥ 0.1.2-rc.1 (same baseline as dsh-ego-browser).
|
||||||
|
- Runtime dependency: `@deepseek-ai/schemastery` (already present in the profile).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# dsh-browser-install — ensure Playwright Chromium is installed and reachable
|
||||||
|
# by dsh-ego-browser in root / Docker containers. See README.md for details and
|
||||||
|
# the full config table; every field is optional (schema defaults apply).
|
||||||
|
- insert:
|
||||||
|
- id: browser-install
|
||||||
|
name: "dsh-browser-install"
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
// @ts-check
|
||||||
|
/**
|
||||||
|
* dsh-browser-install — make a Playwright Chromium available to dsh-ego-browser
|
||||||
|
* in root / Docker containers, with zero manual PATH / env-var / wrapper steps.
|
||||||
|
*
|
||||||
|
* Why this plugin exists
|
||||||
|
* ----------------------
|
||||||
|
* dsh-ego-browser discovers its browser binary by scanning PATH + fixed
|
||||||
|
* locations (google-chrome, chromium, ...) and, on root hosts, hands the found
|
||||||
|
* binary to a bundled `--no-sandbox` wrapper. Playwright installs Chromium
|
||||||
|
* into `~/.cache/ms-playwright/chromium-<rev>/chrome-linux{,64}/chrome`, which
|
||||||
|
* is invisible to both scans — so agents had to hunt the binary down, symlink
|
||||||
|
* or wrap it, and set EGO_LINUX_* env vars by hand.
|
||||||
|
*
|
||||||
|
* This plugin does that once, automatically, on load:
|
||||||
|
* 1. ensures `npx playwright install chromium --with-deps` has been run
|
||||||
|
* (background when the browser is missing, so profile startup is not
|
||||||
|
* blocked), and
|
||||||
|
* 2. symlinks the installed binary at `/usr/local/bin/google-chrome-stable`
|
||||||
|
* (+ `chromium`), which is exactly where ego-browser's PATH scan and its
|
||||||
|
* bundled --no-sandbox wrapper both look.
|
||||||
|
*
|
||||||
|
* Nothing is ever clobbered: pre-existing non-Playwright binaries (system
|
||||||
|
* Chrome, hand-made wrappers) are left untouched and keep winning.
|
||||||
|
*
|
||||||
|
* Revertible-effect discipline: the background installer child is tracked with
|
||||||
|
* ctx.effect so it is killed if the plugin is unloaded mid-install. The PATH
|
||||||
|
* symlinks are deliberately NOT removed on unload — a running browser may be
|
||||||
|
* using them, and they are harmless shared state (idempotent on next load).
|
||||||
|
*/
|
||||||
|
import { spawn } from 'node:child_process'
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
lstatSync,
|
||||||
|
mkdirSync,
|
||||||
|
readdirSync,
|
||||||
|
readlinkSync,
|
||||||
|
symlinkSync,
|
||||||
|
unlinkSync,
|
||||||
|
} from 'node:fs'
|
||||||
|
import { homedir } from 'node:os'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import z from '@deepseek-ai/schemastery'
|
||||||
|
|
||||||
|
export const name = 'dsh-browser-install'
|
||||||
|
|
||||||
|
// ── Config ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Config = z.object({
|
||||||
|
installOnStartup: z
|
||||||
|
.boolean()
|
||||||
|
.description('Run the Playwright browser install when the plugin loads and no matching browser is found.')
|
||||||
|
.default(true),
|
||||||
|
forceInstall: z
|
||||||
|
.boolean()
|
||||||
|
.description('Always run `npx playwright install ...` on load, even when a browser binary is already present.')
|
||||||
|
.default(false),
|
||||||
|
package: z
|
||||||
|
.string()
|
||||||
|
.description('Package spec passed to npx, e.g. "playwright" or "playwright@1.54.1".')
|
||||||
|
.default('playwright'),
|
||||||
|
target: z
|
||||||
|
.union(['chromium', 'chromium-headless-shell', 'chrome'])
|
||||||
|
.description('Playwright install target to ensure. "chromium" is the full build dsh-ego-browser expects.')
|
||||||
|
.default('chromium'),
|
||||||
|
withDeps: z
|
||||||
|
.boolean()
|
||||||
|
.description('Pass --with-deps so Playwright also installs the browser system dependencies (apt).')
|
||||||
|
.default(true),
|
||||||
|
cacheDir: z
|
||||||
|
.string()
|
||||||
|
.description('Playwright browsers cache root. Empty = $PLAYWRIGHT_BROWSERS_PATH or ~/.cache/ms-playwright.')
|
||||||
|
.default(''),
|
||||||
|
symlinkDir: z
|
||||||
|
.string()
|
||||||
|
.description('Directory for the PATH links (must be on PATH; /usr/local/bin works in the DSH docker image).')
|
||||||
|
.default('/usr/local/bin'),
|
||||||
|
symlinkNames: z
|
||||||
|
.array(z.string())
|
||||||
|
.description('Link names so dsh-ego-browser and CLI tools can find the browser.')
|
||||||
|
.default(['google-chrome-stable', 'chromium']),
|
||||||
|
installTimeoutMs: z
|
||||||
|
.number()
|
||||||
|
.description('Max time allowed for the background install step before it is killed.')
|
||||||
|
.default(600000),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Pure helpers (exported for testing) ──────────────────────────────────────
|
||||||
|
|
||||||
|
/** Where Playwright keeps its browsers. */
|
||||||
|
export function resolveCacheDir(config, env = process.env) {
|
||||||
|
if (config.cacheDir) return config.cacheDir
|
||||||
|
if (env.PLAYWRIGHT_BROWSERS_PATH) return env.PLAYWRIGHT_BROWSERS_PATH
|
||||||
|
return join(env.HOME || homedir(), '.cache', 'ms-playwright')
|
||||||
|
}
|
||||||
|
|
||||||
|
const TARGET_LAYOUT = {
|
||||||
|
chromium: {
|
||||||
|
prefix: 'chromium-',
|
||||||
|
names: ['chrome-linux/chrome', 'chrome-linux64/chrome'],
|
||||||
|
},
|
||||||
|
chrome: {
|
||||||
|
prefix: 'chrome-',
|
||||||
|
names: ['chrome-linux/chrome', 'chrome-linux64/chrome'],
|
||||||
|
},
|
||||||
|
'chromium-headless-shell': {
|
||||||
|
prefix: 'chromium_headless_shell-',
|
||||||
|
names: ['chrome-linux/headless_shell', 'chrome-linux64/headless_shell'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate the newest installed Playwright browser binary for `target`.
|
||||||
|
* Returns an absolute path, or null when no revision is installed.
|
||||||
|
*/
|
||||||
|
export function resolveBrowserBinary({ target, cacheDir }) {
|
||||||
|
const layout = TARGET_LAYOUT[target]
|
||||||
|
if (!layout) throw new Error(`unknown browser target: ${target}`)
|
||||||
|
if (!existsSync(cacheDir)) return null
|
||||||
|
let best = null
|
||||||
|
for (const entry of readdirSync(cacheDir)) {
|
||||||
|
if (!entry.startsWith(layout.prefix)) continue
|
||||||
|
const rev = Number.parseInt(entry.slice(layout.prefix.length), 10)
|
||||||
|
if (!Number.isFinite(rev)) continue
|
||||||
|
for (const rel of layout.names) {
|
||||||
|
const abs = join(cacheDir, entry, rel)
|
||||||
|
if (existsSync(abs)) {
|
||||||
|
if (!best || rev > best.rev) best = { rev, path: abs }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best?.path ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide what to do with each link path, without touching the filesystem.
|
||||||
|
*
|
||||||
|
* action:
|
||||||
|
* - 'create' -> nothing at the link path, safe to create
|
||||||
|
* - 'refresh' -> our own previous link (target lives inside the Playwright
|
||||||
|
* cache) pointing at a stale revision: recreate to `binary`
|
||||||
|
* - 'keep' -> a user-managed file/symlink (or other): leave it alone
|
||||||
|
* - 'error' -> lstat failed for a reason other than ENOENT
|
||||||
|
*/
|
||||||
|
export function planSymlinks({ binary, symlinkDir, symlinkNames, cacheDir }) {
|
||||||
|
return symlinkNames.map((name) => {
|
||||||
|
const link = join(symlinkDir, name)
|
||||||
|
let action
|
||||||
|
try {
|
||||||
|
const st = lstatSync(link)
|
||||||
|
if (st.isSymbolicLink()) {
|
||||||
|
const target = readlinkSync(link)
|
||||||
|
action = target === binary ? 'ok' : target.startsWith(cacheDir) ? 'refresh' : 'keep'
|
||||||
|
} else {
|
||||||
|
action = 'keep'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
action = error?.code === 'ENOENT' ? 'create' : 'error'
|
||||||
|
}
|
||||||
|
return { name, link, action, binary }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execute a symlink plan. Returns the links it (re)created. */
|
||||||
|
export function applySymlinkPlan(plan) {
|
||||||
|
const created = []
|
||||||
|
for (const item of plan) {
|
||||||
|
if (item.action !== 'create' && item.action !== 'refresh') continue
|
||||||
|
mkdirSync(dirname(item.link), { recursive: true })
|
||||||
|
try {
|
||||||
|
unlinkSync(item.link)
|
||||||
|
} catch {
|
||||||
|
// nothing at the path yet — fine
|
||||||
|
}
|
||||||
|
symlinkSync(item.binary, item.link, 'file')
|
||||||
|
created.push(item.link)
|
||||||
|
}
|
||||||
|
return created
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The npx command this plugin runs. */
|
||||||
|
export function buildInstallCmd(config) {
|
||||||
|
const args = [
|
||||||
|
'--yes',
|
||||||
|
config.package,
|
||||||
|
'install',
|
||||||
|
...(config.withDeps ? ['--with-deps'] : []),
|
||||||
|
config.target,
|
||||||
|
]
|
||||||
|
return { cmd: 'npx', args }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Runtime wiring ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeLogger(ctx) {
|
||||||
|
const log = ctx.logger?.(name)
|
||||||
|
return log ?? { debug() {}, info() {}, warn() {}, error() {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn the Playwright installer, stream its output to the logger, and return
|
||||||
|
* a handle bound to the plugin lifetime (ctx.effect kills the child on
|
||||||
|
* unload). Never throws synchronously; failures surface through promise.
|
||||||
|
*/
|
||||||
|
function runInstall(ctx, config, cacheDir, log) {
|
||||||
|
const { cmd, args } = buildInstallCmd(config)
|
||||||
|
const child = spawn(cmd, args, {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Make the installer write where this plugin will look for the binary.
|
||||||
|
...(config.cacheDir ? { PLAYWRIGHT_BROWSERS_PATH: cacheDir } : {}),
|
||||||
|
},
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
log.warn(`install exceeded ${config.installTimeoutMs}ms; killing it`)
|
||||||
|
try {
|
||||||
|
child.kill('SIGTERM')
|
||||||
|
} catch {
|
||||||
|
// already gone
|
||||||
|
}
|
||||||
|
}, config.installTimeoutMs)
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
try {
|
||||||
|
child.kill('SIGTERM')
|
||||||
|
} catch {
|
||||||
|
// already gone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.effect(() => cleanup)
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
let stdout = ''
|
||||||
|
let stderr = ''
|
||||||
|
const cap = 60000 // keep only the tail for error reporting
|
||||||
|
child.stdout.on('data', (chunk) => {
|
||||||
|
const text = chunk.toString()
|
||||||
|
stdout = (stdout + text).slice(-cap)
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
const trimmed = line.trimEnd()
|
||||||
|
if (trimmed) log.debug(trimmed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
child.stderr.on('data', (chunk) => {
|
||||||
|
const text = chunk.toString()
|
||||||
|
stderr = (stderr + text).slice(-cap)
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
const trimmed = line.trimEnd()
|
||||||
|
if (trimmed) log.warn(trimmed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
child.on('error', (error) => reject(error))
|
||||||
|
child.on('close', (code) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ stdout, stderr })
|
||||||
|
} else {
|
||||||
|
const tail = [stdout, stderr].filter(Boolean).join('\n').trimEnd().slice(-2000)
|
||||||
|
reject(new Error(`npx playwright install exited with code ${code}${tail ? `\n--- tail ---\n${tail}` : ''}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info(`installing: ${cmd} ${args.join(' ')}`)
|
||||||
|
return { child, promise, cleanup }
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureLinks(binary, cacheDir, config, log) {
|
||||||
|
const plan = planSymlinks({ binary, symlinkDir: config.symlinkDir, symlinkNames: config.symlinkNames, cacheDir })
|
||||||
|
let created = []
|
||||||
|
try {
|
||||||
|
created = applySymlinkPlan(plan)
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`symlink creation failed: ${error?.message ?? error}`)
|
||||||
|
return { created: [], errors: [String(error?.message ?? error)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const kept = plan.filter((item) => item.action === 'keep').map((item) => item.link)
|
||||||
|
if (kept.length) log.info(`kept existing (user-managed): ${kept.join(', ')}`)
|
||||||
|
for (const link of created) log.info(`linked ${link} -> ${binary}`)
|
||||||
|
|
||||||
|
const pathDirs = (process.env.PATH ?? '')
|
||||||
|
.split(':')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((dir) => dir.replace(/\/+$/, ''))
|
||||||
|
if (!pathDirs.includes(config.symlinkDir.replace(/\/+$/, ''))) {
|
||||||
|
log.warn(
|
||||||
|
`symlinkDir "${config.symlinkDir}" is not on PATH — dsh-ego-browser's binary scan and the --no-sandbox wrapper may miss it. ` +
|
||||||
|
`Add it to PATH or set symlinkDir to a directory already on PATH (e.g. /usr/local/bin).`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return { created, errors: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-shot ensure: resolve the binary, install in the background if missing
|
||||||
|
* (or if forceInstall), then (re)link. Returns a status string for apply().
|
||||||
|
*/
|
||||||
|
async function ensureBrowser(ctx, config, log) {
|
||||||
|
const target = config.target
|
||||||
|
const cacheDir = resolveCacheDir(config)
|
||||||
|
log.info(`target=${target} cacheDir=${cacheDir}`)
|
||||||
|
|
||||||
|
let binary = resolveBrowserBinary({ target, cacheDir })
|
||||||
|
if (binary) log.info(`found browser at ${binary}`)
|
||||||
|
|
||||||
|
if (!binary && !config.installOnStartup) {
|
||||||
|
log.warn(
|
||||||
|
`no ${target} installed at ${cacheDir} and installOnStartup=false — ` +
|
||||||
|
`run \`npx playwright install ${config.withDeps ? '--with-deps ' : ''}${target}\` manually, or enable installOnStartup.`,
|
||||||
|
)
|
||||||
|
return { status: 'missing', cacheDir }
|
||||||
|
}
|
||||||
|
|
||||||
|
const needInstall = !binary || config.forceInstall
|
||||||
|
if (!needInstall) {
|
||||||
|
ensureLinks(binary, cacheDir, config, log)
|
||||||
|
return { status: 'ready', binary, cacheDir }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background install so profile startup is never blocked for minutes.
|
||||||
|
const { promise } = runInstall(ctx, config, cacheDir, log)
|
||||||
|
void promise.then(
|
||||||
|
async () => {
|
||||||
|
const installed = resolveBrowserBinary({ target, cacheDir })
|
||||||
|
if (!installed) {
|
||||||
|
log.error('install reported success but no browser binary was found in cacheDir')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.info(`install finished; browser at ${installed}`)
|
||||||
|
ensureLinks(installed, cacheDir, config, log)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
log.error(`install failed: ${error?.message ?? error}`)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return { status: 'installing', cacheDir }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apply(ctx, config) {
|
||||||
|
const log = makeLogger(ctx)
|
||||||
|
log.info('mounting: ensure a Playwright browser is installed and discoverable by dsh-ego-browser')
|
||||||
|
const outcome = await ensureBrowser(ctx, config, log)
|
||||||
|
if (outcome.status === 'ready') {
|
||||||
|
log.info(`ready: dsh-ego-browser will find "${outcome.binary}" on the next ego_* call (root -> --no-sandbox wrapper, no DISPLAY -> headless)`)
|
||||||
|
} else if (outcome.status === 'installing') {
|
||||||
|
log.info('browser missing — install is running in the background; ego_* browser launches will fail until it completes')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "dsh-browser-install",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Auto-installs Playwright Chromium (npx playwright install chromium --with-deps) into the DSH container and bridges it onto PATH so dsh-ego-browser finds it without manual env-var or wrapper steps.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@deepseek-ai/schemastery": "^3.18.3"
|
||||||
|
},
|
||||||
|
"dsh": {
|
||||||
|
"engines": {
|
||||||
|
"dsh": ">=0.1.2-rc.1"
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"patch": "./cordis.patch.yml"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"cordis.patch.yml",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"dsh",
|
||||||
|
"dsh-plugin",
|
||||||
|
"playwright",
|
||||||
|
"chromium",
|
||||||
|
"ego-browser",
|
||||||
|
"browser"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user