Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 116 additions & 184 deletions src/browser/browser.zig

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/browser/dom/mutation_observer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/browser/env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/browser/polyfill/fetch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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(&.{
.{
Expand Down
6 changes: 3 additions & 3 deletions src/browser/polyfill/polyfill.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
111 changes: 64 additions & 47 deletions src/cdp/cdp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -61,9 +62,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
session_id_gen: SessionIdGen = .{},
browser_context_id_gen: BrowserContextIdGen = .{},

browser_context: ?*BrowserContext(Self),

browser_context_pool: std.heap.MemoryPool(BrowserContext(Self)),
browser_context: ?BrowserContext(Self),

// Re-used arena for processing a message. We're assuming that we're getting
// 1 message at a time.
Expand All @@ -82,17 +81,15 @@ pub fn CDPT(comptime TypeProvider: type) type {
.allocator = allocator,
.browser_context = null,
.message_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
};
}

pub fn deinit(self: *Self) void {
if (self.browser_context) |bc| {
if (self.browser_context) |*bc| {
bc.deinit();
}
self.browser.deinit();
self.message_arena.deinit();
self.browser_context_pool.deinit();
}

pub fn handleMessage(self: *Self, msg: []const u8) bool {
Expand Down Expand Up @@ -126,7 +123,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
.cdp = self,
.arena = arena,
.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.
Expand Down Expand Up @@ -221,7 +218,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
}

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;
return std.mem.eql(u8, session_id, input_session_id);
}
Expand All @@ -230,24 +227,22 @@ pub fn CDPT(comptime TypeProvider: type) type {
if (self.browser_context != null) {
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();
errdefer self.browser_context_pool.destroy(browser_context);
self.browser_context = @as(BrowserContext(Self), undefined);
const browser_context = &self.browser_context.?;

try BrowserContext(Self).init(browser_context, browser_context_id, self);
self.browser_context = browser_context;
return browser_context_id;
try BrowserContext(Self).init(browser_context, id, self);
return id;
}

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) {
return false;
}
bc.deinit();
self.browser.closeSession();
self.browser_context_pool.destroy(bc);
self.browser_context = null;
return true;
}
Expand Down Expand Up @@ -309,40 +304,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();
Expand All @@ -353,25 +359,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;

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);
pub fn createIsolatedWorld(self: *Self) !void {
if (self.isolated_world != null) {
return error.CurrentlyOnly1IsolatedWorldSupported;
}

// 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, {});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, doesn't need to be fixed in this PR.
(When grantUniversalAccess is true)
I'm fairly certain we should put in the page's Window, such that they share the same DOM tree.
I expect we keep running into issues if we combine steps like creating a scope and a context.

}

pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
Expand All @@ -384,7 +390,8 @@ 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 {
Expand All @@ -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 {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"id":<id>,...
Expand Down Expand Up @@ -481,13 +494,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
Expand Down Expand Up @@ -530,7 +547,7 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type {

pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
_ = try self.cdp.createBrowserContext();
self.browser_context = self.cdp.browser_context.?;
self.browser_context = &(self.cdp.browser_context.?);
return self.browser_context.?;
}

Expand Down
82 changes: 59 additions & 23 deletions src/cdp/domains/dom.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
Expand All @@ -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");
Expand Down
Loading
Loading