Reorganize v8 contexts and scope

- Pages within the same session have proper isolation
  - they have their own window
  - they have their own SessionState
  - they have their own v8.Context

- Move inspector to CDP browser context
  - Browser now knows nothing about the inspector

- Use notification to emit a context-created message
  - This is still a bit hacky, but again, it decouples browser from CDP
This commit is contained in:
Karl Seguin
2025-04-28 21:04:01 +08:00
parent 0fb0532875
commit 2d5ff8252c
19 changed files with 1213 additions and 1236 deletions

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Dump = @import("dump.zig");
const Mime = @import("mime.zig").Mime;
@@ -53,13 +54,10 @@ pub const user_agent = "Lightpanda/1.0";
pub const Browser = struct {
env: *Env,
app: *App,
session: ?*Session,
session: ?Session,
allocator: Allocator,
http_client: *http.Client,
session_pool: SessionPool,
page_arena: std.heap.ArenaAllocator,
const SessionPool = std.heap.MemoryPool(Session);
page_arena: ArenaAllocator,
pub fn init(app: *App) !Browser {
const allocator = app.allocator;
@@ -75,31 +73,27 @@ pub const Browser = struct {
.session = null,
.allocator = allocator,
.http_client = &app.http_client,
.session_pool = SessionPool.init(allocator),
.page_arena = std.heap.ArenaAllocator.init(allocator),
.page_arena = ArenaAllocator.init(allocator),
};
}
pub fn deinit(self: *Browser) void {
self.closeSession();
self.env.deinit();
self.session_pool.deinit();
self.page_arena.deinit();
}
pub fn newSession(self: *Browser, ctx: anytype) !*Session {
self.closeSession();
const session = try self.session_pool.create();
self.session = undefined;
const session = &self.session.?;
try Session.init(session, self, ctx);
self.session = session;
return session;
}
pub fn closeSession(self: *Browser) void {
if (self.session) |session| {
if (self.session) |*session| {
session.deinit();
self.session_pool.destroy(session);
self.session = null;
}
}
@@ -114,33 +108,16 @@ pub const Browser = struct {
// You can create successively multiple pages for a session, but you must
// deinit a page before running another one.
pub const Session = struct {
state: SessionState,
executor: *Env.Executor,
inspector: Env.Inspector,
app: *App,
browser: *Browser,
// The arena is used only to bound the js env init b/c it leaks memory.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/181
//
// The arena is initialised with self.alloc allocator.
// all others Session deps use directly self.alloc and not the arena.
// The arena is also used in the BrowserContext
arena: std.heap.ArenaAllocator,
// Used to create our Inspector and in the BrowserContext.
arena: ArenaAllocator,
window: Window,
// TODO move the shed/jar to the browser?
executor: Env.Executor,
storage_shed: storage.Shed,
cookie_jar: storage.CookieJar,
// arbitrary that we pass to the inspector, which the inspector will include
// in any response/event that it emits.
aux_data: ?[]const u8 = null,
page: ?Page = null,
http_client: *http.Client,
// recipient of notification, passed as the first parameter to notify
notify_ctx: *anyopaque,
@@ -159,109 +136,45 @@ pub const Session = struct {
// need to play a little game.
const any_ctx: *anyopaque = if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx;
const app = browser.app;
const allocator = app.allocator;
var executor = try browser.env.newExecutor();
errdefer executor.deinit();
const allocator = browser.app.allocator;
self.* = .{
.app = app,
.aux_data = null,
.browser = browser,
.executor = executor,
.notify_ctx = any_ctx,
.inspector = undefined,
.notify_func = ContextStruct.notify,
.http_client = browser.http_client,
.executor = undefined,
.arena = ArenaAllocator.init(allocator),
.storage_shed = storage.Shed.init(allocator),
.arena = std.heap.ArenaAllocator.init(allocator),
.cookie_jar = storage.CookieJar.init(allocator),
.window = Window.create(null, .{ .agent = user_agent }),
.state = .{
.loop = app.loop,
.document = null,
.http_client = browser.http_client,
// we'll set this immediately after
.cookie_jar = undefined,
// nothing should be used on the state until we have a page
// at which point we'll set these fields
.renderer = undefined,
.url = undefined,
.arena = undefined,
},
};
self.state.cookie_jar = &self.cookie_jar;
errdefer self.arena.deinit();
self.executor = try browser.env.startExecutor(Window, &self.state, self, .main);
errdefer browser.env.stopExecutor(self.executor);
self.inspector = try Env.Inspector.init(self.arena.allocator(), self.executor, ctx);
self.microtaskLoop();
}
fn deinit(self: *Session) void {
self.app.loop.resetZig();
if (self.page != null) {
self.removePage();
}
self.inspector.deinit();
self.arena.deinit();
self.cookie_jar.deinit();
self.storage_shed.deinit();
self.browser.env.stopExecutor(self.executor);
}
fn microtaskLoop(self: *Session) void {
self.browser.runMicrotasks();
self.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Session, self, microtaskLoop);
}
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
const self: *Session = @ptrCast(@alignCast(ctx));
const page = &(self.page orelse return error.NoPage);
log.debug("fetch module: specifier: {s}", .{specifier});
// fetchModule is called within the context of processing a page.
// Use the page_arena for this, which has a more appropriate lifetime
// and which has more retained memory between sessions and pages.
const arena = self.browser.page_arena.allocator();
return try page.fetchData(
arena,
specifier,
if (page.current_script) |s| s.src else null,
);
}
pub fn callInspector(self: *const Session, msg: []const u8) void {
self.inspector.send(msg);
self.executor.deinit();
}
// NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session, aux_data: ?[]const u8) !*Page {
pub fn createPage(self: *Session) !*Page {
std.debug.assert(self.page == null);
_ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
self.page = Page.init(self);
self.page = undefined;
const page = &self.page.?;
try Page.init(page, page_arena.allocator(), self);
// start JS env
log.debug("start new js scope", .{});
self.state.arena = self.browser.page_arena.allocator();
errdefer self.state.arena = undefined;
try self.executor.startScope(&self.window);
// load polyfills
try polyfill.load(self.arena.allocator(), self.executor);
if (aux_data) |ad| {
self.aux_data = try self.arena.allocator().dupe(u8, ad);
}
// inspector
self.contextCreated(page);
return page;
}
@@ -269,20 +182,13 @@ pub const Session = struct {
pub fn removePage(self: *Session) void {
std.debug.assert(self.page != null);
// Reset all existing callbacks.
self.app.loop.resetJS();
self.browser.app.loop.resetJS();
self.browser.app.loop.resetZig();
self.executor.endScope();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
self.window.replaceLocation(.{ .url = null }) catch |e| {
log.err("reset window location: {any}", .{e});
};
self.page = null;
// clear netsurf memory arena.
parser.deinit();
self.state.arena = undefined;
self.page = null;
}
pub fn currentPage(self: *Session) ?*Page {
@@ -299,20 +205,15 @@ pub const Session = struct {
// look like a leak if we navigate from page to page a lot.
var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const url = try self.page.?.url.?.resolve(fba.allocator(), url_string);
const url = try self.page.?.url.resolve(fba.allocator(), url_string);
self.removePage();
var page = try self.createPage(null);
var page = try self.createPage();
return page.navigate(url, .{
.reason = .anchor,
});
}
fn contextCreated(self: *Session, page: *Page) void {
log.debug("inspector context created", .{});
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", self.aux_data, true);
}
fn notify(self: *const Session, notification: *const Notification) void {
self.notify_func(self.notify_ctx, notification) catch |err| {
log.err("notify {}: {}", .{ std.meta.activeTag(notification.*), err });
@@ -326,28 +227,64 @@ pub const Session = struct {
// The page handle all its memory in an arena allocator. The arena is reseted
// when end() is called.
pub const Page = struct {
arena: Allocator,
session: *Session,
doc: ?*parser.Document = null,
// an arena with a lifetime for the entire duration of the page
arena: Allocator,
// Gets injected into any WebAPI method that needs it
state: SessionState,
// Serves are the root object of our JavaScript environment
window: Window,
doc: ?*parser.Document,
// The URL of the page
url: ?URL = null,
url: URL,
raw_data: ?[]const u8 = null,
raw_data: ?[]const u8,
renderer: FlatRenderer,
scope: *Env.Scope,
// current_script is the script currently evaluated by the page.
// current_script could by fetch module to resolve module's url to fetch.
current_script: ?*const Script = null,
renderer: FlatRenderer,
fn init(session: *Session) Page {
const arena = session.browser.page_arena.allocator();
return .{
fn init(self: *Page, arena: Allocator, session: *Session) !void {
const browser = session.browser;
self.* = .{
.window = .{},
.arena = arena,
.doc = null,
.raw_data = null,
.url = URL.empty,
.session = session,
.renderer = FlatRenderer.init(arena),
.state = .{
.arena = arena,
.document = null,
.url = &self.url,
.renderer = &self.renderer,
.loop = browser.app.loop,
.cookie_jar = &session.cookie_jar,
.http_client = browser.http_client,
},
.scope = try session.executor.startScope(&self.window, &self.state, self),
};
// load polyfills
try polyfill.load(self.arena, self.scope);
self.microtaskLoop();
}
fn microtaskLoop(self: *Page) void {
const browser = self.session.browser;
browser.runMicrotasks();
browser.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Page, self, microtaskLoop);
}
// dump writes the page content into the given file.
@@ -363,13 +300,23 @@ pub const Page = struct {
try Dump.writeHTML(self.doc.?, out);
}
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
const self: *Page = @ptrCast(@alignCast(ctx));
log.debug("fetch module: specifier: {s}", .{specifier});
return try self.fetchData(
specifier,
if (self.current_script) |s| s.src else null,
);
}
pub fn wait(self: *Page) !void {
// try catch
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.session.executor);
try_catch.init(self.scope);
defer try_catch.deinit();
self.session.app.loop.run() catch |err| {
self.session.browser.app.loop.run() catch |err| {
if (try try_catch.err(self.arena)) |msg| {
log.info("wait error: {s}", .{msg});
return;
@@ -380,16 +327,13 @@ pub const Page = struct {
log.debug("wait: OK", .{});
}
fn origin(self: *const Page) !?[]const u8 {
const url = &(self.url orelse return null);
fn origin(self: *const Page) ![]const u8 {
var arr: std.ArrayListUnmanaged(u8) = .{};
try url.origin(arr.writer(self.arena));
try self.url.origin(arr.writer(self.arena));
return arr.items;
}
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
// - aux_data: extra data forwarded to the Inspector
// see Inspector.contextCreated
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
const arena = self.arena;
const session = self.session;
@@ -405,36 +349,38 @@ pub const Page = struct {
// later in this function, with the final request url (since we might
// redirect)
self.url = request_url;
var url = &self.url.?;
session.app.telemetry.record(.{ .navigate = .{
session.browser.app.telemetry.record(.{ .navigate = .{
.proxy = false,
.tls = std.ascii.eqlIgnoreCase(url.scheme(), "https"),
.tls = std.ascii.eqlIgnoreCase(request_url.scheme(), "https"),
} });
// load the data
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
var request = try self.newHTTPRequest(.GET, &self.url, .{ .navigation = true });
defer request.deinit();
session.notify(&.{ .page_navigate = .{
.url = url,
.url = &self.url,
.reason = opts.reason,
.timestamp = timestamp(),
} });
session.notify(&.{ .context_created = .{
.origin = try self.origin(),
} });
var response = try request.sendSync(.{});
// would be different than self.url in the case of a redirect
self.url = try URL.fromURI(arena, request.uri);
url = &self.url.?;
const header = response.header;
try session.cookie_jar.populateFromResponse(&url.uri, &header);
try session.cookie_jar.populateFromResponse(&self.url.uri, &header);
// TODO handle fragment in url.
try session.window.replaceLocation(.{ .url = try url.toWebApi(arena) });
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(arena) });
log.info("GET {any} {d}", .{ url, header.status });
log.info("GET {any} {d}", .{ self.url, header.status });
const content_type = header.get("content-type");
@@ -458,7 +404,7 @@ pub const Page = struct {
}
session.notify(&.{ .page_navigated = .{
.url = url,
.url = &self.url,
.timestamp = timestamp(),
} });
}
@@ -494,27 +440,18 @@ pub const Page = struct {
// https://html.spec.whatwg.org/#reporting-document-loading-status
// inject the URL to the document including the fragment.
try parser.documentSetDocumentURI(doc, self.url.?.raw);
try parser.documentSetDocumentURI(doc, self.url.raw);
const session = self.session;
// TODO set the referrer to the document.
try session.window.replaceDocument(html_doc);
session.window.setStorageShelf(
try session.storage_shed.getOrPut((try self.origin()) orelse "null"),
try self.window.replaceDocument(html_doc);
self.window.setStorageShelf(
try self.session.storage_shed.getOrPut(try self.origin()),
);
// https://html.spec.whatwg.org/#read-html
// inspector
session.contextCreated(self);
{
// update the sessions state
const state = &session.state;
state.url = &self.url.?;
state.document = html_doc;
state.renderer = &self.renderer;
}
// update the sessions state
self.state.document = html_doc;
// browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing.
@@ -612,7 +549,7 @@ pub const Page = struct {
try parser.eventInit(loadevt, "load", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &self.session.window),
parser.toEventTarget(Window, &self.window),
loadevt,
);
}
@@ -651,7 +588,7 @@ pub const Page = struct {
// TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
if (opt_text) |text| {
try s.eval(self.arena, self.session, text);
try s.eval(self, text);
return;
}
@@ -670,9 +607,10 @@ pub const Page = struct {
// It resolves src using the page's uri.
// If a base path is given, src is resolved according to the base first.
// the caller owns the returned string
fn fetchData(self: *const Page, arena: Allocator, src: []const u8, base: ?[]const u8) ![]const u8 {
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) ![]const u8 {
log.debug("starting fetch {s}", .{src});
const arena = self.arena;
var res_src = src;
// if a base path is given, we resolve src using base.
@@ -682,7 +620,7 @@ pub const Page = struct {
res_src = try std.fs.path.resolve(arena, &.{ _dir, src });
}
}
var origin_url = &self.url.?;
var origin_url = &self.url;
const url = try origin_url.resolve(arena, res_src);
var request = try self.newHTTPRequest(.GET, &url, .{
@@ -716,19 +654,17 @@ pub const Page = struct {
return arr.items;
}
fn fetchScript(self: *const Page, s: *const Script) !void {
const arena = self.arena;
const body = try self.fetchData(arena, s.src, null);
try s.eval(arena, self.session, body);
fn fetchScript(self: *Page, s: *const Script) !void {
const body = try self.fetchData(s.src, null);
try s.eval(self, body);
}
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
const session = self.session;
var request = try session.http_client.request(method, &url.uri);
var request = try self.state.http_client.request(method, &url.uri);
errdefer request.deinit();
var arr: std.ArrayListUnmanaged(u8) = .{};
try session.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts);
try self.state.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts);
if (arr.items.len > 0) {
try request.addHeader("Cookie", arr.items, .{});
@@ -832,24 +768,24 @@ pub const Page = struct {
return .unknown;
}
fn eval(self: Script, arena: Allocator, session: *Session, body: []const u8) !void {
fn eval(self: Script, page: *Page, body: []const u8) !void {
var try_catch: Env.TryCatch = undefined;
try_catch.init(session.executor);
try_catch.init(page.scope);
defer try_catch.deinit();
const res = switch (self.kind) {
.unknown => return error.UnknownScript,
.javascript => session.executor.exec(body, self.src),
.module => session.executor.module(body, self.src),
.javascript => page.scope.exec(body, self.src),
.module => page.scope.module(body, self.src),
} catch {
if (try try_catch.err(arena)) |msg| {
if (try try_catch.err(page.arena)) |msg| {
log.info("eval script {s}: {s}", .{ self.src, msg });
}
return FetchError.JsErr;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(arena);
const msg = try res.toString(page.arena);
log.debug("eval script {s}: {s}", .{ self.src, msg });
}
}

View File

@@ -114,7 +114,7 @@ pub const MutationObserver = struct {
}
}
pub fn jsScopeEnd(self: *MutationObserver, _: anytype) void {
pub fn jsCallScopeEnd(self: *MutationObserver, _: anytype) void {
const record = self.observed.items;
if (record.len == 0) {
return;

View File

@@ -25,6 +25,7 @@ pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject;
pub const Callback = Env.Callback;
pub const Env = js.Env(*SessionState, Interfaces{});
pub const Global = @import("html/window.zig").Window;
pub const SessionState = struct {
loop: *Loop,

View File

@@ -17,7 +17,7 @@ test "Browser.fetch" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try @import("polyfill.zig").load(testing.allocator, runner.executor);
try @import("polyfill.zig").load(testing.allocator, runner.scope);
try runner.testCases(&.{
.{

View File

@@ -31,13 +31,13 @@ const modules = [_]struct {
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
};
pub fn load(allocator: Allocator, executor: *Env.Executor) !void {
pub fn load(allocator: Allocator, scope: *Env.Scope) !void {
var try_catch: Env.TryCatch = undefined;
try_catch.init(executor);
try_catch.init(scope);
defer try_catch.deinit();
for (modules) |m| {
const res = executor.exec(m.source, m.name) catch |err| {
const res = scope.exec(m.source, m.name) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
log.err("load {s}: {s}", .{ m.name, msg });

View File

@@ -25,6 +25,7 @@ const Env = @import("../browser/env.zig").Env;
const asUint = @import("../str/parser.zig").asUint;
const Browser = @import("../browser/browser.zig").Browser;
const Session = @import("../browser/browser.zig").Session;
const Inspector = @import("../browser/env.zig").Env.Inspector;
const Incrementing = @import("../id.zig").Incrementing;
const Notification = @import("../notification.zig").Notification;
@@ -309,40 +310,51 @@ pub fn BrowserContext(comptime CDP_T: type) type {
node_registry: Node.Registry,
node_search_list: Node.Search.List,
isolated_world: ?IsolatedWorld(Env),
inspector: Inspector,
isolated_world: ?IsolatedWorld,
const Self = @This();
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
const allocator = cdp.allocator;
const session = try cdp.browser.newSession(self);
const arena = session.arena.allocator();
const inspector = try cdp.browser.env.newInspector(arena, self);
var registry = Node.Registry.init(allocator);
errdefer registry.deinit();
const session = try cdp.browser.newSession(self);
self.* = .{
.id = id,
.cdp = cdp,
.arena = arena,
.target_id = null,
.session_id = null,
.session = session,
.security_origin = URL_BASE,
.secure_context_type = "Secure", // TODO = enum
.loader_id = LOADER_ID,
.session = session,
.arena = session.arena.allocator(),
.page_life_cycle_events = false, // TODO; Target based value
.node_registry = registry,
.node_search_list = undefined,
.isolated_world = null,
.inspector = inspector,
};
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
}
pub fn deinit(self: *Self) void {
if (self.isolated_world) |isolated_world| {
isolated_world.executor.endScope();
self.cdp.browser.env.stopExecutor(isolated_world.executor);
self.isolated_world = null;
self.inspector.deinit();
// If the session has a page, we need to clear it first. The page
// context is always nested inside of the isolated world context,
// so we need to shutdown the page one first.
self.cdp.browser.closeSession();
if (self.isolated_world) |*world| {
world.deinit();
}
self.node_registry.deinit();
self.node_search_list.deinit();
@@ -353,25 +365,25 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.node_search_list.reset();
}
pub fn createIsolatedWorld(
self: *Self,
world_name: []const u8,
grant_universal_access: bool,
) !void {
if (self.isolated_world != null) return error.CurrentlyOnly1IsolatedWorldSupported;
pub fn createIsolatedWorld(self: *Self) !void {
if (self.isolated_world != null) {
return error.CurrentlyOnly1IsolatedWorldSupported;
}
const executor = try self.cdp.browser.env.startExecutor(@import("../browser/html/window.zig").Window, &self.session.state, self.session, .isolated);
errdefer self.cdp.browser.env.stopExecutor(executor);
// TBD should we endScope on removePage and re-startScope on createPage?
// Window will be refactored into the executor so we leave it ugly here for now as a reminder.
try executor.startScope(@import("../browser/html/window.zig").Window{});
var executor = try self.cdp.browser.env.newExecutor();
errdefer executor.deinit();
self.isolated_world = .{
.name = try self.arena.dupe(u8, world_name),
.grant_universal_access = grant_universal_access,
.name = "",
.global = .{},
.scope = undefined,
.executor = executor,
.grant_universal_access = false,
};
var world = &self.isolated_world.?;
// TODO: can we do something better than passing `undefined` for the state?
world.scope = try world.executor.startScope(&world.global, undefined, {});
}
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
@@ -384,18 +396,35 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn getURL(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null;
return if (page.url) |*url| url.raw else null;
const raw_url = page.url.raw;
return if (raw_url.len == 0) null else raw_url;
}
pub fn notify(ctx: *anyopaque, notification: *const Notification) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
switch (notification.*) {
.context_created => |cc| {
const aux_data = try std.fmt.allocPrint(self.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{self.target_id.?});
self.inspector.contextCreated(
self.session.page.?.scope,
"",
cc.origin,
aux_data,
true,
);
},
.page_navigate => |*pn| return @import("domains/page.zig").pageNavigate(self, pn),
.page_navigated => |*pn| return @import("domains/page.zig").pageNavigated(self, pn),
}
}
pub fn callInspector(self: *const Self, msg: []const u8) void {
self.inspector.send(msg);
// force running micro tasks after send input to the inspector.
self.cdp.browser.runMicrotasks();
}
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"id":<id>,...
@@ -481,13 +510,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
/// An isolated world has it's own instance of globals like Window.
/// Generally the client needs to resolve a node into the isolated world to be able to work with it.
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
pub fn IsolatedWorld(comptime E: type) type {
return struct {
name: []const u8,
grant_universal_access: bool,
executor: *E.Executor,
};
}
const IsolatedWorld = struct {
name: []const u8,
scope: *Env.Scope,
executor: Env.Executor,
grant_universal_access: bool,
global: @import("../browser/html/window.zig").Window,
pub fn deinit(self: *IsolatedWorld) void {
self.executor.deinit();
}
};
// This is a generic because when we send a result we have two different
// behaviors. Normally, we're sending the result to the client. But in some cases

View File

@@ -127,24 +127,27 @@ fn resolveNode(cmd: anytype) !void {
objectGroup: ?[]const u8 = null,
executionContextId: ?u32 = null,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
var executor = bc.session.executor;
var scope = page.scope;
if (params.executionContextId) |context_id| {
if (executor.context.debugContextId() != context_id) {
if (scope.context.debugContextId() != context_id) {
const isolated_world = bc.isolated_world orelse return error.ContextNotFound;
executor = isolated_world.executor;
scope = isolated_world.scope;
if (executor.context.debugContextId() != context_id) return error.ContextNotFound;
if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
}
}
const input_node_id = if (params.nodeId) |node_id| node_id else params.backendNodeId orelse return error.InvalidParams;
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
// So we use the Node.Union when retrieve the value from the environment
const remote_object = try bc.session.inspector.getRemoteObject(
executor,
const remote_object = try bc.inspector.getRemoteObject(
scope,
params.objectGroup orelse "",
try dom_node.Node.toInterface(node._node),
);
@@ -163,28 +166,61 @@ fn resolveNode(cmd: anytype) !void {
fn describeNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
objectId: ?[]const u8 = null,
depth: u32 = 1,
pierce: bool = false,
backendNodeId: ?u32 = null,
objectGroup: ?[]const u8 = null,
executionContextId: ?u32 = null,
})) orelse return error.InvalidParams;
if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
if (params.nodeId == null or params.backendNodeId != null or params.executionContextId != null) {
return error.NotYetImplementedParams;
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.UnknownNode;
if (params.nodeId != null) {
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
}
if (params.objectId != null) {
// Retrieve the object from which ever context it is in.
const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?);
const node = try bc.node_registry.register(@ptrCast(parser_node));
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
}
return error.MissingParams;
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
// So we use the Node.Union when retrieve the value from the environment
const remote_object = try bc.inspector.getRemoteObject(
page.scope,
params.objectGroup orelse "",
try dom_node.Node.toInterface(node._node),
);
defer remote_object.deinit();
const arena = cmd.arena;
return cmd.sendResult(.{ .object = .{
.type = try remote_object.getType(arena),
.subtype = try remote_object.getSubtype(arena),
.className = try remote_object.getClassName(arena),
.description = try remote_object.getDescription(arena),
.objectId = try remote_object.getObjectId(arena),
} }, .{});
// const params = (try cmd.params(struct {
// nodeId: ?Node.Id = null,
// backendNodeId: ?Node.Id = null,
// objectId: ?[]const u8 = null,
// depth: u32 = 1,
// pierce: bool = false,
// })) orelse return error.InvalidParams;
// if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
// return error.NotYetImplementedParams;
// }
// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
// if (params.nodeId != null) {
// const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
// }
// if (params.objectId != null) {
// // Retrieve the object from which ever context it is in.
// const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?);
// const node = try bc.node_registry.register(@ptrCast(parser_node));
// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
// }
// return error.MissingParams;
}
const testing = @import("../testing.zig");

View File

@@ -112,15 +112,15 @@ fn createIsolatedWorld(cmd: anytype) !void {
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
const world = &bc.isolated_world.?;
world.name = try bc.arena.dupe(u8, params.worldName);
world.grant_universal_access = params.grantUniveralAccess;
// Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send the contextCreated event
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
bc.session.inspector.contextCreated(world.executor, world.name, "", aux_data, false);
bc.inspector.contextCreated(world.scope, world.name, "", aux_data, false);
return cmd.sendResult(.{ .executionContextId = world.executor.context.debugContextId() }, .{});
return cmd.sendResult(.{ .executionContextId = world.scope.context.debugContextId() }, .{});
}
fn navigate(cmd: anytype) !void {
@@ -222,8 +222,8 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{bc.target_id.?});
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
bc.session.inspector.contextCreated(
isolated_world.executor,
bc.inspector.contextCreated(
isolated_world.scope,
isolated_world.name,
"://",
aux_json,

View File

@@ -44,10 +44,7 @@ fn sendInspector(cmd: anytype, action: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
// the result to return is handled directly by the inspector.
bc.session.callInspector(cmd.input.json);
// force running micro tasks after send input to the inspector.
cmd.cdp.browser.runMicrotasks();
bc.callInspector(cmd.input.json);
}
fn logInspector(cmd: anytype, action: anytype) !void {

View File

@@ -122,14 +122,9 @@ fn createTarget(cmd: anytype) !void {
const target_id = cmd.cdp.target_id_gen.next();
// start the js env
const aux_data = try std.fmt.allocPrint(
cmd.arena,
// NOTE: we assume this is the default web page
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{target_id},
);
_ = try bc.session.createPage(aux_data);
try bc.createIsolatedWorld();
_ = try bc.session.createPage();
// change CDP state
bc.security_origin = "://";
@@ -219,6 +214,10 @@ fn closeTarget(cmd: anytype) !void {
}
bc.session.removePage();
if (bc.isolated_world) |*world| {
world.deinit();
bc.isolated_world = null;
}
bc.target_id = null;
}
@@ -520,10 +519,6 @@ test "cdp.target: createTarget" {
{
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null);
try testing.expectEqual(
\\{"isDefault":true,"type":"default","frameId":"TID-1"}
, bc.session.aux_data);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
}
@@ -545,7 +540,7 @@ test "cdp.target: closeTarget" {
}
// pretend we createdTarget first
_ = try bc.session.createPage(null);
_ = try bc.session.createPage();
bc.target_id = "TID-A";
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
@@ -576,7 +571,7 @@ test "cdp.target: attachToTarget" {
}
// pretend we createdTarget first
_ = try bc.session.createPage(null);
_ = try bc.session.createPage();
bc.target_id = "TID-B";
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
@@ -620,7 +615,7 @@ test "cdp.target: getTargetInfo" {
}
// pretend we createdTarget first
_ = try bc.session.createPage(null);
_ = try bc.session.createPage();
bc.target_id = "TID-A";
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));

View File

@@ -122,7 +122,7 @@ const TestContext = struct {
if (opts.html) |html| {
parser.deinit();
try parser.init();
const page = try bc.session.createPage(null);
const page = try bc.session.createPage();
page.doc = (try Document.init(html)).doc;
}
return bc;

View File

@@ -100,7 +100,7 @@ pub fn main() !void {
var session = try browser.newSession({});
// page
const page = try session.createPage(null);
const page = try session.createPage();
_ = page.navigate(url, .{}) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => {

View File

@@ -4,6 +4,7 @@ const browser = @import("browser/browser.zig");
pub const Notification = union(enum) {
page_navigate: PageNavigate,
page_navigated: PageNavigated,
context_created: ContextCreated,
pub const PageNavigate = struct {
timestamp: u32,
@@ -15,4 +16,8 @@ pub const Notification = union(enum) {
timestamp: u32,
url: *const URL,
};
pub const ContextCreated = struct {
origin: []const u8,
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,6 @@ const Primitives = struct {
return v;
}
pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 {
std.debug.print("x: {d}\n", .{v});
return v;
}
pub fn _checkOptionalReturn(_: *const Primitives) ?bool {

View File

@@ -30,29 +30,31 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
return struct {
env: *Env,
executor: *Env.Executor,
scope: *Env.Scope,
executor: Env.Executor,
const Self = @This();
pub fn init(state: State, global: Global) !*Self {
const runner = try allocator.create(Self);
errdefer allocator.destroy(runner);
const self = try allocator.create(Self);
errdefer allocator.destroy(self);
runner.env = try Env.init(allocator, .{});
errdefer runner.env.deinit();
self.env = try Env.init(allocator, .{});
errdefer self.env.deinit();
const G = if (Global == void) DefaultGlobal else Global;
self.executor = try self.env.newExecutor();
errdefer self.executor.deinit();
runner.executor = try runner.env.startExecutor(G, state, runner, .main);
errdefer runner.env.stopExecutor(runner.executor);
try runner.executor.startScope(if (Global == void) &default_global else global);
return runner;
self.scope = try self.executor.startScope(
if (Global == void) &default_global else global,
state,
{},
);
return self;
}
pub fn deinit(self: *Self) void {
self.executor.endScope();
self.env.stopExecutor(self.executor);
self.executor.deinit();
self.env.deinit();
allocator.destroy(self);
}
@@ -62,10 +64,10 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
for (cases, 0..) |case, i| {
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor);
try_catch.init(self.scope);
defer try_catch.deinit();
const value = self.executor.exec(case.@"0", null) catch |err| {
const value = self.scope.exec(case.@"0", null) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
if (isExpectedTypeError(case.@"1", msg)) {
@@ -84,12 +86,6 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
}
}
}
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
_ = ctx;
_ = specifier;
return error.DummyModuleLoader;
}
};
}

View File

@@ -381,7 +381,8 @@ pub const JsRunner = struct {
arena: Allocator,
renderer: Renderer,
http_client: HttpClient,
executor: *Env.Executor,
scope: *Env.Scope,
executor: Env.Executor,
storage_shelf: storage.Shelf,
cookie_jar: storage.CookieJar,
@@ -394,55 +395,55 @@ pub const JsRunner = struct {
errdefer aa.deinit();
const arena = aa.allocator();
const runner = try arena.create(JsRunner);
runner.arena = arena;
const self = try arena.create(JsRunner);
self.arena = arena;
runner.env = try Env.init(arena, .{});
errdefer runner.env.deinit();
self.env = try Env.init(arena, .{});
errdefer self.env.deinit();
runner.url = try URL.parse("https://lightpanda.io/opensource-browser/", null);
self.url = try URL.parse("https://lightpanda.io/opensource-browser/", null);
runner.renderer = Renderer.init(arena);
runner.cookie_jar = storage.CookieJar.init(arena);
runner.loop = try Loop.init(arena);
errdefer runner.loop.deinit();
self.renderer = Renderer.init(arena);
self.cookie_jar = storage.CookieJar.init(arena);
self.loop = try Loop.init(arena);
errdefer self.loop.deinit();
var html = std.io.fixedBufferStream(opts.html);
const document = try parser.documentHTMLParse(html.reader(), "UTF-8");
runner.state = .{
self.state = .{
.arena = arena,
.loop = &runner.loop,
.loop = &self.loop,
.document = document,
.url = &runner.url,
.renderer = &runner.renderer,
.cookie_jar = &runner.cookie_jar,
.http_client = &runner.http_client,
.url = &self.url,
.renderer = &self.renderer,
.cookie_jar = &self.cookie_jar,
.http_client = &self.http_client,
};
runner.window = .{};
try runner.window.replaceDocument(document);
try runner.window.replaceLocation(.{
.url = try runner.url.toWebApi(arena),
self.window = .{};
try self.window.replaceDocument(document);
try self.window.replaceLocation(.{
.url = try self.url.toWebApi(arena),
});
runner.storage_shelf = storage.Shelf.init(arena);
runner.window.setStorageShelf(&runner.storage_shelf);
self.storage_shelf = storage.Shelf.init(arena);
self.window.setStorageShelf(&self.storage_shelf);
runner.http_client = try HttpClient.init(arena, 1, .{
self.http_client = try HttpClient.init(arena, 1, .{
.tls_verify_host = false,
});
runner.executor = try runner.env.startExecutor(Window, &runner.state, runner, .main);
errdefer runner.env.stopExecutor(runner.executor);
self.executor = try self.env.newExecutor();
errdefer self.executor.deinit();
try runner.executor.startScope(&runner.window);
return runner;
self.scope = try self.executor.startScope(&self.window, &self.state, {});
return self;
}
pub fn deinit(self: *JsRunner) void {
self.loop.deinit();
self.executor.endScope();
self.executor.deinit();
self.env.deinit();
self.http_client.deinit();
self.storage_shelf.deinit();
@@ -459,10 +460,10 @@ pub const JsRunner = struct {
for (cases, 0..) |case, i| {
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor);
try_catch.init(self.scope);
defer try_catch.deinit();
const value = self.executor.exec(case.@"0", null) catch |err| {
const value = self.scope.exec(case.@"0", null) catch |err| {
if (try try_catch.err(self.arena)) |msg| {
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
}
@@ -485,10 +486,10 @@ pub const JsRunner = struct {
pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor);
try_catch.init(self.scope);
defer try_catch.deinit();
return self.executor.exec(src, name) catch |err| {
return self.scope.exec(src, name) catch |err| {
if (try try_catch.err(self.arena)) |msg| {
err_msg.* = msg;
std.debug.print("Error running script: {s}\n", .{msg});
@@ -496,12 +497,6 @@ pub const JsRunner = struct {
return err;
};
}
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
_ = ctx;
_ = specifier;
return error.DummyModuleLoader;
}
};
const RunnerOpts = struct {

View File

@@ -8,6 +8,8 @@ pub const URL = struct {
uri: Uri,
raw: []const u8,
pub const empty = URL{ .uri = .{ .scheme = "" }, .raw = "" };
// We assume str will last as long as the URL
// In some cases, this is safe to do, because we know the URL is short lived.
// In most cases though, we assume the caller will just dupe the string URL

View File

@@ -50,7 +50,7 @@ pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *F
.html = html,
});
defer runner.deinit();
try polyfill.load(arena, runner.executor);
try polyfill.load(arena, runner.scope);
// display console logs
defer {
@@ -106,7 +106,7 @@ pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *F
// wait for all async executions
{
var try_catch: Env.TryCatch = undefined;
try_catch.init(runner.executor);
try_catch.init(runner.scope);
defer try_catch.deinit();
runner.loop.run() catch |err| {
if (try try_catch.err(arena)) |msg| {