browser

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package browser holds the connection-layer logic for reaching Chrome over CDP: the DevToolsActivePort reader (Path B), the endpoint resolution, the vocabulary a probe answers in (WSState), and the connection-ladder decision.

It is deliberately free of chromedp AND of any I/O against Chrome itself, so it unit-tests without a live browser. The socket work that classifies an endpoint used to live here and no longer does: see chrome.AwaitUpgrade.

Index

Constants

View Source
const ConsentPromptAdvice = "Chrome is holding its \"Allow remote debugging?\" consent prompt. " +
	"The prompt is browser-modal and can sit BEHIND the Chrome window, and Chrome accepts no other input until it is answered, " +
	"so a browser that looks frozen or crashed is usually this dialog. Find it and click Allow."

ConsentPromptAdvice is the single authored explanation of Chrome's consent dialog, for every place that has to describe it: the connect timeout, the generic dial failure, the daemon's wait notice, the client's give-up message, and doctor's consent_pending state.

Every clause is here because a user cannot deduce it. That the dialog is modal to the BROWSER is why the frozen window is a symptom and not a crash; that it can sit behind the window is why they have not seen it; that nothing else in Chrome responds until it is answered is why the tool looks like the thing that broke. Five hand-written copies had already drifted — one said "behind" where the others shouted it, one said "blocks all other input" — and two test files asserted on a substring list that one of those copies would have failed.

View Source
const EnableAdvice = "relaunch Chrome with --remote-debugging-port=9222 " +
	"(on macOS: open -a \"Google Chrome\" --args --remote-debugging-port=9222), which never prompts; " +
	"or enable chrome://inspect/#remote-debugging, which raises a consent prompt on every fresh attach"

EnableAdvice is the single authored answer to "how do I make Chrome debuggable?", and it leads with the launch flag ON PURPOSE.

--remote-debugging-port skips the consent dialog entirely. The chrome://inspect toggle raises a browser-modal prompt on every fresh attach, and every message in this tool used to recommend it first — which routed each new user straight through the failure RFC-0013 exists to remove. The order of these two clauses is the fix.

Variables

This section is empty.

Functions

func CandidatePortFiles

func CandidatePortFiles() []string

CandidatePortFiles returns the OS-specific locations Chrome writes DevToolsActivePort to (Chrome family), first match wins. Overridable by the caller via CHROME_CDP_PORT_FILE.

func EndpointKey

func EndpointKey(portFile string, port int) string

EndpointKey identifies the debug endpoint a command targets, so the daemon socket and sticky state are keyed to the actual Chrome instance rather than a fixed port. An explicit --port wins (distinct ports get distinct keys); otherwise the key comes from the discovered DevToolsActivePort file, falling back to "default".

func FindPortFile

func FindPortFile(override string) string

FindPortFile returns the DevToolsActivePort path to use: an explicit override, else $CHROME_CDP_PORT_FILE, else the first existing OS candidate ("" if none).

func HostPort

func HostPort(wsURL string) (string, bool)

HostPort extracts the host:port authority from a ws:// URL.

func ParseDevToolsActivePort

func ParseDevToolsActivePort(content []byte) (port int, wsPath string, err error)

ParseDevToolsActivePort parses the two-line DevToolsActivePort file (line 1 = port, line 2 = the browser-level ws path).

func WSURLFromPortFile

func WSURLFromPortFile(path string) (string, error)

WSURLFromPortFile reads a DevToolsActivePort file and returns the literal browser-level WebSocket URL to hand to chromedp.NewRemoteAllocator.

Types

type Action

type Action int

Action is the connection-ladder outcome for a given Probe.

const (
	Attach           Action = iota // attach to Probe.Endpoint (Path B)
	Launch                         // launch a managed Chrome (Path A fallback)
	InstructToggle                 // Chrome is running but not debug-enabled — guide the launch flag / chrome://inspect
	InstructNoLaunch               // nothing debug-enabled and --no-launch — print the launch command
	ConsentPending                 // open port, hanging upgrade — Chrome is holding its consent prompt
)

func DecideConnection

func DecideConnection(p Probe) Action

