diff --git a/build.zig.zon b/build.zig.zon index d4f5d74d5..aa2dda2d2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd", }, .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/5790c80fcd12dec64e596f6f66f09de567020e8a.tar.gz", - .hash = "v8-0.0.0-xddH66roIAAdXNJpBKN_NO8zBz2H8b9moUzshBCfns2p", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/97bcfb61da8c97de1321d677a6727a927a9db9a4.tar.gz", + .hash = "v8-0.0.0-xddH69DoIADZ8YXZ_EIx_tKdQKEoGsgob_3_ZIi0O_nV", }, //.v8 = .{ .path = "../zig-v8-fork" }, //.tigerbeetle_io = .{ .path = "../tigerbeetle-io" }, diff --git a/src/browser/browser.zig b/src/browser/browser.zig index a357c7da7..c5f96b368 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -58,6 +58,7 @@ pub const Browser = struct { http_client: *http.Client, session_pool: SessionPool, page_arena: std.heap.ArenaAllocator, + pub const EnvType = Env; const SessionPool = std.heap.MemoryPool(Session); @@ -126,6 +127,7 @@ pub const Session = struct { // // 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, @@ -191,7 +193,7 @@ pub const Session = struct { self.state.cookie_jar = &self.cookie_jar; errdefer self.arena.deinit(); - self.executor = try browser.env.startExecutor(Window, &self.state, self); + 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); @@ -309,7 +311,7 @@ pub const Session = struct { fn contextCreated(self: *Session, page: *Page) void { log.debug("inspector context created", .{}); - self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", self.aux_data); + self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", self.aux_data, true); } fn notify(self: *const Session, notification: *const Notification) void { diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 146fc84e4..ed2973552 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -284,6 +284,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { // RELATION TO SESSION_ID session: *CDP_T.Session, + // Points to the session arena + arena: Allocator, + // Maps to our Page. (There are other types of targets, but we only // deal with "pages" for now). Since we only allow 1 open page at a // time, we only have 1 target_id. @@ -306,6 +309,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { node_registry: Node.Registry, node_search_list: Node.Search.List, + isolated_world: ?IsolatedWorld(CDP_T.Browser.EnvType), + const Self = @This(); fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void { @@ -314,6 +319,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { var registry = Node.Registry.init(allocator); errdefer registry.deinit(); + const session = try cdp.browser.newSession(self); self.* = .{ .id = id, .cdp = cdp, @@ -322,15 +328,22 @@ pub fn BrowserContext(comptime CDP_T: type) type { .security_origin = URL_BASE, .secure_context_type = "Secure", // TODO = enum .loader_id = LOADER_ID, - .session = try cdp.browser.newSession(self), + .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, }; 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.node_registry.deinit(); self.node_search_list.deinit(); } @@ -340,6 +353,27 @@ 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); + + // 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 = .{ + .name = try self.arena.dupe(u8, world_name), + .grant_universal_access = grant_universal_access, + .executor = executor, + }; + } + pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer { return .{ .node = node, @@ -437,6 +471,24 @@ pub fn BrowserContext(comptime CDP_T: type) type { }; } +/// see: https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world +/// The current understanding. An isolated world lives in the same isolate, but a separated context. +/// Clients create this to be able to create variables and run code without interfering with the +/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after +/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed +/// in the isolated world by using its Context ID or the worldName. +/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects. +/// 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, + }; +} + // 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 // we want to capture the result. So we want the command.sendResult to be diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index a51a40590..0f594fd34 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -127,17 +127,24 @@ fn resolveNode(cmd: anytype) !void { objectGroup: ?[]const u8 = null, executionContextId: ?u32 = null, })) orelse return error.InvalidParams; - 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 node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.UnknownNode; + + var executor = bc.session.executor; + if (params.executionContextId) |context_id| { + if (executor.context.debugContextId() != context_id) { + const isolated_world = bc.isolated_world orelse return error.ContextNotFound; + executor = isolated_world.executor; + + if (executor.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 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( - bc.session.executor, + executor, params.objectGroup orelse "", try dom_node.Node.toInterface(node._node), ); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 94c569abf..d32b9bee5 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -84,6 +84,8 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void { } // TODO: hard coded method +// With the command we receive a script we need to store and run for each new document. +// Note that the worldName refers to the name given to the isolated world. fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { // const params = (try cmd.params(struct { // source: []const u8, @@ -97,37 +99,28 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { }, .{}); } -// TODO: hard coded method fn createIsolatedWorld(cmd: anytype) !void { - _ = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const session_id = cmd.input.session_id orelse return error.SessionIdRequired; - const params = (try cmd.params(struct { frameId: []const u8, worldName: []const u8, grantUniveralAccess: bool, })) orelse return error.InvalidParams; + if (!params.grantUniveralAccess) { + std.debug.print("grantUniveralAccess == false is not yet implemented", .{}); + // When grantUniveralAccess == false and the client attempts to resolve + // or otherwise access a DOM or other JS Object from another context that should fail. + } + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - // noop executionContextCreated event - try cmd.sendEvent("Runtime.executionContextCreated", .{ - .context = runtime.ExecutionContextCreated{ - .id = 0, - .origin = "", - .name = params.worldName, - // TODO: hard coded ID - .uniqueId = "7102379147004877974.3265385113993241162", - .auxData = .{ - .isDefault = false, - .type = "isolated", - .frameId = params.frameId, - }, - }, - }, .{ .session_id = session_id }); + try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess); + const world = &bc.isolated_world.?; - return cmd.sendResult(.{ - .executionContextId = 0, - }, .{}); + // Create the auxdata json for the contextCreated event + // Calling contextCreated will assign a Id to the context and send the contextCreated event + const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId}); + bc.session.inspector.contextCreated(world.executor, world.name, "", aux_data, false); + + return cmd.sendResult(.{ .executionContextId = world.executor.context.debugContextId() }, .{}); } fn navigate(cmd: anytype) !void { @@ -220,9 +213,24 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void }, .{ .session_id = session_id }); } - // Send Runtime.executionContextsCleared event - // TODO: noop event, we have no env context at this point, is it necesarry? + // When we actually recreated the context we should have the inspector send this event, see: resetContextGroup + // Sending this event will tell the client that the context ids they had are invalid and the context shouls be dropped + // 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 }); + + if (bc.isolated_world) |*isolated_world| { + var buffer: [256]u8 = undefined; + const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{bc.target_id.?}); + + // Calling contextCreated will assign a new Id to the context and send the contextCreated event + bc.session.inspector.contextCreated( + isolated_world.executor, + isolated_world.name, + "://", + aux_json, + false, + ); + } } pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void { diff --git a/src/cdp/domains/runtime.zig b/src/cdp/domains/runtime.zig index 9ff22ddc6..8e8b1079f 100644 --- a/src/cdp/domains/runtime.zig +++ b/src/cdp/domains/runtime.zig @@ -50,20 +50,6 @@ fn sendInspector(cmd: anytype, action: anytype) !void { cmd.cdp.browser.runMicrotasks(); } -pub const ExecutionContextCreated = struct { - id: u64, - origin: []const u8, - name: []const u8, - uniqueId: []const u8, - auxData: ?AuxData = null, - - pub const AuxData = struct { - isDefault: bool = true, - type: []const u8 = "default", - frameId: []const u8, - }; -}; - fn logInspector(cmd: anytype, action: anytype) !void { const script = switch (action) { .evaluate => blk: { diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index b702b9237..eff633e46 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -39,10 +39,13 @@ pub const Document = @import("../testing.zig").Document; const Browser = struct { session: ?*Session = null, arena: std.heap.ArenaAllocator, + env: Env, + pub const EnvType = Env; pub fn init(app: *App) !Browser { return .{ .arena = std.heap.ArenaAllocator.init(app.allocator), + .env = Env{}, }; } @@ -56,13 +59,14 @@ const Browser = struct { return error.MockBrowserSessionAlreadyExists; } const arena = self.arena.allocator(); - const executor = arena.create(Executor) catch unreachable; + const executor = arena.create(Env.Executor) catch unreachable; self.session = try arena.create(Session); self.session.?.* = .{ .page = null, - .arena = arena, + .arena = self.arena, .executor = executor, .inspector = .{}, + .state = 0, }; return self.session.?; } @@ -77,9 +81,10 @@ const Browser = struct { const Session = struct { page: ?Page = null, - arena: Allocator, - executor: *Executor, + arena: std.heap.ArenaAllocator, + executor: *Env.Executor, inspector: Inspector, + state: i32, pub fn currentPage(self: *Session) ?*Page { return &(self.page orelse return null); @@ -92,7 +97,7 @@ const Session = struct { self.page = .{ .session = self, .url = URL.parse("https://lightpanda.io/", null) catch unreachable, - .aux_data = try self.arena.dupe(u8, aux_data orelse ""), + .aux_data = try self.arena.allocator().dupe(u8, aux_data orelse ""), }; return &self.page.?; } @@ -107,12 +112,43 @@ const Session = struct { } }; -const Executor = struct {}; +const Env = struct { + pub const Executor = MockExecutor; + pub fn startExecutor(self: *Env, comptime Global: type, state: anytype, module_loader: anytype, kind: anytype) !*Executor { + _ = self; + _ = Global; + _ = state; + _ = module_loader; + _ = kind; + return error.MockExecutor; + } + pub fn stopExecutor(self: *Env, executor: *Executor) void { + _ = self; + _ = executor; + } +}; +const MockExecutor = struct { + context: Context, + + pub fn startScope(self: *MockExecutor, global: anytype) !void { + _ = self; + _ = global; + } + pub fn endScope(self: *MockExecutor) void { + _ = self; + } +}; +const Context = struct { + pub fn debugContextId(self: Context) i32 { + _ = self; + return 0; + } +}; const Inspector = struct { pub fn getRemoteObject( self: *const Inspector, - executor: *Executor, + executor: *Env.Executor, group: []const u8, value: anytype, ) !RemoteObject { @@ -127,6 +163,19 @@ const Inspector = struct { _ = object_id; return try alloc.create(i32); } + pub fn contextCreated(self: *const Inspector, + executor: *const Env.Executor, + name: []const u8, + origin: []const u8, + aux_data: ?[]const u8, + is_default_context: bool,) void { + _ = self; + _ = executor; + _ = name; + _ = origin; + _ = aux_data; + _ = is_default_context; + } }; const RemoteObject = struct { diff --git a/src/runtime/js.zig b/src/runtime/js.zig index b98690678..fab5f2463 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -265,10 +265,12 @@ pub fn Env(comptime S: type, comptime types: anytype) type { self.isolate.performMicrotasksCheckpoint(); } - pub fn startExecutor(self: *Self, comptime Global: type, state: State, module_loader: anytype) !*Executor { + pub fn startExecutor(self: *Self, comptime Global: type, state: State, module_loader: anytype, kind: WorldKind) !*Executor { if (comptime builtin.mode == .Debug) { - std.debug.assert(self.has_executor == false); - self.has_executor = true; + if (kind == .main) { + std.debug.assert(self.has_executor == false); + self.has_executor = true; + } } const isolate = self.isolate; const templates = &self.templates; @@ -307,8 +309,8 @@ pub fn Env(comptime S: type, comptime types: anytype) type { } const context = v8.Context.init(isolate, global_template, null); - context.enter(); - errdefer context.exit(); + if (kind == .main) context.enter(); + errdefer if (kind == .main) context.exit(); // This shouldn't be necessary, but it is: // https://groups.google.com/g/v8-users/c/qAQQBmbi--8 @@ -344,8 +346,9 @@ pub fn Env(comptime S: type, comptime types: anytype) type { executor.* = .{ .state = state, - .context = context, .isolate = isolate, + .kind = kind, + .context = context, .templates = templates, .handle_scope = handle_scope, .call_arena = undefined, @@ -364,7 +367,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { executor.call_arena = executor._call_arena_instance.allocator(); executor.scope_arena = executor._scope_arena_instance.allocator(); - errdefer self.stopExecutor(executor); + errdefer self.stopExecutor(executor); // Note: This likely has issues as context.exit() is errdefered as well // Custom exception // NOTE: there is no way in v8 to subclass the Error built-in type @@ -393,8 +396,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type { } if (comptime builtin.mode == .Debug) { - std.debug.assert(self.has_executor == true); - self.has_executor = false; + if (executor.kind == .main) { + std.debug.assert(self.has_executor == true); + self.has_executor = false; + } } } @@ -415,7 +420,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { // object (i.e. the Window), which gets attached not only to the Window // constructor/FunctionTemplate as normal, but also through the default // FunctionTemplate of the isolate (in startExecutor) - fn attachClass(self: *Self, comptime Struct: type, template: v8.FunctionTemplate) void { + fn attachClass(self: *const Self, comptime Struct: type, template: v8.FunctionTemplate) void { const template_proto = template.getPrototypeTemplate(); inline for (@typeInfo(Struct).@"struct".decls) |declaration| { const name = declaration.name; @@ -491,7 +496,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { return template; } - fn generateMethod(self: *Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void { + fn generateMethod(self: *const Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void { var js_name: v8.Name = undefined; if (comptime std.mem.eql(u8, name, "_symbol_iterator")) { js_name = v8.Symbol.getIterator(self.isolate).toName(); @@ -513,7 +518,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { template_proto.set(js_name, function_template, v8.PropertyAttribute.None); } - fn generateAttribute(self: *Self, comptime Struct: type, comptime name: []const u8, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void { + fn generateAttribute(self: *const Self, comptime Struct: type, comptime name: []const u8, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void { const zig_value = @field(Struct, name); const js_value = simpleZigValueToJs(self.isolate, zig_value, true); @@ -526,7 +531,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete); } - fn generateProperty(self: *Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void { + fn generateProperty(self: *const Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void { const getter = @field(Struct, "get_" ++ name); const param_count = @typeInfo(@TypeOf(getter)).@"fn".params.len; @@ -576,7 +581,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { template_proto.setGetterAndSetter(js_name, getter_callback, setter_callback); } - fn generateIndexer(_: *Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void { + fn generateIndexer(_: *const Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void { if (@hasDecl(Struct, "indexed_get") == false) { return; } @@ -605,7 +610,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { template_proto.setIndexedProperty(configuration, null); } - fn generateNamedIndexer(_: *Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void { + fn generateNamedIndexer(_: *const Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void { if (@hasDecl(Struct, "named_get") == false) { return; } @@ -765,10 +770,17 @@ pub fn Env(comptime S: type, comptime types: anytype) type { const PersistentObject = v8.Persistent(v8.Object); const PersistentFunction = v8.Persistent(v8.Function); + const WorldKind = enum { + main, + isolated, + worker, + }; + // This is capable of executing JavaScript. pub const Executor = struct { state: State, isolate: v8.Isolate, + kind: WorldKind, handle_scope: v8.HandleScope, @@ -818,11 +830,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type { // no init, must be initialized via env.startExecutor() // not public, must be destroyed via env.stopExecutor() + fn deinit(self: *Executor) void { - if (self.scope != null) { - self.endScope(); - } - self.context.exit(); + if (self.scope != null) self.endScope(); + if (self.kind == .main) self.context.exit(); self.handle_scope.deinit(); self._call_arena_instance.deinit(); @@ -1359,14 +1370,23 @@ pub fn Env(comptime S: type, comptime types: anytype) type { self.session.dispatchProtocolMessage(self.isolate, msg); } + // From CDP docs + // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription + // ---- + // - name: Human readable name describing given context. + // - origin: Execution context origin (ie. URL who initialised the request) + // - auxData: Embedder-specific auxiliary data likely matching + // {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} + // - is_default_context: Whether the execution context is default, should match the auxData pub fn contextCreated( self: *const Inspector, executor: *const Executor, name: []const u8, origin: []const u8, aux_data: ?[]const u8, + is_default_context: bool, ) void { - self.inner.contextCreated(executor.context, name, origin, aux_data); + self.inner.contextCreated(executor.context, name, origin, aux_data, is_default_context); } // Retrieves the RemoteObject for a given value. diff --git a/src/runtime/testing.zig b/src/runtime/testing.zig index 2a09b2e82..fcfab82b1 100644 --- a/src/runtime/testing.zig +++ b/src/runtime/testing.zig @@ -43,7 +43,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty const G = if (Global == void) DefaultGlobal else Global; - runner.executor = try runner.env.startExecutor(G, state, runner); + runner.executor = try runner.env.startExecutor(G, state, runner, .main); errdefer runner.env.stopExecutor(runner.executor); try runner.executor.startScope(if (Global == void) &default_global else global); diff --git a/src/testing.zig b/src/testing.zig index e37b5e40e..191dde5b6 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -433,7 +433,7 @@ pub const JsRunner = struct { .tls_verify_host = false, }); - runner.executor = try runner.env.startExecutor(Window, &runner.state, runner); + runner.executor = try runner.env.startExecutor(Window, &runner.state, runner, .main); errdefer runner.env.stopExecutor(runner.executor); try runner.executor.startScope(&runner.window);