Commit Graph
247 Commits
Author SHA1 Message Date
Karl SeguinandAdrià Arrufat 789ec07545 screenshot: --dump png and Page.captureScreenshot with a Zig rasterizer
Lightpanda has no layout engine, so a screenshot is the page's text
content flowed into blocks, the same content the markdown dump produces,
word-wrapped and rasterized. The DOM walk (src/browser/screenshot.zig),
CDP wiring, viewport/clip/scale handling and tests are from #3231; the
rasterizer behind the block/span model is Zig instead of a Rust crate:

- z2d (pure Zig, MPL-2.0) for glyph outlines, antialiased fills and
  compositing, from the arrufat/z2d fork's lightpanda branch: upstream
  plus one line exporting glyph metrics, since z2d has no public
  text-measurement API and takes no public pull requests.
- the ICU bundled in V8's static archive for bidi (ubidi), Arabic
  shaping to presentation forms (u_shapeArabic) and UAX #14 line
  breaking (ubrk). Symbols carry ICU's major version. The shared dev
  build of V8 hides them behind its version script; until a prebuilt
  ships with lightpanda-io/zig-v8-fork#201, build and test with
  -Dprebuilt_v8_path pointing at the static archive.
- gpos.zig, a reader for the fonts' mark-to-base, mark-to-ligature and
  mark-to-mark lookups, so harakat, niqqud and combining Latin accents
  anchor to their base. Tashkeel come back from ICU as FE70-block forms
  with no anchors and are mapped back to the bare marks first.
- glyph masks rasterized once per unique glyph and quarter-pixel phase
  into an alpha8 scratch sized from the outline's bbox, then blitted
  directly; PNG streamed scanline-in / IDAT-out through the zlib the
  binary already links. Renderer state is arena-owned per call.
- four bundled DejaVu 2.37 faces, including Sans Mono Bold.

Output matches the parley/tiny-skia renderer of #3231 line for line on
Latin, Arabic, Hebrew, mixed-direction and vocalized text; release
builds render a dense page in about 23ms against 58ms.

Co-authored-by: Adrià Arrufat <adria.arrufat@gmail.com>
2026-08-24 23:39:46 +02:00
Karl Seguin 09edfd3ce5 http: Add optional rate limit
Add an optional per-host rate limit. This currently only applies to the top-
level navigation. This makes it simpler to implement and simpler to reason
about. The full load of a page is only ever delayed at its head, not
sporadically through the page loading.

The use-case where a RateLimiter is most useful is when the browser is crawling
multiple pages of the same site, and in that case, the top-level rate limit
still applies some degree of limit to any linked resources (e.g. a JS on a
different host).

`--http-nav-delay` is the delay, in milliseconds, to apply to top level
navigates per host. Currently defaults to 0 (disabled).

`--http-nav-burst` is the burst allowed per host. Defaults to 1 (has no impact
when `--http-nav-delay` is disabled).
2026-08-20 16:03:42 +08:00
Karl Seguin d3c26f589e Merge pull request #3219 from lightpanda-io/sec-ch-ua-full-version
send Sec-Ch-Ua-Full-Version-List header
2026-08-18 20:00:44 +08:00
Pierre Tachoire 0a0164c972 remove useless null terminated version_z 2026-08-18 12:28:21 +02:00
Pierre Tachoire a961156283 send Sec-Ch-Ua-Full-Version-List header
Add Sec-Ch-Ua-Full-Version-List header with the full LP version

```
Sec-Ch-Ua-Full-Version-List: "Lightpanda";v="1.0.0-dev.8713+33867c727"
```
2026-08-18 09:38:00 +02:00
Karl Seguin 5c0aa90ea4 Merge pull request #3187 from lightpanda-io/http-header-option
add --http-header option to send headers on every HTTP requests
2026-08-18 15:24:10 +08:00
Pierre Tachoire 33867c7275 Merge pull request #3116 from lightpanda-io/nikneym/adblocker-init
Initial implementation for easy list parsing and AdBlocker
2026-08-18 08:50:42 +02:00
Karl Seguin a9b96fe5f8 Centralize header validation
Mime.isHttpToken already exists for the header name, added
Mime.isHttpHeaderValue which was extracted from het/Headers.zig. Now Config (
for validating CLI headers), net/Headers for script headers, and cdp/network.zig
all use the same validation methods.
2026-08-18 13:02:15 +08:00
Pierre Tachoire a6afdce444 add --http-header option to send headers on every HTTP requests 2026-08-17 17:18:13 +02:00
Karl Seguin 737f69ee44 http: improve header overwrite/enforcement
Builds ontop of https://github.com/lightpanda-io/browser/pull/3200 to centralize
header enforcement and standardize merge vs overwrite header logic.