DecideConnection walks the connection ladder:

  1. upgrade completed -> Attach (Path B)
  2. open port, hanging upgrade -> ConsentPending (Chrome is asking the user, not failing)
  3. no reachable endpoint, Chrome up -> InstructToggle (use the running session; never shadow it)
  4. no reachable endpoint, no Chrome -> Launch (Path A) unless --no-launch
  5. ...with --no-launch -> InstructNoLaunch

Rungs 1 and 2 are separate only because WS is three-way: see WSState.

func (Action) String

func (a Action) String() string

String names an Action. Its only callers are %v in test failure messages, which is reason enough: the alternative is a diff that says "= 4, want 2".

type Endpoint added in v0.2.0

type Endpoint struct {
	// URL is the ws:// (port-file path) or http:// (explicit --port) endpoint,
	// or "" when none was found.
	URL string
	// PortFile is the DevToolsActivePort file URL came from, "" when an
	// explicit --port was used (there is no file in that case).
	PortFile string
	// Err is set when a port file was found and could not be read or parsed —
	// distinct from "no endpoint", because the remedy is different.
	Err error
}

Endpoint is where a command should try to reach Chrome, and how that was decided. It exists because two callers have to agree on it: chrome.Connect, which attaches, and `doctor`, which diagnoses. doctor used to read the port file directly and ignore --port entirely, so `doctor --port 9333` probed a different browser than the one the flag named and reported IT healthy.

func FindEndpoint added in v0.2.0

func FindEndpoint(portFileOverride string, port int) Endpoint

FindEndpoint resolves the debug endpoint from an explicit port, else the DevToolsActivePort file. An explicit --port wins: it names a specific Chrome, and a port file naming a different one is not a fallback for it.

type Probe

type Probe struct {
	// Endpoint is where Chrome was looked for: a ws:// URL from
	// DevToolsActivePort, an http:// one from --port, or "" if neither
	// resolved. It was called PortFileWS, which was untrue of half the values
	// it holds.
	//
	// The ladder does not read it: WS already implies it, since an upgrade can
	// only be attempted against an endpoint. It is here because a Probe is a
	// record of what was observed, and "where" is part of that.
	Endpoint      string
	WS            WSState // what one WebSocket upgrade against Endpoint did
	ChromeRunning bool    // is a Chrome process running (possibly without debug)?
	NoLaunch      bool    // the --no-launch flag
}

Probe captures the observable connection state the ladder decides on.

type WSState added in v0.2.0

type WSState int

WSState is what one WebSocket upgrade against Chrome's browser-level debug endpoint actually did. It is three-way, and that is the whole point.

While consent for a fresh attach is pending, Chrome does not refuse the connection: it accepts the TCP connect, then holds the upgrade open and says nothing until the user answers a browser-modal dialog. There is no error to classify — only silence. A boolean "reachable" collapses that silence into the same value as a refused port, so the tool cannot tell "nothing is listening" (a real failure, and fast) from "Chrome is waiting for a human" (not a failure at all, and slow by nature). Splitting them is what lets a refused endpoint keep failing in milliseconds while a pending one is waited out for minutes.

Note that Chrome's HTTP JSON API is NOT a substitute signal: on the chrome://inspect toggle path GET /json/version answers 404 whether or not consent has been granted. Only the upgrade distinguishes the states.

const (
	// WSRefused: nothing accepted the connection, or something answered the
	// upgrade with anything other than 101 (a stale port file, a different
	// server on the port). A real failure.
	WSRefused WSState = iota
	// WSPending: the port accepted and the upgrade never completed. This is the
	// consent signature.
	WSPending
	// WSReady: the upgrade completed — the endpoint is live and consented.
	WSReady
)

func (WSState) String added in v0.2.0

func (s WSState) String() string

String is the WIRE value of this state: it is what `doctor` reports as the envelope's `state` field, and callers branch on it. There is exactly one vocabulary for these three answers on purpose. There used to be two — this method said "refused" where doctor said "no_endpoint" — kept apart only by this method having no callers at all, which is not a design, it is a coincidence that was one use away from shipping two names for one state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL