Merge pull request #578 from lightpanda-io/scope_tightening

Reorganize v8 contexts and scope
This commit is contained in:
Pierre Tachoire
2025-04-29 18:46:31 +02:00
committed by GitHub
19 changed files with 1203 additions and 1254 deletions

View File

@@ -13,8 +13,8 @@
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd", .hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
}, },
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/5d46f159ca44535cfb4fccd9d46f719eb7eac5fc.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/363e2899e6d782ad999edbfae048228871230467.tar.gz",
.hash = "v8-0.0.0-xddH66zuIADu8FcQx2kkczC0yhqBY7LoA08-GRWF_zMA", .hash = "v8-0.0.0-xddH6wHzIAARDy1uFvPqqBpTXzhlnEGDTuX9IAUQz3oU",
}, },
//.v8 = .{ .path = "../zig-v8-fork" }, //.v8 = .{ .path = "../zig-v8-fork" },
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" }, //.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Dump = @import("dump.zig"); const Dump = @import("dump.zig");
const Mime = @import("mime.zig").Mime; const Mime = @import("mime.zig").Mime;
@@ -53,13 +54,10 @@ pub const user_agent = "Lightpanda/1.0";
pub const Browser = struct { pub const Browser = struct {
env: *Env, env: *Env,
app: *App, app: *App,
session: ?*Session, session: ?Session,
allocator: Allocator, allocator: Allocator,
http_client: *http.Client, http_client: *http.Client,
session_pool: SessionPool, page_arena: ArenaAllocator,
page_arena: std.heap.ArenaAllocator,
const SessionPool = std.heap.MemoryPool(Session);
pub fn init(app: *App) !Browser { pub fn init(app: *App) !Browser {
const allocator = app.allocator; const allocator = app.allocator;
@@ -75,31 +73,27 @@ pub const Browser = struct {
.session = null, .session = null,
.allocator = allocator, .allocator = allocator,
.http_client = &app.http_client, .http_client = &app.http_client,
.session_pool = SessionPool.init(allocator), .page_arena = ArenaAllocator.init(allocator),
.page_arena = std.heap.ArenaAllocator.init(allocator),
}; };
} }
pub fn deinit(self: *Browser) void { pub fn deinit(self: *Browser) void {
self.closeSession(); self.closeSession();
self.env.deinit(); self.env.deinit();
self.session_pool.deinit();
self.page_arena.deinit(); self.page_arena.deinit();
} }
pub fn newSession(self: *Browser, ctx: anytype) !*Session { pub fn newSession(self: *Browser, ctx: anytype) !*Session {
self.closeSession(); self.closeSession();
self.session = @as(Session, undefined);
const session = try self.session_pool.create(); const session = &self.session.?;
try Session.init(session, self, ctx); try Session.init(session, self, ctx);
self.session = session;
return session; return session;
} }
pub fn closeSession(self: *Browser) void { pub fn closeSession(self: *Browser) void {
if (self.session) |session| { if (self.session) |*session| {
session.deinit(); session.deinit();
self.session_pool.destroy(session);
self.session = null; self.session = null;
} }
} }
@@ -114,33 +108,16 @@ pub const Browser = struct {
// You can create successively multiple pages for a session, but you must // You can create successively multiple pages for a session, but you must
// deinit a page before running another one. // deinit a page before running another one.
pub const Session = struct { pub const Session = struct {
state: SessionState,
executor: *Env.Executor,
inspector: Env.Inspector,
app: *App,
browser: *Browser, browser: *Browser,
// The arena is used only to bound the js env init b/c it leaks memory. // Used to create our Inspector and in the BrowserContext.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/181 arena: ArenaAllocator,
//
// 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,
window: Window, executor: Env.Executor,
// TODO move the shed/jar to the browser?
storage_shed: storage.Shed, storage_shed: storage.Shed,
cookie_jar: storage.CookieJar, 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, page: ?Page = null,
http_client: *http.Client,
// recipient of notification, passed as the first parameter to notify // recipient of notification, passed as the first parameter to notify
notify_ctx: *anyopaque, notify_ctx: *anyopaque,
@@ -159,109 +136,45 @@ pub const Session = struct {
// need to play a little game. // need to play a little game.
const any_ctx: *anyopaque = if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx; const any_ctx: *anyopaque = if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx;
const app = browser.app; var executor = try browser.env.newExecutor();
const allocator = app.allocator; errdefer executor.deinit();
const allocator = browser.app.allocator;
self.* = .{ self.* = .{
.app = app,
.aux_data = null,
.browser = browser, .browser = browser,
.executor = executor,
.notify_ctx = any_ctx, .notify_ctx = any_ctx,
.inspector = undefined,
.notify_func = ContextStruct.notify, .notify_func = ContextStruct.notify,
.http_client = browser.http_client, .arena = ArenaAllocator.init(allocator),
.executor = undefined,
.storage_shed = storage.Shed.init(allocator), .storage_shed = storage.Shed.init(allocator),
.arena = std.heap.ArenaAllocator.init(allocator),
.cookie_jar = storage.CookieJar.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 { fn deinit(self: *Session) void {
self.app.loop.resetZig();
if (self.page != null) { if (self.page != null) {
self.removePage(); self.removePage();
} }
self.inspector.deinit();
self.arena.deinit(); self.arena.deinit();
self.cookie_jar.deinit(); self.cookie_jar.deinit();
self.storage_shed.deinit(); self.storage_shed.deinit();
self.browser.env.stopExecutor(self.executor); self.executor.deinit();
}
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);
} }
// NOTE: the caller is not the owner of the returned value, // NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience // 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); 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 = @as(Page, undefined);
const page = &self.page.?; const page = &self.page.?;
try Page.init(page, page_arena.allocator(), self);
// start JS env // start JS env
log.debug("start new js scope", .{}); 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; return page;
} }
@@ -269,20 +182,13 @@ pub const Session = struct {
pub fn removePage(self: *Session) void { pub fn removePage(self: *Session) void {
std.debug.assert(self.page != null); std.debug.assert(self.page != null);
// Reset all existing callbacks. // Reset all existing callbacks.
self.app.loop.resetJS(); self.browser.app.loop.resetJS();
self.browser.app.loop.resetZig();
self.executor.endScope(); self.executor.endScope();
self.page = null;
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
self.window.replaceLocation(.{ .url = null }) catch |e| {
log.err("reset window location: {any}", .{e});
};
// clear netsurf memory arena. // clear netsurf memory arena.
parser.deinit(); parser.deinit();
self.state.arena = undefined;
self.page = null;
} }
pub fn currentPage(self: *Session) ?*Page { 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. // look like a leak if we navigate from page to page a lot.
var buf: [1024]u8 = undefined; var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf); 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(); self.removePage();
var page = try self.createPage(null); var page = try self.createPage();
return page.navigate(url, .{ return page.navigate(url, .{
.reason = .anchor, .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 { fn notify(self: *const Session, notification: *const Notification) void {
self.notify_func(self.notify_ctx, notification) catch |err| { self.notify_func(self.notify_ctx, notification) catch |err| {
log.err("notify {}: {}", .{ std.meta.activeTag(notification.*), 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 // The page handle all its memory in an arena allocator. The arena is reseted
// when end() is called. // when end() is called.
pub const Page = struct { pub const Page = struct {
arena: Allocator,
session: *Session, 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 // 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 is the script currently evaluated by the page.
// current_script could by fetch module to resolve module's url to fetch. // current_script could by fetch module to resolve module's url to fetch.
current_script: ?*const Script = null, current_script: ?*const Script = null,
renderer: FlatRenderer, fn init(self: *Page, arena: Allocator, session: *Session) !void {
const browser = session.browser;
fn init(session: *Session) Page { self.* = .{
const arena = session.browser.page_arena.allocator(); .window = .{},
return .{
.arena = arena, .arena = arena,
.doc = null,
.raw_data = null,
.url = URL.empty,
.session = session, .session = session,
.renderer = FlatRenderer.init(arena), .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. // dump writes the page content into the given file.
@@ -363,13 +300,23 @@ pub const Page = struct {
try Dump.writeHTML(self.doc.?, out); 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 { pub fn wait(self: *Page) !void {
// try catch // try catch
var try_catch: Env.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(self.session.executor); try_catch.init(self.scope);
defer try_catch.deinit(); 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| { if (try try_catch.err(self.arena)) |msg| {
log.info("wait error: {s}", .{msg}); log.info("wait error: {s}", .{msg});
return; return;
@@ -380,16 +327,13 @@ pub const Page = struct {
log.debug("wait: OK", .{}); log.debug("wait: OK", .{});
} }
fn origin(self: *const Page) !?[]const u8 { pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
const url = &(self.url orelse return null);
var arr: std.ArrayListUnmanaged(u8) = .{}; var arr: std.ArrayListUnmanaged(u8) = .{};
try url.origin(arr.writer(self.arena)); try self.url.origin(arr.writer(arena));
return arr.items; return arr.items;
} }
// spec reference: https://html.spec.whatwg.org/#document-lifecycle // 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 { pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
const arena = self.arena; const arena = self.arena;
const session = self.session; const session = self.session;
@@ -405,19 +349,18 @@ pub const Page = struct {
// later in this function, with the final request url (since we might // later in this function, with the final request url (since we might
// redirect) // redirect)
self.url = request_url; self.url = request_url;
var url = &self.url.?;
session.app.telemetry.record(.{ .navigate = .{ session.browser.app.telemetry.record(.{ .navigate = .{
.proxy = false, .proxy = false,
.tls = std.ascii.eqlIgnoreCase(url.scheme(), "https"), .tls = std.ascii.eqlIgnoreCase(request_url.scheme(), "https"),
} }); } });
// load the data // 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(); defer request.deinit();
session.notify(&.{ .page_navigate = .{ session.notify(&.{ .page_navigate = .{
.url = url, .url = &self.url,
.reason = opts.reason, .reason = opts.reason,
.timestamp = timestamp(), .timestamp = timestamp(),
} }); } });
@@ -426,15 +369,14 @@ pub const Page = struct {
// would be different than self.url in the case of a redirect // would be different than self.url in the case of a redirect
self.url = try URL.fromURI(arena, request.uri); self.url = try URL.fromURI(arena, request.uri);
url = &self.url.?;
const header = response.header; 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. // 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"); const content_type = header.get("content-type");
@@ -458,7 +400,7 @@ pub const Page = struct {
} }
session.notify(&.{ .page_navigated = .{ session.notify(&.{ .page_navigated = .{
.url = url, .url = &self.url,
.timestamp = timestamp(), .timestamp = timestamp(),
} }); } });
} }
@@ -494,27 +436,18 @@ pub const Page = struct {
// https://html.spec.whatwg.org/#reporting-document-loading-status // https://html.spec.whatwg.org/#reporting-document-loading-status
// inject the URL to the document including the fragment. // 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. // TODO set the referrer to the document.
try session.window.replaceDocument(html_doc); try self.window.replaceDocument(html_doc);
session.window.setStorageShelf( self.window.setStorageShelf(
try session.storage_shed.getOrPut((try self.origin()) orelse "null"), try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
); );
// https://html.spec.whatwg.org/#read-html // https://html.spec.whatwg.org/#read-html
// inspector // update the sessions state
session.contextCreated(self); self.state.document = html_doc;
{
// update the sessions state
const state = &session.state;
state.url = &self.url.?;
state.document = html_doc;
state.renderer = &self.renderer;
}
// browse the DOM tree to retrieve scripts // browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing. // TODO execute the synchronous scripts during the HTL parsing.
@@ -612,7 +545,7 @@ pub const Page = struct {
try parser.eventInit(loadevt, "load", .{}); try parser.eventInit(loadevt, "load", .{});
_ = try parser.eventTargetDispatchEvent( _ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &self.session.window), parser.toEventTarget(Window, &self.window),
loadevt, loadevt,
); );
} }
@@ -651,7 +584,7 @@ pub const Page = struct {
// TODO handle charset attribute // TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element)); const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
if (opt_text) |text| { if (opt_text) |text| {
try s.eval(self.arena, self.session, text); try s.eval(self, text);
return; return;
} }
@@ -670,9 +603,10 @@ pub const Page = struct {
// It resolves src using the page's uri. // It resolves src using the page's uri.
// If a base path is given, src is resolved according to the base first. // If a base path is given, src is resolved according to the base first.
// the caller owns the returned string // 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}); log.debug("starting fetch {s}", .{src});
const arena = self.arena;
var res_src = src; var res_src = src;
// if a base path is given, we resolve src using base. // if a base path is given, we resolve src using base.
@@ -682,7 +616,7 @@ pub const Page = struct {
res_src = try std.fs.path.resolve(arena, &.{ _dir, src }); 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); const url = try origin_url.resolve(arena, res_src);
var request = try self.newHTTPRequest(.GET, &url, .{ var request = try self.newHTTPRequest(.GET, &url, .{
@@ -716,19 +650,17 @@ pub const Page = struct {
return arr.items; return arr.items;
} }
fn fetchScript(self: *const Page, s: *const Script) !void { fn fetchScript(self: *Page, s: *const Script) !void {
const arena = self.arena; const body = try self.fetchData(s.src, null);
const body = try self.fetchData(arena, s.src, null); try s.eval(self, body);
try s.eval(arena, self.session, body);
} }
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request { 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 self.state.http_client.request(method, &url.uri);
var request = try session.http_client.request(method, &url.uri);
errdefer request.deinit(); errdefer request.deinit();
var arr: std.ArrayListUnmanaged(u8) = .{}; 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) { if (arr.items.len > 0) {
try request.addHeader("Cookie", arr.items, .{}); try request.addHeader("Cookie", arr.items, .{});
@@ -832,24 +764,24 @@ pub const Page = struct {
return .unknown; 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; var try_catch: Env.TryCatch = undefined;
try_catch.init(session.executor); try_catch.init(page.scope);
defer try_catch.deinit(); defer try_catch.deinit();
const res = switch (self.kind) { const res = switch (self.kind) {
.unknown => return error.UnknownScript, .unknown => return error.UnknownScript,
.javascript => session.executor.exec(body, self.src), .javascript => page.scope.exec(body, self.src),
.module => session.executor.module(body, self.src), .module => page.scope.module(body, self.src),
} catch { } 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 }); log.info("eval script {s}: {s}", .{ self.src, msg });
} }
return FetchError.JsErr; return FetchError.JsErr;
}; };
if (builtin.mode == .Debug) { 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 }); 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; const record = self.observed.items;
if (record.len == 0) { if (record.len == 0) {
return; return;

View File

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

View File

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

View File

@@ -31,13 +31,13 @@ const modules = [_]struct {
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source }, .{ .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; var try_catch: Env.TryCatch = undefined;
try_catch.init(executor); try_catch.init(scope);
defer try_catch.deinit(); defer try_catch.deinit();
for (modules) |m| { 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| { if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg); defer allocator.free(msg);
log.err("load {s}: {s}", .{ m.name, 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 asUint = @import("../str/parser.zig").asUint;
const Browser = @import("../browser/browser.zig").Browser; const Browser = @import("../browser/browser.zig").Browser;
const Session = @import("../browser/browser.zig").Session; const Session = @import("../browser/browser.zig").Session;
const Inspector = @import("../browser/env.zig").Env.Inspector;
const Incrementing = @import("../id.zig").Incrementing; const Incrementing = @import("../id.zig").Incrementing;
const Notification = @import("../notification.zig").Notification; const Notification = @import("../notification.zig").Notification;
@@ -61,9 +62,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
session_id_gen: SessionIdGen = .{}, session_id_gen: SessionIdGen = .{},
browser_context_id_gen: BrowserContextIdGen = .{}, browser_context_id_gen: BrowserContextIdGen = .{},
browser_context: ?*BrowserContext(Self), browser_context: ?BrowserContext(Self),
browser_context_pool: std.heap.MemoryPool(BrowserContext(Self)),
// Re-used arena for processing a message. We're assuming that we're getting // Re-used arena for processing a message. We're assuming that we're getting
// 1 message at a time. // 1 message at a time.
@@ -82,17 +81,15 @@ pub fn CDPT(comptime TypeProvider: type) type {
.allocator = allocator, .allocator = allocator,
.browser_context = null, .browser_context = null,
.message_arena = std.heap.ArenaAllocator.init(allocator), .message_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
}; };
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
if (self.browser_context) |bc| { if (self.browser_context) |*bc| {
bc.deinit(); bc.deinit();
} }
self.browser.deinit(); self.browser.deinit();
self.message_arena.deinit(); self.message_arena.deinit();
self.browser_context_pool.deinit();
} }
pub fn handleMessage(self: *Self, msg: []const u8) bool { pub fn handleMessage(self: *Self, msg: []const u8) bool {
@@ -126,7 +123,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
.cdp = self, .cdp = self,
.arena = arena, .arena = arena,
.sender = sender, .sender = sender,
.browser_context = if (self.browser_context) |bc| bc else null, .browser_context = if (self.browser_context) |*bc| bc else null,
}; };
// See dispatchStartupCommand for more info on this. // See dispatchStartupCommand for more info on this.
@@ -221,7 +218,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
} }
fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool { fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool {
const browser_context = self.browser_context orelse return false; const browser_context = &(self.browser_context orelse return false);
const session_id = browser_context.session_id orelse return false; const session_id = browser_context.session_id orelse return false;
return std.mem.eql(u8, session_id, input_session_id); return std.mem.eql(u8, session_id, input_session_id);
} }
@@ -230,24 +227,22 @@ pub fn CDPT(comptime TypeProvider: type) type {
if (self.browser_context != null) { if (self.browser_context != null) {
return error.AlreadyExists; return error.AlreadyExists;
} }
const browser_context_id = self.browser_context_id_gen.next(); const id = self.browser_context_id_gen.next();
const browser_context = try self.browser_context_pool.create(); self.browser_context = @as(BrowserContext(Self), undefined);
errdefer self.browser_context_pool.destroy(browser_context); const browser_context = &self.browser_context.?;
try BrowserContext(Self).init(browser_context, browser_context_id, self); try BrowserContext(Self).init(browser_context, id, self);
self.browser_context = browser_context; return id;
return browser_context_id;
} }
pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool { pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool {
const bc = self.browser_context orelse return false; const bc = &(self.browser_context orelse return false);
if (std.mem.eql(u8, bc.id, browser_context_id) == false) { if (std.mem.eql(u8, bc.id, browser_context_id) == false) {
return false; return false;
} }
bc.deinit(); bc.deinit();
self.browser.closeSession(); self.browser.closeSession();
self.browser_context_pool.destroy(bc);
self.browser_context = null; self.browser_context = null;
return true; return true;
} }
@@ -309,40 +304,51 @@ pub fn BrowserContext(comptime CDP_T: type) type {
node_registry: Node.Registry, node_registry: Node.Registry,
node_search_list: Node.Search.List, node_search_list: Node.Search.List,
isolated_world: ?IsolatedWorld(Env), inspector: Inspector,
isolated_world: ?IsolatedWorld,
const Self = @This(); const Self = @This();
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void { fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
const allocator = cdp.allocator; 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); var registry = Node.Registry.init(allocator);
errdefer registry.deinit(); errdefer registry.deinit();
const session = try cdp.browser.newSession(self);
self.* = .{ self.* = .{
.id = id, .id = id,
.cdp = cdp, .cdp = cdp,
.arena = arena,
.target_id = null, .target_id = null,
.session_id = null, .session_id = null,
.session = session,
.security_origin = URL_BASE, .security_origin = URL_BASE,
.secure_context_type = "Secure", // TODO = enum .secure_context_type = "Secure", // TODO = enum
.loader_id = LOADER_ID, .loader_id = LOADER_ID,
.session = session,
.arena = session.arena.allocator(),
.page_life_cycle_events = false, // TODO; Target based value .page_life_cycle_events = false, // TODO; Target based value
.node_registry = registry, .node_registry = registry,
.node_search_list = undefined, .node_search_list = undefined,
.isolated_world = null, .isolated_world = null,
.inspector = inspector,
}; };
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
if (self.isolated_world) |isolated_world| { self.inspector.deinit();
isolated_world.executor.endScope();
self.cdp.browser.env.stopExecutor(isolated_world.executor); // If the session has a page, we need to clear it first. The page
self.isolated_world = null; // 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_registry.deinit();
self.node_search_list.deinit(); self.node_search_list.deinit();
@@ -353,25 +359,25 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.node_search_list.reset(); self.node_search_list.reset();
} }
pub fn createIsolatedWorld( pub fn createIsolatedWorld(self: *Self) !void {
self: *Self, if (self.isolated_world != null) {
world_name: []const u8, return error.CurrentlyOnly1IsolatedWorldSupported;
grant_universal_access: bool, }
) !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); var executor = try self.cdp.browser.env.newExecutor();
errdefer self.cdp.browser.env.stopExecutor(executor); errdefer executor.deinit();
// 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{});
self.isolated_world = .{ self.isolated_world = .{
.name = try self.arena.dupe(u8, world_name), .name = "",
.grant_universal_access = grant_universal_access, .global = .{},
.scope = undefined,
.executor = executor, .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 { pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
@@ -384,7 +390,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn getURL(self: *const Self) ?[]const u8 { pub fn getURL(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null; 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 { pub fn notify(ctx: *anyopaque, notification: *const Notification) !void {
@@ -396,6 +403,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
} }
} }
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 { pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) { if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"id":<id>,... // msg should be {"id":<id>,...
@@ -481,13 +494,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
/// An isolated world has it's own instance of globals like Window. /// 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. /// 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. /// 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 { const IsolatedWorld = struct {
return struct { name: []const u8,
name: []const u8, scope: *Env.Scope,
grant_universal_access: bool, executor: Env.Executor,
executor: *E.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 // 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 // behaviors. Normally, we're sending the result to the client. But in some cases
@@ -530,7 +547,7 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) { pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
_ = try self.cdp.createBrowserContext(); _ = try self.cdp.createBrowserContext();
self.browser_context = self.cdp.browser_context.?; self.browser_context = &(self.cdp.browser_context.?);
return self.browser_context.?; return self.browser_context.?;
} }

View File

@@ -127,24 +127,27 @@ fn resolveNode(cmd: anytype) !void {
objectGroup: ?[]const u8 = null, objectGroup: ?[]const u8 = null,
executionContextId: ?u32 = null, executionContextId: ?u32 = null,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; 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 (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; 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; 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 // 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 // So we use the Node.Union when retrieve the value from the environment
const remote_object = try bc.session.inspector.getRemoteObject( const remote_object = try bc.inspector.getRemoteObject(
executor, scope,
params.objectGroup orelse "", params.objectGroup orelse "",
try dom_node.Node.toInterface(node._node), try dom_node.Node.toInterface(node._node),
); );
@@ -180,7 +183,7 @@ fn describeNode(cmd: anytype) !void {
} }
if (params.objectId != null) { if (params.objectId != null) {
// Retrieve the object from which ever context it is in. // Retrieve the object from which ever context it is in.
const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?); const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?);
const node = try bc.node_registry.register(@ptrCast(parser_node)); const node = try bc.node_registry.register(@ptrCast(parser_node));
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{}); return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
} }

View File

@@ -112,15 +112,15 @@ fn createIsolatedWorld(cmd: anytype) !void {
} }
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
const world = &bc.isolated_world.?; 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 // Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send 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}); 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 { fn navigate(cmd: anytype) !void {
@@ -217,13 +217,25 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
// The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts. // The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts.
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id }); try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
if (bc.isolated_world) |*isolated_world| { var buffer: [512]u8 = undefined;
var buffer: [256]u8 = undefined; {
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{bc.target_id.?}); var fba = std.heap.FixedBufferAllocator.init(&buffer);
const page = bc.session.currentPage().?;
const aux_data = try std.fmt.allocPrint(fba.allocator(), "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated(
page.scope,
"",
try page.origin(fba.allocator()),
aux_data,
true,
);
}
if (bc.isolated_world) |*isolated_world| {
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
// Calling contextCreated will assign a new Id to the context and send the contextCreated event // Calling contextCreated will assign a new Id to the context and send the contextCreated event
bc.session.inspector.contextCreated( bc.inspector.contextCreated(
isolated_world.executor, isolated_world.scope,
isolated_world.name, isolated_world.name,
"://", "://",
aux_json, aux_json,

View File

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

View File

@@ -122,14 +122,28 @@ fn createTarget(cmd: anytype) !void {
const target_id = cmd.cdp.target_id_gen.next(); const target_id = cmd.cdp.target_id_gen.next();
// start the js env try bc.createIsolatedWorld();
const aux_data = try std.fmt.allocPrint( bc.target_id = target_id;
cmd.arena,
// NOTE: we assume this is the default web page const page = try bc.session.createPage();
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{target_id}, // The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
); // (assuming grantUniveralAccess will be set to True!).
_ = try bc.session.createPage(aux_data); // We just created the world and the page. The page's state lives in the session, but is update on navigation.
// This also means this pointer becomes invalid after removePage untill a new page is created.
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
bc.isolated_world.?.scope.state = &page.state;
{
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated(
page.scope,
"",
try page.origin(cmd.arena),
aux_data,
true,
);
}
// change CDP state // change CDP state
bc.security_origin = "://"; bc.security_origin = "://";
@@ -154,8 +168,6 @@ fn createTarget(cmd: anytype) !void {
try doAttachtoTarget(cmd, target_id); try doAttachtoTarget(cmd, target_id);
} }
bc.target_id = target_id;
try cmd.sendResult(.{ try cmd.sendResult(.{
.targetId = target_id, .targetId = target_id,
}, .{}); }, .{});
@@ -219,6 +231,10 @@ fn closeTarget(cmd: anytype) !void {
} }
bc.session.removePage(); bc.session.removePage();
if (bc.isolated_world) |*world| {
world.deinit();
bc.isolated_world = null;
}
bc.target_id = null; bc.target_id = null;
} }
@@ -520,10 +536,6 @@ test "cdp.target: createTarget" {
{ {
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } }); try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null); 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.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.? } }, .{}); 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 +557,7 @@ test "cdp.target: closeTarget" {
} }
// pretend we createdTarget first // pretend we createdTarget first
_ = try bc.session.createPage(null); _ = try bc.session.createPage();
bc.target_id = "TID-A"; bc.target_id = "TID-A";
{ {
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } })); try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
@@ -576,7 +588,7 @@ test "cdp.target: attachToTarget" {
} }
// pretend we createdTarget first // pretend we createdTarget first
_ = try bc.session.createPage(null); _ = try bc.session.createPage();
bc.target_id = "TID-B"; bc.target_id = "TID-B";
{ {
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } })); try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
@@ -620,7 +632,7 @@ test "cdp.target: getTargetInfo" {
} }
// pretend we createdTarget first // pretend we createdTarget first
_ = try bc.session.createPage(null); _ = try bc.session.createPage();
bc.target_id = "TID-A"; bc.target_id = "TID-A";
{ {
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } })); try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));

View File

@@ -105,7 +105,7 @@ const TestContext = struct {
} }
_ = try c.createBrowserContext(); _ = try c.createBrowserContext();
var bc = c.browser_context.?; var bc = &c.browser_context.?;
if (opts.id) |id| { if (opts.id) |id| {
bc.id = id; bc.id = id;
@@ -122,7 +122,7 @@ const TestContext = struct {
if (opts.html) |html| { if (opts.html) |html| {
parser.deinit(); parser.deinit();
try parser.init(); try parser.init();
const page = try bc.session.createPage(null); const page = try bc.session.createPage();
page.doc = (try Document.init(html)).doc; page.doc = (try Document.init(html)).doc;
} }
return bc; return bc;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,6 @@ const Primitives = struct {
return v; return v;
} }
pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 { pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 {
std.debug.print("x: {d}\n", .{v});
return v; return v;
} }
pub fn _checkOptionalReturn(_: *const Primitives) ?bool { 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 { return struct {
env: *Env, env: *Env,
executor: *Env.Executor, scope: *Env.Scope,
executor: Env.Executor,
const Self = @This(); const Self = @This();
pub fn init(state: State, global: Global) !*Self { pub fn init(state: State, global: Global) !*Self {
const runner = try allocator.create(Self); const self = try allocator.create(Self);
errdefer allocator.destroy(runner); errdefer allocator.destroy(self);
runner.env = try Env.init(allocator, .{}); self.env = try Env.init(allocator, .{});
errdefer runner.env.deinit(); 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); self.scope = try self.executor.startScope(
errdefer runner.env.stopExecutor(runner.executor); if (Global == void) &default_global else global,
state,
try runner.executor.startScope(if (Global == void) &default_global else global); {},
return runner; );
return self;
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.executor.endScope(); self.executor.deinit();
self.env.stopExecutor(self.executor);
self.env.deinit(); self.env.deinit();
allocator.destroy(self); 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 { pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
for (cases, 0..) |case, i| { for (cases, 0..) |case, i| {
var try_catch: Env.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor); try_catch.init(self.scope);
defer try_catch.deinit(); 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| { if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg); defer allocator.free(msg);
if (isExpectedTypeError(case.@"1", 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, arena: Allocator,
renderer: Renderer, renderer: Renderer,
http_client: HttpClient, http_client: HttpClient,
executor: *Env.Executor, scope: *Env.Scope,
executor: Env.Executor,
storage_shelf: storage.Shelf, storage_shelf: storage.Shelf,
cookie_jar: storage.CookieJar, cookie_jar: storage.CookieJar,
@@ -394,55 +395,55 @@ pub const JsRunner = struct {
errdefer aa.deinit(); errdefer aa.deinit();
const arena = aa.allocator(); const arena = aa.allocator();
const runner = try arena.create(JsRunner); const self = try arena.create(JsRunner);
runner.arena = arena; self.arena = arena;
runner.env = try Env.init(arena, .{}); self.env = try Env.init(arena, .{});
errdefer runner.env.deinit(); 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); self.renderer = Renderer.init(arena);
runner.cookie_jar = storage.CookieJar.init(arena); self.cookie_jar = storage.CookieJar.init(arena);
runner.loop = try Loop.init(arena); self.loop = try Loop.init(arena);
errdefer runner.loop.deinit(); errdefer self.loop.deinit();
var html = std.io.fixedBufferStream(opts.html); var html = std.io.fixedBufferStream(opts.html);
const document = try parser.documentHTMLParse(html.reader(), "UTF-8"); const document = try parser.documentHTMLParse(html.reader(), "UTF-8");
runner.state = .{ self.state = .{
.arena = arena, .arena = arena,
.loop = &runner.loop, .loop = &self.loop,
.document = document, .document = document,
.url = &runner.url, .url = &self.url,
.renderer = &runner.renderer, .renderer = &self.renderer,
.cookie_jar = &runner.cookie_jar, .cookie_jar = &self.cookie_jar,
.http_client = &runner.http_client, .http_client = &self.http_client,
}; };
runner.window = .{}; self.window = .{};
try runner.window.replaceDocument(document); try self.window.replaceDocument(document);
try runner.window.replaceLocation(.{ try self.window.replaceLocation(.{
.url = try runner.url.toWebApi(arena), .url = try self.url.toWebApi(arena),
}); });
runner.storage_shelf = storage.Shelf.init(arena); self.storage_shelf = storage.Shelf.init(arena);
runner.window.setStorageShelf(&runner.storage_shelf); 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, .tls_verify_host = false,
}); });
runner.executor = try runner.env.startExecutor(Window, &runner.state, runner, .main); self.executor = try self.env.newExecutor();
errdefer runner.env.stopExecutor(runner.executor); errdefer self.executor.deinit();
try runner.executor.startScope(&runner.window); self.scope = try self.executor.startScope(&self.window, &self.state, {});
return runner; return self;
} }
pub fn deinit(self: *JsRunner) void { pub fn deinit(self: *JsRunner) void {
self.loop.deinit(); self.loop.deinit();
self.executor.endScope(); self.executor.deinit();
self.env.deinit(); self.env.deinit();
self.http_client.deinit(); self.http_client.deinit();
self.storage_shelf.deinit(); self.storage_shelf.deinit();
@@ -459,10 +460,10 @@ pub const JsRunner = struct {
for (cases, 0..) |case, i| { for (cases, 0..) |case, i| {
var try_catch: Env.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor); try_catch.init(self.scope);
defer try_catch.deinit(); 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| { if (try try_catch.err(self.arena)) |msg| {
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" }); 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 { pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
var try_catch: Env.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor); try_catch.init(self.scope);
defer try_catch.deinit(); 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| { if (try try_catch.err(self.arena)) |msg| {
err_msg.* = msg; err_msg.* = msg;
std.debug.print("Error running script: {s}\n", .{msg}); std.debug.print("Error running script: {s}\n", .{msg});
@@ -496,12 +497,6 @@ pub const JsRunner = struct {
return err; return err;
}; };
} }
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
_ = ctx;
_ = specifier;
return error.DummyModuleLoader;
}
}; };
const RunnerOpts = struct { const RunnerOpts = struct {

View File

@@ -8,6 +8,8 @@ pub const URL = struct {
uri: Uri, uri: Uri,
raw: []const u8, raw: []const u8,
pub const empty = URL{ .uri = .{ .scheme = "" }, .raw = "" };
// We assume str will last as long as the URL // 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 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 // 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, .html = html,
}); });
defer runner.deinit(); defer runner.deinit();
try polyfill.load(arena, runner.executor); try polyfill.load(arena, runner.scope);
// display console logs // display console logs
defer { defer {
@@ -106,7 +106,7 @@ pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *F
// wait for all async executions // wait for all async executions
{ {
var try_catch: Env.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(runner.executor); try_catch.init(runner.scope);
defer try_catch.deinit(); defer try_catch.deinit();
runner.loop.run() catch |err| { runner.loop.run() catch |err| {
if (try try_catch.err(arena)) |msg| { if (try try_catch.err(arena)) |msg| {