The API is still a `setHeader` and `appendHeader`, with a source, but set/append
both are thin wrappers around private `putHeader`. putHeader blocks overwriting
restricted headers (user-agent). The `source` acts as a priority (ordered enum)
which further restricts the header AND depending on whether set or append were
called, controls if the value is overwritten or appended to.

3200 had an always-append which can cause problems, e.g. a script setting
Accept-Language: fr would have results in the value being appended to the
default, e.g.: 'en-US,en;q=0.9, fr'.
2026-08-15 12:09:08 +08:00
Pierre Tachoire ca4726c1cd add --user-agent unit test 2026-08-14 19:04:16 +02:00
Pierre Tachoire 859543ed3e add missing user-agent validator for CLI 2026-08-14 18:35:55 +02:00
Halil Durak 835cf8917b update tests 2026-08-13 16:39:13 +03:00
Halil Durak b1b9a64573 CLI option for adblock list passing + integrate adblocker to http client 2026-08-13 16:39:13 +03:00
Pierre Tachoire b340071d03 config: only flag wildcard bind when no --advertise-host is set 2026-08-13 09:54:28 +02:00
Pierre Tachoire 1f4a060765 add a func to detect wildcard host 2026-08-13 09:21:42 +02:00
guyua9 8be30d56f8 cdp: fall back to loopback when /json/version bind host is a wildcard
buildJSONVersionResponse() previously formatted whatever Config.advertiseHost()
returned straight into webSocketDebuggerUrl. When serve was started with
--host 0.0.0.0 (or ::) without an explicit --advertise-host, advertiseHost()
returned the literal wildcard, so CDP discovery handed remote clients a
URL like ws://0.0.0.0:9222/ that can never be dialed. This was the root
cause reported in issue #1922: Playwright MCP and chromedp fail with
ECONNREFUSED when connecting through the official Docker image, which
sets --host 0.0.0.0 unconditionally.

