browser

package
v0.0.9 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package browser owns the Chrome lifecycle: how it is launched, how the persistent profile is guarded, and how pages are opened against it.

Everything Chrome-shaped lives here so the rest of the project can be written and tested without a browser present.

Index

Constants

This section is empty.

Variables

View Source
var ErrProfileInUse = errors.New("chrome profile is already in use")

ErrProfileInUse reports that another Chrome already holds the profile. It is a sentinel so callers can branch on it instead of matching error text.

Functions

func ChromePathForTest added in v0.0.5

func ChromePathForTest() string

ChromePathForTest exposes Chrome discovery to tests in this package's suite that need a real browser and should skip when none is installed.

func ClearStale

func ClearStale(profileDir string) error

ClearStale removes a leftover profile lock. Callers must confirm no Chrome is running against the profile first; removing a live lock invites two instances onto the same profile.

func InUse

func InUse(profileDir string) bool

InUse reports whether a Chrome instance currently holds the profile.

This is the single authority on that question. Everything else in the project -- a pool retiring a session, a write handing the profile back, a login window taking it -- used to answer it from its own bookkeeping, and bookkeeping is a cache: every place it was kept was a place it could be dropped, and dropping it puts two browsers on one directory.

The directory itself cannot be out of date. Chrome writes SingletonLock as a symlink to "host-pid" on startup and removes it on a clean quit; a killed Chrome leaves it behind. So the lock existing is not ownership -- ownership is the process it names still being alive. A lock naming a dead process is stale and the profile is free.

An unreadable lock, or one issued by another host, is treated as in use. That is the safe direction throughout: refusing to start is recoverable and self-clearing, while two Chromes on one profile corrupts it. The same applies to the one way this can be wrong -- an exited Chrome whose pid has since been reused -- which reads as held until that process ends.

func Launcher

func Launcher(opts Options) *launcher.Launcher

Launcher builds the Chrome launcher.

It is pure configuration: no directories are created and no process is started, so the resulting flags can be asserted on in tests. Opening a browser is Open's job.

func WaitUntilFree added in v0.0.5

func WaitUntilFree(ctx context.Context, profileDir string, budget time.Duration) error

WaitUntilFree blocks until no Chrome holds the profile.

Callers that have just shut a browser down use this to confirm the handoff instead of trusting that Close did what it was asked. It returns an error if the profile is still held when the budget runs out, so an unconfirmed shutdown stays unconfirmed rather than quietly becoming a success.

Types

type Options

type Options struct {
	ChromePath  string
	ProfileDir  string // --user-data-dir
	ProfileName string // --profile-directory
	Headless    bool
}

Options describes how to start Chrome.

type Page

type Page struct {
	// contains filtered or unexported fields
}

Page is a browser tab with the navigation behavior X requires.

func (*Page) Close

func (p *Page) Close()

Close closes the tab, using the browser's context so it still works after the caller's deadline has passed.

The tab is closed at the target level rather than through the page, because the page-level close runs the document's beforeunload hooks. X registers one on its composer: a write that has just posted still counts as unsaved work, so closing the tab raises a "Leave site?" dialog and then waits for someone to answer it. Nobody is there to -- the browser is being torn down -- so the write browser sits behind a modal, holding the profile until the shutdown wait gives up on it.

Discarding the tab outright is the right behaviour here regardless. Anything the page still wants to save is a side effect of automation we have already finished with.

func (*Page) Goto

func (p *Page) Goto(url string) error

Goto navigates to url and waits for the load event.

The first automated page load after a manual login can lose its target to Chrome restoring the previous session's tabs, which CDP reports as "Inspected target navigated or closed". That is transient, so one retry is enough.

func (*Page) Has

func (p *Page) Has(selector string, wait time.Duration) bool

Has reports whether an element matching selector is present, waiting briefly for it to render. A missing element is an answer, not an error.

func (*Page) Rod

func (p *Page) Rod() *rod.Page

Rod exposes the underlying page for callers that need raw access, such as evaluating extraction scripts.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is a running Chrome instance and its rod connection.

func Open

func Open(ctx context.Context, opts Options) (*Session, error)

Open starts Chrome and connects to it. It returns only once the launch has resolved, one way or the other.

There is deliberately no way to give up on a launch early. An abandoned launch is worse than a slow one: the Chrome it started still arrives, still takes the profile, and nothing is tracking it -- which is exactly the duplicate-browser failure the caller was trying to avoid. Callers that cannot wait should stop waiting on their own; ownership stays with whoever started the launch until it finishes.

When a persistent profile is configured it is created if missing and checked for a competing Chrome first; Open fails with ErrProfileInUse rather than letting two instances corrupt each other's state.

func (*Session) Alive added in v0.0.5

func (s *Session) Alive(ctx context.Context) bool

Alive reports whether the browser is still reachable.

Chrome can go away without this process being told -- a crash, an OOM kill, or the user quitting it -- after which every call fails with a closed connection. A pooled session is checked before reuse so a dead browser is replaced rather than handed out repeatedly.

func (*Session) Close

func (s *Session) Close() error

Close shuts down Chrome and does not return until the profile is free.

rod's Kill signals the process and returns immediately, so Close completing is not by itself proof that Chrome let go. Anything that takes the profile next -- a login window, a write, a replacement read -- would then be starting against a directory the old process still holds. Close therefore waits for the process to exit, and clears a lock file that a killed Chrome left behind.

func (*Session) Cookies

func (s *Session) Cookies() ([]*proto.NetworkCookie, error)

Cookies returns every cookie the browser currently holds.

func (*Session) OnClose

func (s *Session) OnClose(fn func())

OnClose registers a callback run once when the session closes.

func (*Session) Page

func (s *Session) Page(ctx context.Context) (*Page, error)

Page opens a blank page whose operations are bound to ctx.

A shared browser lives for the life of the server, and pages inherit their browser's context, so without rebinding here a caller's deadline would never reach Goto, WaitLoad or Eval -- a stalled navigation would hold its lease indefinitely and block anything waiting to reserve the profile.

Closing deliberately uses the browser's context rather than ctx: cleanup has to succeed after the caller has given up, or abandoned tabs accumulate in a browser that is never restarted.

func (*Session) ProfileDir added in v0.0.5

func (s *Session) ProfileDir() string

ProfileDir reports the persistent profile this session runs against, or "" for a throwaway one. Callers use it to confirm the profile is free after a shutdown they could not otherwise verify.

Jump to

Keyboard shortcuts

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