Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Electron and natural mode

Electron apps, --electron; --natural: a headless run a page cannot tell.

Electron apps: --electron <app> (with start, or serve). navigera launches the app with a DevTools port of the app’s choosing (--remote-debugging-port=0, read back from its stderr) and nothing else of Chrome’s launch flags: an app is not a browser, and a throwaway profile or headless mode would change how it starts. start returns once a window the app shows is open and has loaded something to read or use (text or a control), up to the navigation timeout: an app that boots its UI after the window opens is past its empty shell. The session starts on the largest window the app shows: an app’s helper windows, a dropdown or a tray menu made with show: false, are tabs but not the one the session works in, now or after a window closes, as its main process says (BrowserWindow.isVisible()). Where the main process can’t be reached (an app attached to, or one that turns Node’s inspector off), the largest window is taken: a page’s own visibility is wrong both ways (a hidden window says visible on its first load, a shown one under Xvfb says hidden). A <webview> inside a window is a tab too, marked "webview": true in tab-list: tab-select it to read and act inside it; the session never moves into one by itself. start’s result says electron: {version, main_process}: the app’s Electron version (null when the app set its own user agent and its main process is out of reach) and whether eval --main can reach its main process. An ax of a window that still shows nothing notes that the app may be starting. A single-instance app that exits at once because a copy of it is already running fails start saying so.

An app that is already running with a DevTools port (started with --remote-debugging-port=9222, as for Chrome’s DevTools) is driven with start --attach 9222 (or its ws:// URL): navigera sees it is Electron, takes its windows as the tabs (and the ones it opens later), and starts in the largest one it shows. quit leaves the app running, quit --close-browser ends it, and eval --main needs the launch with --electron, which starts Node’s inspector too.

<app>what runs
the app’s directory, or its main script (.js, .cjs, .mjs)the Electron binary of the electron npm package found in node_modules from the app upward (dist/electron, dist/Electron.app/Contents/MacOS/Electron, dist/electron.exe), with the app as its first argument
an executable (Electron itself, or a packaged app)that file; with plain Electron, pass the app with --electron-arg
a macOS .app bundleits Contents/MacOS executable
a name with no path, not a file herethat command, found on PATH

--electron-arg <arg> (repeatable; --electron-arg=<arg> for one that starts with a dash) adds arguments after the app. The app also starts with --inspect=0, Node’s inspector on its main process: eval --main <js> runs there (an expression, or a function called with the electron module, awaited: eval --main "({ app, BrowserWindow }) => BrowserWindow.getAllWindows().length"); MCP clients pass main: true to browser_evaluate. An app that turns the switch off (Electron’s EnableNodeCliInspectArguments fuse) still runs; eval --main then says so. Every window the app opens is adopted as a tab (DevTools windows are not). One it opens after the agent has acted, navigated or picked a tab becomes the active one, though it has no opener as a popup does; one it opens on its own before that (a start-up window that shows up after start chose one) is chosen among as at start, so the session stays in the largest window the app shows. A window that closes itself is dropped and the session goes on in the one before it. quit ends the app (its whole process group, or its job object on Windows). A sandbox this machine can’t give it (root, Ubuntu 24.04’s AppArmor rule for unprivileged namespaces, a setuid helper that isn’t set up) is handled as for Chrome: the app runs with --no-sandbox and the warning says so.

What differs from a browser session: the app’s windows are its own, so tab-new (the app opens windows itself), reset (its storage is its own; quit and start again), show and hide (it is a visible app) are refused with that reason. --electron is an alternative to --attach, --profile and --engine lightpanda, and always gets a process of its own (never the shared browser). On Linux the app needs a display: on a server, run the whole session inside one xvfb-run (or start one Xvfb and export DISPLAY); an xvfb-run per command ends the display, and the app with it, when that command does. An app that removes --remote-debugging-port from its command line can’t be driven; start says so with the app’s last stderr lines. CI runs tests/electron.rs against tests/fixtures/electron-app on Linux, macOS and Windows.

Natural mode: --natural (or NAVIGERA_NATURAL=1). What headless Chrome shows a page that a windowed one doesn’t, removed: the user agent says HeadlessChrome in navigator.userAgent, in workers and in every request header; it becomes the one the same Chrome sends with a window (its major version, the reduced form Chrome sends). The screen is 800x600 behind a 1440x900 window; it becomes 1920x1080. The full Chrome is used, not chrome-headless-shell, which is a different program; --natural with a chrome-headless-shell given (--chromium, $CHROME_BIN) is refused, since its client hints, plugins and window.chrome say headless whatever the flags. In every mode, navigator.webdriver is false, there is no --enable-automation, and a --profile browser starts with no automation flag. The CI check is tests/handoff.rs against tests/fixtures/natural_site.py, a page that runs the checks anti-bot scripts make: webdriver, user agent and client hints (page, worker, header), Runtime.enable side effects, plugins, languages, window.chrome, window and screen sizes, notification permissions. What natural mode does not do: solve CAPTCHAs, or fake a fingerprint (WebGL on a machine without a GPU still reports a software renderer).

Diagnostics:

  • NAVIGERA_TIMINGS=1 prints launch phases at shutdown.
  • NAVIGERA_CDP_TRACE=<file> records every CDP command sent; CI checks it with tools/cdp_check.py.
  • NAVIGERA_VERBOSE=1 turns on launch logs.
  • NAVIGERA_CHROME_LOG=<file> appends the browser’s own stderr there.
  • NAVIGERA_RSS_REPORT=1 prints navigera’s peak memory when a serve ends.
  • Off switches, for telling a navigera behaviour from the page’s: NAVIGERA_NO_NETWORK=1 (no Network domain: network stays empty and ops don’t wait for data), NAVIGERA_NO_CLICK_CHECK=1 (clicks don’t watch what they changed: no no_effect), NAVIGERA_CLICK_AGAIN=0 (a click nothing listened for isn’t sent again once the page wires it up), NAVIGERA_NO_DISCOVER=1 (popups aren’t adopted as tabs).

Made from docs/navigera.md.