AdvertiseHost now substitutes 127.0.0.1 for wildcard binds so the
discovery URL stays connectable from the same host (covers the official
Docker image via its published port, plus WSL's localhost bridge).
Users who serve from a dedicated interface can still pin the URL with
--advertise-host.

buildJSONVersionResponse now emits a one-time log.note when the bind
host is a wildcard so operators know to set --advertise-host for
multi-host setups. Tests cover the wildcard, the explicit-override and
the concrete-host cases.
2026-08-11 18:20:42 +08:00
Muki Kiboigo 32364a26da use proper default on http_cache_entry_limit option 2026-08-07 08:01:55 -07:00
Muki Kiboigo e26a200146 add http cache entry limit options 2026-08-06 13:29:05 -07:00
Muki Kiboigo b2727a1ab4 remove Storage and FsCache 2026-08-03 18:17:43 -07:00
Karl Seguin fd2e6a8512 refactor: rework how httpclient headers work
Adding headers to an HTTP request was a bit awkward due to my desire to avoid
having an intermediate representation (e.g. an ArrayList(Header)). Going
straight to a curl slist avoids double-copying the headers (first to Zig, then
to curl).

But the CORS work (https://github.com/lightpanda-io/browser/pull/3002) showcases
that this micro-optimization simply isn't worth it, since it needs that
intermediate representation anyways.

And, this change isn't just for CORS. Headers have been a silly pain in the past
like unclear ownership, and messy APIs used in _a lot_ of places (WebBotAuth,
WebSocket, Fetch, ...)

This new approach stores headers on the transfer in an ArrayList. The API is:

```
const transfer = try client.newRequest(.{...}, owner);
{
    errdefer transfer.deinit();
    try transfer.addHeader("Over", "9000", .{});
}
try transfer.submit();
```

This:
1 - Eliminates ambiguity about errdefer cleanup responsibility
2 - Eliminates a bunch of stringZ concat that Frame, Config, CDP were doing
3 - Transfer.arena is now available for headers
2026-08-03 18:26:32 +08:00
Karl Seguin 6a83881634 chore: add lp.IS_DEBUG and lp.IS_TEST
Change all users to builtin.mode and built.is_test
2026-08-01 09:15:18 +08:00
Karl Seguin de85a51dda Merge pull request #3039 from lightpanda-io/fix-help-pager
cli: fix help pager broken by lp.io's failing allocator
2026-07-24 08:10:45 +08:00
Adrià Arrufat 4840a96645 cli: fix help pager broken by lp.io's failing allocator
std.Io.Threaded.init_single_threaded sets .allocator = .failing, which
spawnPosix uses to build the child's argv/env, so
std.process.spawn(lp.io, ...) always returns OutOfMemory and printPaged
silently fell back to plain output. Spawn the pager through a local
Threaded instance with a real allocator and the real environ (the
single-threaded one is empty, breaking PATH lookup of the less
fallback).
2026-07-23 12:16:19 +02:00
Matt Van Horn 311ab3aaae feat(cdp): add Network.setBlockedURLs and --block-urls request blocking 2026-07-23 12:15:47 +02:00
Halil Durak 9d6c54ac81 changes for Zig 0.16 2026-07-22 18:29:32 +03:00
Halil Durak 4eeb9f5ecf Config: add customCertStore helper
Returns a pointer to `X509_STORE` if custom CA supplied, else null.
2026-07-22 18:29:32 +03:00
Halil Durak 0e32b9c41f Config: remove dead isHashedDirectory function 2026-07-22 18:29:31 +03:00
Halil Durak 6a9a1f9729 Config: cert store creation is done in Config if custom CA given 2026-07-22 18:29:31 +03:00
Halil Durak cbaef301d9 Config: fix crash related to absolute paths
BoringSSL is fine with that still.
2026-07-22 18:29:31 +03:00
Halil Durak 03cfdbe567 Config: dupe received dir from arg iterator 2026-07-22 18:29:30 +03:00
Halil Durak d6ce3738a5 remove --disable-root-certificates flag 2026-07-22 18:29:29 +03:00
Halil Durak 1b15985e7f Config: drop local in hashed dir check 2026-07-22 18:29:29 +03:00
Halil Durak bf916e71af Config: remove unnecessary import 2026-07-22 18:29:28 +03:00
Halil Durak 11570202f3 Config: ensure passed --ca-path paths contain hashed certs 2026-07-22 18:29:28 +03:00
Halil Durak b7f61e5766 Network: support custom CA install 2026-07-22 18:29:27 +03:00
Karl Seguin c0c8d4dc70 Merge pull request #3018 from lightpanda-io/improve-dead-peer-detection
cdp: Improve dead-peer detection
2026-07-22 22:01:10 +08:00
Karl Seguin 29e31377ed cdp: Improve dead-peer detection
1 - On linux, set TCP_USER_TIMEOUT to 10 seconds. This closes the gap where
    TCP_KEEPALIVE doesn't trigger if there are un-ack'd writes

2 - shutdown the socket on cases where we know the peer is gone. This would
    unblock a worker blocked on a send()
2026-07-22 18:37:37 +08:00
Karl Seguin 8e42d63c1c zig: Zig 0.16
Built against https://github.com/lightpanda-io/zig-v8-fork/tree/zig-0.16 but
it doesn't require a new v8 build.

Built against https://github.com/lightpanda-io/boringssl-zig/tree/zig-0.16
since the current fork we point to isn't updated.

A global std.Io instance, lp.io. Way easier this way and requires 0 changes to
our libcurl integration / event loop.

Network code uses a new layer that does what Zig 0.15's posix package used to
do. Again, quicker migration that way. But, as long as we have the global IO,
and given the half-baked nature of networking in std.Io 0.16, this just makes
sense. Things can be migrated as needed.

The std.time.* -> std.Io.Timestamp/Clock/Duration resulted in _a lot_ of
changes. ArrayList = .{} -> ArrayList -> .empty also resulted in a lot of
changes, but that's obviously superficial. As is the trimLeft/trimRight ->
trimStart/trimEnd rename.

Locking adopt the `Uncancelable` variants, e.g. mutex.lockUncancelable() to
preserve the error-free signature (and, because cancellation would be something
we'd have to put more thought into).

std.json.ObjectMap is now unmanaged, so the allocator had to be passed along.
However, there's still a deprecated managed variant of MemoryPool, so I switched
to it (we can do a small follow up PR to move to the unmanaged after).

I tried use_llvm = false, but it locks my computer, consuming RAM until MacOS
gives me a popup I've never seen before, begging me to start killing processes.

Agent and the networking stuff saw the most significant changes.
2026-07-22 13:26:03 +08:00
Adrià Arrufat 805b60d626 Merge branch 'main' into run-command
# Conflicts:
#	src/Config.zig
#	src/help.zon
2026-07-17 22:46:47 +02:00
Adrià Arrufat f02fba40cd cli: drop redundant SIGPIPE ignore in the help pager
std.start.maybeIgnoreSigpipe() already installs a noop SIGPIPE handler
at startup (std.options.keep_sigpipe defaults to false), so a write to
a dead pager returns error.BrokenPipe — which the existing catch
handles — instead of killing the process. The manual SIG_IGN was also
the disposition std deliberately avoids, since ignored dispositions
survive exec and would leak to child processes.
2026-07-17 11:50:51 +02:00
Adrià Arrufat d06731d8fb cli: page help output through $PAGER when stdout is a tty
Explicitly requested help (help, help <cmd>, --help) now goes to stdout
instead of stderr, and is piped through $PAGER (fallback: less -FIRX)
when stdout is an interactive terminal, so long help like 'help agent'
(~225 lines) no longer clobbers the screen. Piped or redirected output
stays plain, and error-path usage remains unpaged on stderr.
2026-07-16 11:27:32 +02:00
Karl Seguin 4d81e48e44 ops: Add prometheus metrics
Adds a /metrics endpoint in serve mode. Can be disabled via --disable-metrics.

In fetch, dumps the metrics if --metrics is specified (defaults to false).

None of the metrics being collected are on a hot path, so they're just always
collected using atomic operations (i.e. no `if (!enabled) return;`). Because
the operations are cheap and infrequent enough not to matter.

The one place I want to add more metrics is in HttpClient (bytes, status,
counts, ...) but after the pending HttpClient-related PR is merged.

./lightpanda fetch --log-level fatal --metrics "https://lightpanda.io/"

```
build_info{version="1.0.0-dev.7837+70493ce35"} 1
cdp_connections_total 0
cdp_connection_limit_total 0
cdp_active_connections 0
cdp_commands_total 0
cdp_unknown_commands_total 0
js_heap_limits_total 0
script_errors_total 0
arena_hit_total{size="tiny"} 908
arena_hit_total{size="small"} 35
arena_hit_total{size="medium"} 0
arena_hit_total{size="large"} 27
arena_miss_total{size="tiny"} 193
arena_miss_total{size="small"} 31
arena_miss_total{size="medium"} 5
arena_miss_total{size="large"} 23
navigate_total{type="page"} 1
navigate_total{type="iframe"} 0
navigate_total{type="popup"} 0
js_heap_size_bytes_bucket{le="4194304"} 0
js_heap_size_bytes_bucket{le="8388608"} 0
js_heap_size_bytes_bucket{le="16777216"} 1
js_heap_size_bytes_bucket{le="33554432"} 1
js_heap_size_bytes_bucket{le="67108864"} 1
js_heap_size_bytes_bucket{le="134217728"} 1
js_heap_size_bytes_bucket{le="268435456"} 1
js_heap_size_bytes_bucket{le="536870912"} 1
js_heap_size_bytes_bucket{le="+Inf"} 1
js_heap_size_bytes_sum 11223040
js_heap_size_bytes_count 1
```
2026-07-14 09:17:14 +08:00
Adrià Arrufat 08eecfbbac cli: add run command for script replay
`lightpanda agent <script>` replays a saved script token-free (no LLM),
but running it under `agent` reads wrong. Add `run` as a dedicated,
script-only command: `lightpanda run <script>`.

`run` exposes only a required [SCRIPT] positional plus the common
options; its LLM-session flags (--task/--save/--list-models/REPL) are
absent, so they're rejected at parse time. `parseArgs` normalizes a
`run` invocation to an `.agent` command, reusing the existing token-free
replay path unchanged, so nothing downstream needs a `.run` case.

`agent <script>` keeps working. Telemetry records `run` as its own mode
code (R) via a new Config.command field, so its adoption is measurable
separately from agent script replay (AR).
2026-07-12 23:37:23 +02:00
Adrià Arrufat f66f0c191d mcp: add HTTP transport and multi-session support
Introduces an HTTP transport option to serve multipleagents from a
single process. Each connection is routed to its own isolated
browsing session using the `Mcp-Session-Id` header.

Also adds new session management tools (`session_new`, `session_list`,
`session_close`) and refactors the MCP server to support multiple V8
isolates with parking.
2026-07-12 15:27:20 +02:00
Karl Seguin a3ee14c69c default watchdog-ms to 30000 2026-07-09 21:03:56 +08:00
Karl Seguin 6f641b1250 feature: add watchdog for JS hangs
Adds a new command line argument `--watchdog-ms` which, when set, will terminate
any JS that appears to be hung.

When configured, a new thread is started. Workers heartbeat this thread to
signal activity (e.g. not stuck in a JS loop). However, workers also block for
their own reason (e.g. network polling), so they can signal the watchdog that
they are "entering a wait" and, when complete, that they are "existing a wait".
During such waits, the watchdog will not signal the isolate to terminate.

Obviously, it's important for workers to signal aliveness and whenever they plan
on doing a non-JS wait. So you could say we introduce safe points where the
watchdog (for that browser) is disabled. We could do the opposite: enable the
watchdog whenever we enter JS ("hey, I'm about to execute JS, monitor me). But
there are _a lot_ more place where this happens.
2026-07-09 20:59:58 +08:00
Halil Durak d8184dc462 move update command to version --check 2026-07-08 13:00:30 +03:00
Halil Durak be8628f235 update: add help command entry for update 2026-07-08 13:00:29 +03:00
Halil Durak 32fefeaa7a update: remove nightly channel check 2026-07-08 13:00:29 +03:00