Add --extension flag to load Chrome extensions in headless mode - #52
Add --extension flag to load Chrome extensions in headless mode#52goeric wants to merge 2 commits into
Conversation
Extensions previously only worked with "rodney start --show". The cause was that Chrome's old headless mode cannot run extensions at all, and rod's Headless() sets a bare --headless, which selects old headless on the Chromium build rodney downloads. Passing --extension now switches to the new headless mode, where extensions work fully, service workers included. rodney start --extension ./my-extension rodney start --extension ./packed.crx --extension ./other.zip Chrome only accepts unpacked extensions on the command line, so .crx and .zip archives are unpacked into the session directory first (CRX2 and CRX3 headers are both handled). Chrome 137+ also ignores --load-extension unless DisableLoadExtensionCommandLineSwitch is disabled, so that is appended to the existing --disable-features list. The new "rodney extensions" command lists the loaded extensions along with the ID Chrome assigned to each, which is needed to reach chrome-extension:// URLs. IDs are derived the same way Chrome derives them for unpacked extensions, from the symlink-resolved path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rodney sets --single-process unconditionally, for screenshots in gVisor/container environments. That is survivable under old headless, which is a stripped-down shell, but --extension switches Chrome to new headless, where the full browser stack comes up: renderer, GPU, viz compositor, media, and the extension's own service worker. Collapsing all of that into one OS process removes every fault boundary, so a CHECK failure or bad access anywhere takes down the whole browser instead of one renderer. Measured with an MV3 extension loaded: --headless=new + --single-process 0 child processes, 205 threads --headless=new 10 child processes, 59 threads Local Chromium crash reports match the first row: a single process holding Chrome_InProcGpuThread, Chrome_InProcRendererThread, Chrome_InProcUtilityThread, a ServiceWorker thread and 23 DedicatedWorker threads, aborting on a CHECK on the in-process utility thread. Drop --single-process only on the extension path; launches without --extension keep it and are unchanged. Also extract configureExtensions() so cmdStart and the end-to-end test build the launcher the same way — the test previously duplicated the flags inline, so it could not have caught this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed 920b808 — extensions no longer run under I hit real instability using this branch locally: Chromium crashing repeatedly with Scoped the change to the extension path only, so launches without Worth noting separately: whether rodney should set |
Extensions currently only work with
rodney start --show. This adds--extensionso they work headless too.Why headless didn't work
Chrome's old headless mode cannot run extensions at all. rod's
Headless()sets a bare--headless, which selects old headless on the Chromium build rodney downloads, so--load-extensionwas silently ignored. Passing--extensionnow switches to new headless, where extensions work fully — I verified an MV3 extension's content scripts and its background service worker both run (the service worker fired on install and opened its onboarding tab). Launches without--extensionare unchanged.Two other things were needed:
.crx/.ziparchives are unpacked into the session directory first. Both CRX2 and CRX3 headers are handled.--load-extensionunlessDisableLoadExtensionCommandLineSwitchis disabled. That's appended to--disable-featuresrather than set, so rod's existing defaults survive.rodney extensionsYou need the extension ID to reach
chrome-extension://URLs, and nothing else in the CLI surfaces it:IDs are computed the way Chrome computes them for unpacked extensions (first 16 bytes of the SHA-256 of the path, hex digits mapped onto a–p). Two subtleties I hit while testing:
/tmp/xon macOS hashes as/private/tmp/x. WithoutEvalSymlinksthe reported ID is wrong for anything under/tmp.crx_file::id_util::GenerateIdForPath). I implemented this from the Chromium source but could not test it on a Windows host — worth a check by someone who can, or I'm happy to drop the Windows branch if you'd rather not carry untested code. It's isolated inextensionIDPathBytesForand unit-tested for the encoding itself.Extensions can't run under
--single-processrodney sets
--single-processunconditionally (for screenshots in gVisor/containerenvironments). That's survivable under old headless, which is a stripped-down shell,
but new headless brings up the full browser stack — renderer, GPU, viz compositor,
media, and the extension's service worker. Putting all of that in one OS process
removes every fault boundary, so a
CHECKfailure anywhere kills the whole browserrather than one renderer. Measured with an MV3 extension loaded:
--headless=new --single-process--headless=newChromium crash reports on my machine match the first row exactly: one process holding
Chrome_InProcGpuThread,Chrome_InProcRendererThread,Chrome_InProcUtilityThread,a
ServiceWorker threadand 23DedicatedWorkerthreads, aborting on aCHECKon thein-process utility thread.
So
--single-processis now dropped on the extension path only — launches without--extensionkeep it and are byte-identical to before. Whether rodney should set itunconditionally at all feels like a separate question, happy to open an issue.
Notes
extensions.go/extensions_test.goto keepmain.goreviewable; happy to inline it if you'd prefer to keep everything in one file.parseStartArgsnow returns astartOptionsstruct rather than a growing tuple. The existing tests are updated accordingly....ziphas stem.., which would otherwise resolve to the session directory and get deleted.🤖 Generated with Claude Code