Files
browser/src/ToolSession.zig
T
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

116 lines
4.0 KiB
Zig

// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! An isolated browsing context driven through `lp.tools`: its own Browser
//! (hence V8 isolate), Session, Notification and node Registry. `self` must
//! not move after `init` — Browser registers self-pointers.
const App = @import("App.zig");
const Browser = @import("browser/Browser.zig");
const Session = @import("browser/Session.zig");
const Notification = @import("Notification.zig");
const NodeRegistry = @import("NodeRegistry.zig");
const ToolSession = @This();
browser: Browser,
session: *Session,
notification: *Notification,
registry: NodeRegistry,
/// Leaves the browser's isolate entered, like `Browser.init`; callers
/// sharing one thread between several isolates park it with
/// `exitIsolate` afterwards.
pub fn init(self: *ToolSession, app: *App) !void {
self.notification = try .init(app.allocator);
errdefer self.notification.deinit();
self.registry = .init(app.allocator);
errdefer self.registry.deinit();
try self.browser.init(app, .{});
errdefer self.browser.deinit();
try self.restartSession();
}
/// Replace the session with a fresh one on the same browser: page, cookies,
/// storage and history gone. The old `session` pointer is invalid afterwards.
pub fn restartSession(self: *ToolSession) !void {
self.session = try self.browser.newSession(self.notification);
try self.session.enableConsoleCapture();
}
/// The isolate must be current (`enterIsolate` if parked): Browser.deinit's
/// Env.deinit exit has to balance against this context's isolate.
pub fn deinit(self: *ToolSession) void {
self.registry.deinit();
self.browser.deinit();
self.notification.deinit();
}
/// V8's "current isolate" is a per-thread stack: when several contexts
/// share a thread, bracket any use of the Browser/Session with
/// enterIsolate/exitIsolate and leave it un-entered otherwise.
pub fn enterIsolate(self: *ToolSession) void {
self.browser.env.isolate.enter();
}
pub fn exitIsolate(self: *ToolSession) void {
self.browser.env.isolate.exit();
}
const tools = @import("browser/tools.zig");
const testing = @import("testing.zig");
test "ToolSession: isolates interleave on one thread and tear down balanced" {
const arena = testing.arena_allocator;
var a: ToolSession = undefined;
try a.init(testing.test_app);
try loadBlank(&a);
a.exitIsolate();
var b: ToolSession = undefined;
try b.init(testing.test_app);
try loadBlank(&b);
b.exitIsolate();
a.enterIsolate();
try testing.expectEqual("a", (try tools.evalScript(arena, a.session, &a.registry, "globalThis.tag = 'a'")).text);
a.exitIsolate();
b.enterIsolate();
try testing.expectEqual("undefined", (try tools.evalScript(arena, b.session, &b.registry, "String(globalThis.tag)")).text);
b.exitIsolate();
a.enterIsolate();
try testing.expectEqual("a", (try tools.evalScript(arena, a.session, &a.registry, "globalThis.tag")).text);
a.deinit();
b.enterIsolate();
b.deinit();
}
fn loadBlank(ts: *ToolSession) !void {
const page = try ts.session.createPage();
try page.navigate("about:blank", .{});
var runner = ts.session.runner(.{});
try runner.waitForFrame(page.frame_id, 2000, .{ .until = .done });
}