Commit Graph
11 Commits
Author SHA1 Message Date
Karl Seguin 2ace9d1548 make socket ownership more explicit and future-proof 2026-09-11 05:24:12 +08:00
Karl Seguin 8ad9eaf48d webdriver: HTTP WebDriver session management
This is a small step towards WebDriver supports (non-bidi). It allows creating
and deleting a BiDi "Session" (e.g. a worker). It also allows attaching a BiDi
driver to an HTTP-created BiDi session (the typical selenium startup flow).

This change unblocks the most basic setup/teardown of Selenium, so it still
isn't enough to actually use a Selenium script as-is. But it's significant
because it models a worker (thread) that isn't tied to a WebSocket, something we
haven't had before.

A consequence of a pure HTTP Session is that we don't have a clear cleanup
signal. There is no "the socket is disconnected". There's a new HTTP reaper
which kills HTTP Sessions after --http-session-timeout. It's expected that
drivers properly DELETE /session/:id. I imagine we're going to run into
--cdp-max-connections limits and need to tweak this code. BUT, this entire flow
is only enabled with --protocol webdriver, so it won't impact exiting CDP users.
2026-09-11 05:11:26 +08:00
Karl Seguin 2e6999f20b chore: make declarations private if they don't need to be public
This change is 99%  s/pub//   + a handful of dead code removal.
2026-09-10 14:42:09 +08:00
Karl Seguin 87320a506d chore: Move ownership of the Inbox from HttpClient to WebSocker Driver
Currently, the HttpClient owns the inbox and its borrowed by the Link. This is
a bit backwards, but it also means that we can't eagerly create a Link: the
Link needs the inbox, so it needs the HttpClient, which is created by the
Browser (which creates an Isolate).

Remember, the Inbox is one of the few things shared between the main thread
and the worker, so either end can own it and the other can borrow it.

This switches the ownership so that the HttpClient now borrows the Inbox from
the Server's side of the Link (the WebSocket).

The main goal of this change is to prepare for more advanced HTTP WebDriver
flows. The more we can create _without_ a Browser, the fewer edge cases we have
to deal with (Browser because it's expensive and has to be created on the
Worker thread due to how V8::Isolate works).
2026-09-03 16:51:12 +08:00
Karl Seguin 17c2aa7d1f Hardening
Give it one pass through some Claude fuzz testing. Add a max message size,
protect against weird interactions during a shutdown and we had some pending
accepts. Put a time limit on blocked writes.
2026-09-03 06:47:53 +08:00
Karl Seguin 7c51dfff05 bidi: add skeleton for bidi
1. Abstract "Driver". Non-CDP things that referenced *CDP now reference a Driver
2. Move the NodeRegistry out of CDP. This created an artificial link between
   agent / mcp and CDP
3. Add BiDi driver with enough to navigate to a page
2026-08-31 22:44:59 +08:00
Karl Seguin bdce49a4b5 refactor: Introduce Arena wrapper
Introduces a ArenaAllocator wrappre (lp.Arena). This is groundwork for better
memory tracking and reporting memory usage to v8. This is almost purely a
mechanical change to lay a foundation for a follow up PR that will address
https://github.com/lightpanda-io/browser/issues/3027

Some code became a bit leaner: a pooled arena can release itself (it has a
reference to the ArenaPool).

Some code became uglier: The Frame has a `_local_arena: *lp.Arena` and a
`local_arena: Allocator` (same with call_arena, and same with a few other types)
so that consumers aren't impacted (they continue to use `frame.local_arena`).
2026-07-30 13:02:13 +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
Scott Taylor 0bfd0222f2 fix(http): abort blocking-script sync wait when a teardown command is queued
A blocking <script src> loads via HttpClient.syncRequest, which spins on
tick(200, .sync_wait). That drain mode only dispatches Fetch-interception
methods (it can't free Page/Frame state mid-parse), so Target.closeTarget /
Target.disposeBrowserContext / Page.close sit undispatched until the blocking
fetch completes — i.e. up to the per-request timeout, per blocking script.

For storefronts with slow/never-completing third-party subresources this
stalls page.close()/context dispose for tens of seconds and collapses CDP
throughput under concurrency (the process stays up; other connections are
unaffected).

Fix: while waiting, peek the inbox; if a teardown/close command (or a
client close/disconnect) is queued, abort the in-flight blocking transfer.
syncRequest then returns the same way it already does on a fetch failure,
the parser unwinds to the next safe .all drain, and the queued command
runs there. No command is dispatched mid-parse, so no UAF.

Repro: 8 workers looping newContext->goto(commit)->page.close against a page
with 15 never-answered subresources went from 0.1 ctx/s (page.close stalling
~30-60s) to ~25 ctx/s; a no-hanging-subresource baseline is unchanged.

Fixes #2646
2026-06-04 23:17:24 -04:00
Karl Seguin 88b98e705f Capture disconnect/close in Worker
Currently, if a disconnect/close is captured in a worker during a syncRequest,
that specific request is terminated, but the error doesn't bubble up. The worker
remains alive and will subsequently block in a perform, with no connection alive
to wake it up.

In this commit, when disconnect/close is received, inbox.terminate is set to
true. This flag is checked (in syncRequest and http_client.tick) and
error.ClientDisconnected is returned.

(Also, on network shutdown, always broadcast the cdp_unregister since there's
no harm in sending an extra signal even if nothing was removed).
2026-05-21 10:38:52 +08:00
Karl Seguin 875c147783 Main/Network reads CDP socket
Previously, the CDP socket was added to the worker's multi and fully owned
by the worker. While this is simple, it introduced some issues:

1 - Cannot detect a disconnected client during JS processing ( for(;;) )

2 - A blocked worker can cause back-pressure that blocks the client. This can
    cause a deadlock if the worker is blocked waiting for a CDP message

In addition to these 2 problems, there was 1 other serious CDP-related issue:
arbitrary CDP messages could be processed during JavaScript callback. For
example, a Worker calls importScripts while request interception is enabled,
this requires us to tick the HttpClient waiting for the interception response.
But, a client could sent Target.closeTarget, which we'd process and delete the
frame..all while importScripts is still blocked. Assuming importScripts unblocks
everything is a big UAF since the frame (and its workers) were cleared from
closeTarget.

The CDP socket is now read from the network (main) thread and an OTP-style
mailbox is used. The network thread posts message to the Worker's inbox and
signals it to wakeup. This solves #1 and #2. It doesn't directly solve the
reentrancy issue, but it provides the foundation. Specifically, in introduces
a queue for of CDP message and more control over when/how that queue is
processed. At "safe points" (Runner.tick, HttpClient.tick), any message can
be processed. But, when inside a JavaScript callback, we can process only non-
destructive/mutating message. Specifically, we can process only messages related
to request interception.
2026-05-19 20:52:21 +08:00