mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-28 19:32:57 +08:00
fix: explain launch failures caused by running as root (#2634)
Follow-up to #261. That issue was closed by #338, which added `--chrome-arg` so the sandbox can be disabled explicitly. The flag works, but the failure it fixes is still undiscoverable — you cannot find out that `--chrome-arg=--no-sandbox` is the answer. ### Problem `launch()` uses `pipe: true`. In pipe mode Puppeteer never calls `waitForLineOutput()`, which is the only place Chrome's stderr makes it into the thrown error. So when Chrome exits at startup with ``` Running as root without --no-sandbox is not supported. See https://crbug.com/638180. ``` that line is dropped, and what reaches the MCP client is: ``` Protocol error (Target.setDiscoverTargets): Target closed ``` Reproduced on `main` (v1.8.0) by pointing `executablePath` at a stub that prints Chrome's message to stderr and exits 1 — exactly the symptom reported in #261. The `catch` in `launch()` only recognised `The browser is already running`. ### Fix Detect the case and rethrow with an explanation. Deliberately **not** disabling the sandbox automatically, per https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/261#issuecomment-3389871157: ``` Chrome failed to start: Protocol error (Target.setDiscoverTargets): Target closed chrome-devtools-mcp is running as root and Chrome does not start as root unless its sandbox is disabled (https://crbug.com/638180). Prefer running chrome-devtools-mcp as a non-root user. If that is not possible, for example in a container, pass --chrome-arg=--no-sandbox. That disables the Chrome sandbox, so only do it for content you trust. ``` Notes on the shape of the check: - It runs **only on a failed launch**, so root with a properly installed setuid sandbox is unaffected. - The original error is kept in the message and as `cause`, so an unrelated failure under root is never masked or mislabelled. - `--no-sandbox` present in the args short-circuits it: root is then not what stopped Chrome. - `--disable-setuid-sandbox` and `--no-sandbox-and-elevated` do **not** count as opting out — Chrome's zygote check requires `--no-sandbox` specifically. Covered by a test. - On Windows `process.getuid` is undefined, so the check is a no-op. Also adds a "Running as root" section to `docs/troubleshooting.md`, next to the existing "Operating system sandboxes". ### Testing Five unit tests in `tests/browser.test.ts` covering root / non-root / no-uid platforms, the already-opted-out args, and the near-miss args above. The pre-existing integration tests in that file cannot run in my environment — Chrome reports `No usable sandbox! ... AppArmor userns restrictions` there, on a clean checkout as well as with this change.
This commit is contained in:
@@ -81,6 +81,19 @@ either disable sandboxing for `chrome-devtools-mcp` in your MCP client or use
|
||||
`--browser-url` to connect to a Chrome instance that you start manually outside
|
||||
of the MCP client sandbox.
|
||||
|
||||
### Running as root
|
||||
|
||||
Chrome does not start as root
|
||||
([crbug.com/638180](https://crbug.com/638180)). It exits immediately and
|
||||
`chrome-devtools-mcp` reports that Chrome failed to start. This is a common
|
||||
issue in containers and CI images that run everything as root.
|
||||
|
||||
Run `chrome-devtools-mcp` as a non-root user. In a container, create an
|
||||
unprivileged user in the image and switch to it with `USER`; the build itself
|
||||
can still run as root. For the host-side setup that Chrome's sandbox needs, see
|
||||
Puppeteer's
|
||||
[Setting up Chrome Linux sandbox](https://pptr.dev/troubleshooting#setting-up-chrome-linux-sandbox).
|
||||
|
||||
### WSL
|
||||
|
||||
By default, `chrome-devtools-mcp` in WSL requires Chrome to be installed within the Linux environment. While it normally attempts to launch Chrome on the Windows side, this currently fails due to a [known WSL issue](https://github.com/microsoft/WSL/issues/14201). Ensure you are using a [Linux distribution compatible with Chrome](https://support.google.com/chrome/a/answer/7100626).
|
||||
|
||||
@@ -170,6 +170,45 @@ export function detectDisplay(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chrome refuses to start as root unless the sandbox is explicitly disabled and
|
||||
* only says so on its stderr. Because we launch with `pipe: true`, Puppeteer
|
||||
* never surfaces that stderr and the failure reaches the client as an opaque
|
||||
* `Protocol error (Target.setDiscoverTargets): Target closed`. Detect the
|
||||
* situation and explain the way out instead. See https://crbug.com/638180.
|
||||
*
|
||||
* Returns `undefined` when the failure cannot be explained by running as root,
|
||||
* including on platforms without uids and when the sandbox was already disabled
|
||||
* through `--chrome-arg` (in which case root is not what stopped Chrome).
|
||||
*
|
||||
* Exported for testing.
|
||||
*/
|
||||
export function rootSandboxLaunchError(
|
||||
error: Error,
|
||||
args: readonly string[],
|
||||
uid = process.getuid?.(),
|
||||
): Error | undefined {
|
||||
if (uid !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
args.some(arg => arg === '--no-sandbox' || arg.startsWith('--no-sandbox='))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return new Error(
|
||||
`Chrome failed to start: ${error.message}\n\n` +
|
||||
'chrome-devtools-mcp is running as root and Chrome does not start as root ' +
|
||||
'(https://crbug.com/638180). Run chrome-devtools-mcp as a non-root user; in a ' +
|
||||
'container, create an unprivileged user in the image and switch to it with ' +
|
||||
"USER. For the setup that Chrome's sandbox needs, see " +
|
||||
'https://pptr.dev/troubleshooting#setting-up-chrome-linux-sandbox.',
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function launch(options: McpLaunchOptions): Promise<Browser> {
|
||||
const {channel, executablePath, headless, isolated} = options;
|
||||
const profileDirName =
|
||||
@@ -259,6 +298,10 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
|
||||
},
|
||||
);
|
||||
}
|
||||
const rootError = rootSandboxLaunchError(error as Error, args);
|
||||
if (rootError) {
|
||||
throw rootError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ensureBrowserConnected,
|
||||
launch,
|
||||
makeTargetFilter,
|
||||
rootSandboxLaunchError,
|
||||
} from '../src/browser.js';
|
||||
import type {Browser} from '../src/third_party/index.js';
|
||||
|
||||
@@ -61,6 +62,56 @@ describe('browser', () => {
|
||||
detectDisplay();
|
||||
});
|
||||
|
||||
describe('rootSandboxLaunchError', () => {
|
||||
const targetClosed = new Error(
|
||||
'Protocol error (Target.setDiscoverTargets): Target closed',
|
||||
);
|
||||
|
||||
it('explains an opaque launch failure when running as root', () => {
|
||||
const error = rootSandboxLaunchError(targetClosed, [], 0);
|
||||
assert.ok(error);
|
||||
assert.match(error.message, /non-root user/);
|
||||
assert.match(error.message, /pptr\.dev\/troubleshooting/);
|
||||
// The original failure stays visible so unrelated errors are not masked.
|
||||
assert.match(error.message, /Target closed/);
|
||||
assert.strictEqual(error.cause, targetClosed);
|
||||
});
|
||||
|
||||
it('does not explain failures when not running as root', () => {
|
||||
assert.strictEqual(
|
||||
rootSandboxLaunchError(targetClosed, [], 1000),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not explain failures on platforms without uids', () => {
|
||||
assert.strictEqual(
|
||||
rootSandboxLaunchError(targetClosed, [], undefined),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not explain failures when the sandbox is already disabled', () => {
|
||||
assert.strictEqual(
|
||||
rootSandboxLaunchError(targetClosed, ['--no-sandbox'], 0),
|
||||
undefined,
|
||||
);
|
||||
assert.strictEqual(
|
||||
rootSandboxLaunchError(targetClosed, ['--no-sandbox=true'], 0),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('is not fooled by unrelated arguments that start the same', () => {
|
||||
assert.ok(
|
||||
rootSandboxLaunchError(targetClosed, ['--no-sandbox-and-elevated'], 0),
|
||||
);
|
||||
assert.ok(
|
||||
rootSandboxLaunchError(targetClosed, ['--disable-setuid-sandbox'], 0),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot launch multiple times with the same profile', async () => {
|
||||
await runWithRetry(async () => {
|
||||
const tmpDir = os.tmpdir();
|
||||
|
||||
Reference in New Issue
Block a user