Skip to content

Commit 2d5ff82

Browse files
committed
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
1 parent 0fb0532 commit 2d5ff82

File tree

19 files changed

+1144
-1167
lines changed

19 files changed

+1144
-1167
lines changed

src/browser/browser.zig

Lines changed: 120 additions & 184 deletions
Large diffs are not rendered by default.

src/browser/dom/mutation_observer.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ pub const MutationObserver = struct {
114114
}
115115
}
116116

117-
pub fn jsScopeEnd(self: *MutationObserver, _: anytype) void {
117+
pub fn jsCallScopeEnd(self: *MutationObserver, _: anytype) void {
118118
const record = self.observed.items;
119119
if (record.len == 0) {
120120
return;

src/browser/env.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub const JsThis = Env.JsThis;
2525
pub const JsObject = Env.JsObject;
2626
pub const Callback = Env.Callback;
2727
pub const Env = js.Env(*SessionState, Interfaces{});
28+
pub const Global = @import("html/window.zig").Window;
2829

2930
pub const SessionState = struct {
3031
loop: *Loop,

src/browser/polyfill/fetch.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ test "Browser.fetch" {
1717
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
1818
defer runner.deinit();
1919

20-
try @import("polyfill.zig").load(testing.allocator, runner.executor);
20+
try @import("polyfill.zig").load(testing.allocator, runner.scope);
2121

2222
try runner.testCases(&.{
2323
.{

src/browser/polyfill/polyfill.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ const modules = [_]struct {
3131
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
3232
};
3333

34-
pub fn load(allocator: Allocator, executor: *Env.Executor) !void {
34+
pub fn load(allocator: Allocator, scope: *Env.Scope) !void {
3535
var try_catch: Env.TryCatch = undefined;
36-
try_catch.init(executor);
36+
try_catch.init(scope);
3737
defer try_catch.deinit();
3838

3939
for (modules) |m| {
40-
const res = executor.exec(m.source, m.name) catch |err| {
40+
const res = scope.exec(m.source, m.name) catch |err| {
4141
if (try try_catch.err(allocator)) |msg| {
4242
defer allocator.free(msg);
4343
log.err("load {s}: {s}", .{ m.name, msg });

src/cdp/cdp.zig

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const Env = @import("../browser/env.zig").Env;
2525
const asUint = @import("../str/parser.zig").asUint;
2626
const Browser = @import("../browser/browser.zig").Browser;
2727
const Session = @import("../browser/browser.zig").Session;
28+
const Inspector = @import("../browser/env.zig").Env.Inspector;
2829
const Incrementing = @import("../id.zig").Incrementing;
2930
const Notification = @import("../notification.zig").Notification;
3031

@@ -309,40 +310,51 @@ pub fn BrowserContext(comptime CDP_T: type) type {
309310
node_registry: Node.Registry,
310311
node_search_list: Node.Search.List,
311312

312-
isolated_world: ?IsolatedWorld(Env),
313+
inspector: Inspector,
314+
isolated_world: ?IsolatedWorld,
313315

314316
const Self = @This();
315317

316318
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
317319
const allocator = cdp.allocator;
318320

321+
const session = try cdp.browser.newSession(self);
322+
const arena = session.arena.allocator();
323+
324+
const inspector = try cdp.browser.env.newInspector(arena, self);
325+
319326
var registry = Node.Registry.init(allocator);
320327
errdefer registry.deinit();
321328

322-
const session = try cdp.browser.newSession(self);
323329
self.* = .{
324330
.id = id,
325331
.cdp = cdp,
332+
.arena = arena,
326333
.target_id = null,
327334
.session_id = null,
335+
.session = session,
328336
.security_origin = URL_BASE,
329337
.secure_context_type = "Secure", // TODO = enum
330338
.loader_id = LOADER_ID,
331-
.session = session,
332-
.arena = session.arena.allocator(),
333339
.page_life_cycle_events = false, // TODO; Target based value
334340
.node_registry = registry,
335341
.node_search_list = undefined,
336342
.isolated_world = null,
343+
.inspector = inspector,
337344
};
338345
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
339346
}
340347

341348
pub fn deinit(self: *Self) void {
342-
if (self.isolated_world) |isolated_world| {
343-
isolated_world.executor.endScope();
344-
self.cdp.browser.env.stopExecutor(isolated_world.executor);
345-
self.isolated_world = null;
349+
self.inspector.deinit();
350+
351+
// If the session has a page, we need to clear it first. The page
352+
// context is always nested inside of the isolated world context,
353+
// so we need to shutdown the page one first.
354+
self.cdp.browser.closeSession();
355+
356+
if (self.isolated_world) |*world| {
357+
world.deinit();
346358
}
347359
self.node_registry.deinit();
348360
self.node_search_list.deinit();
@@ -353,25 +365,25 @@ pub fn BrowserContext(comptime CDP_T: type) type {
353365
self.node_search_list.reset();
354366
}
355367

356-
pub fn createIsolatedWorld(
357-
self: *Self,
358-
world_name: []const u8,
359-
grant_universal_access: bool,
360-
) !void {
361-
if (self.isolated_world != null) return error.CurrentlyOnly1IsolatedWorldSupported;
362-
363-
const executor = try self.cdp.browser.env.startExecutor(@import("../browser/html/window.zig").Window, &self.session.state, self.session, .isolated);
364-
errdefer self.cdp.browser.env.stopExecutor(executor);
368+
pub fn createIsolatedWorld(self: *Self) !void {
369+
if (self.isolated_world != null) {
370+
return error.CurrentlyOnly1IsolatedWorldSupported;
371+
}
365372

366-
// TBD should we endScope on removePage and re-startScope on createPage?
367-
// Window will be refactored into the executor so we leave it ugly here for now as a reminder.
368-
try executor.startScope(@import("../browser/html/window.zig").Window{});
373+
var executor = try self.cdp.browser.env.newExecutor();
374+
errdefer executor.deinit();
369375

370376
self.isolated_world = .{
371-
.name = try self.arena.dupe(u8, world_name),
372-
.grant_universal_access = grant_universal_access,
377+
.name = "",
378+
.global = .{},
379+
.scope = undefined,
373380
.executor = executor,
381+
.grant_universal_access = false,
374382
};
383+
var world = &self.isolated_world.?;
384+
385+
// TODO: can we do something better than passing `undefined` for the state?
386+
world.scope = try world.executor.startScope(&world.global, undefined, {});
375387
}
376388

377389
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 {
384396

385397
pub fn getURL(self: *const Self) ?[]const u8 {
386398
const page = self.session.currentPage() orelse return null;
387-
return if (page.url) |*url| url.raw else null;
399+
const raw_url = page.url.raw;
400+
return if (raw_url.len == 0) null else raw_url;
388401
}
389402

390403
pub fn notify(ctx: *anyopaque, notification: *const Notification) !void {
391404
const self: *Self = @alignCast(@ptrCast(ctx));
392405

393406
switch (notification.*) {
407+
.context_created => |cc| {
408+
const aux_data = try std.fmt.allocPrint(self.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{self.target_id.?});
409+
self.inspector.contextCreated(
410+
self.session.page.?.scope,
411+
"",
412+
cc.origin,
413+
aux_data,
414+
true,
415+
);
416+
},
394417
.page_navigate => |*pn| return @import("domains/page.zig").pageNavigate(self, pn),
395418
.page_navigated => |*pn| return @import("domains/page.zig").pageNavigated(self, pn),
396419
}
397420
}
398421

422+
pub fn callInspector(self: *const Self, msg: []const u8) void {
423+
self.inspector.send(msg);
424+
// force running micro tasks after send input to the inspector.
425+
self.cdp.browser.runMicrotasks();
426+
}
427+
399428
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
400429
if (std.log.defaultLogEnabled(.debug)) {
401430
// msg should be {"id":<id>,...
@@ -481,13 +510,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
481510
/// An isolated world has it's own instance of globals like Window.
482511
/// Generally the client needs to resolve a node into the isolated world to be able to work with it.
483512
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
484-
pub fn IsolatedWorld(comptime E: type) type {
485-
return struct {
486-
name: []const u8,
487-
grant_universal_access: bool,
488-
executor: *E.Executor,
489-
};
490-
}
513+
const IsolatedWorld = struct {
514+
name: []const u8,
515+
scope: *Env.Scope,
516+
executor: Env.Executor,
517+
grant_universal_access: bool,
518+
global: @import("../browser/html/window.zig").Window,
519+
520+
pub fn deinit(self: *IsolatedWorld) void {
521+
self.executor.deinit();
522+
}
523+
};
491524

492525
// This is a generic because when we send a result we have two different
493526
// behaviors. Normally, we're sending the result to the client. But in some cases

src/cdp/domains/dom.zig

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -127,24 +127,27 @@ fn resolveNode(cmd: anytype) !void {
127127
objectGroup: ?[]const u8 = null,
128128
executionContextId: ?u32 = null,
129129
})) orelse return error.InvalidParams;
130+
130131
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
132+
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
131133

132-
var executor = bc.session.executor;
134+
var scope = page.scope;
133135
if (params.executionContextId) |context_id| {
134-
if (executor.context.debugContextId() != context_id) {
136+
if (scope.context.debugContextId() != context_id) {
135137
const isolated_world = bc.isolated_world orelse return error.ContextNotFound;
136-
executor = isolated_world.executor;
138+
scope = isolated_world.scope;
137139

138-
if (executor.context.debugContextId() != context_id) return error.ContextNotFound;
140+
if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
139141
}
140142
}
141-
const input_node_id = if (params.nodeId) |node_id| node_id else params.backendNodeId orelse return error.InvalidParams;
143+
144+
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
142145
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
143146

144147
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
145148
// So we use the Node.Union when retrieve the value from the environment
146-
const remote_object = try bc.session.inspector.getRemoteObject(
147-
executor,
149+
const remote_object = try bc.inspector.getRemoteObject(
150+
scope,
148151
params.objectGroup orelse "",
149152
try dom_node.Node.toInterface(node._node),
150153
);
@@ -163,28 +166,61 @@ fn resolveNode(cmd: anytype) !void {
163166
fn describeNode(cmd: anytype) !void {
164167
const params = (try cmd.params(struct {
165168
nodeId: ?Node.Id = null,
166-
backendNodeId: ?Node.Id = null,
167-
objectId: ?[]const u8 = null,
168-
depth: u32 = 1,
169-
pierce: bool = false,
169+
backendNodeId: ?u32 = null,
170+
objectGroup: ?[]const u8 = null,
171+
executionContextId: ?u32 = null,
170172
})) orelse return error.InvalidParams;
171-
if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
173+
174+
if (params.nodeId == null or params.backendNodeId != null or params.executionContextId != null) {
172175
return error.NotYetImplementedParams;
173176
}
174177

175178
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
179+
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
180+
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.UnknownNode;
176181

177-
if (params.nodeId != null) {
178-
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
179-
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
180-
}
181-
if (params.objectId != null) {
182-
// Retrieve the object from which ever context it is in.
183-
const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?);
184-
const node = try bc.node_registry.register(@ptrCast(parser_node));
185-
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
186-
}
187-
return error.MissingParams;
182+
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
183+
// So we use the Node.Union when retrieve the value from the environment
184+
const remote_object = try bc.inspector.getRemoteObject(
185+
page.scope,
186+
params.objectGroup orelse "",
187+
try dom_node.Node.toInterface(node._node),
188+
);
189+
defer remote_object.deinit();
190+
191+
const arena = cmd.arena;
192+
return cmd.sendResult(.{ .object = .{
193+
.type = try remote_object.getType(arena),
194+
.subtype = try remote_object.getSubtype(arena),
195+
.className = try remote_object.getClassName(arena),
196+
.description = try remote_object.getDescription(arena),
197+
.objectId = try remote_object.getObjectId(arena),
198+
} }, .{});
199+
200+
// const params = (try cmd.params(struct {
201+
// nodeId: ?Node.Id = null,
202+
// backendNodeId: ?Node.Id = null,
203+
// objectId: ?[]const u8 = null,
204+
// depth: u32 = 1,
205+
// pierce: bool = false,
206+
// })) orelse return error.InvalidParams;
207+
// if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
208+
// return error.NotYetImplementedParams;
209+
// }
210+
211+
// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
212+
213+
// if (params.nodeId != null) {
214+
// const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
215+
// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
216+
// }
217+
// if (params.objectId != null) {
218+
// // Retrieve the object from which ever context it is in.
219+
// const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?);
220+
// const node = try bc.node_registry.register(@ptrCast(parser_node));
221+
// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
222+
// }
223+
// return error.MissingParams;
188224
}
189225

190226
const testing = @import("../testing.zig");

src/cdp/domains/page.zig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,15 @@ fn createIsolatedWorld(cmd: anytype) !void {
112112
}
113113
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
114114

115-
try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
116115
const world = &bc.isolated_world.?;
117-
116+
world.name = try bc.arena.dupe(u8, params.worldName);
117+
world.grant_universal_access = params.grantUniveralAccess;
118118
// Create the auxdata json for the contextCreated event
119119
// Calling contextCreated will assign a Id to the context and send the contextCreated event
120120
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
121-
bc.session.inspector.contextCreated(world.executor, world.name, "", aux_data, false);
121+
bc.inspector.contextCreated(world.scope, world.name, "", aux_data, false);
122122

123-
return cmd.sendResult(.{ .executionContextId = world.executor.context.debugContextId() }, .{});
123+
return cmd.sendResult(.{ .executionContextId = world.scope.context.debugContextId() }, .{});
124124
}
125125

126126
fn navigate(cmd: anytype) !void {
@@ -222,8 +222,8 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
222222
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{bc.target_id.?});
223223

224224
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
225-
bc.session.inspector.contextCreated(
226-
isolated_world.executor,
225+
bc.inspector.contextCreated(
226+
isolated_world.scope,
227227
isolated_world.name,
228228
"://",
229229
aux_json,

src/cdp/domains/runtime.zig

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ fn sendInspector(cmd: anytype, action: anytype) !void {
4444
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
4545

4646
// the result to return is handled directly by the inspector.
47-
bc.session.callInspector(cmd.input.json);
48-
49-
// force running micro tasks after send input to the inspector.
50-
cmd.cdp.browser.runMicrotasks();
47+
bc.callInspector(cmd.input.json);
5148
}
5249

5350
fn logInspector(cmd: anytype, action: anytype) !void {

0 commit comments

Comments
 (0)