diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 7cb5c043..d29fc691 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -164,11 +164,16 @@ pub const Session = struct { // start JS env log.debug("start new js scope", .{}); + // Inform CDP the main page has been created such that additional context for other Worlds can be created as well + self.browser.notification.dispatch(.page_created, page); return page; } pub fn removePage(self: *Session) void { + // Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one + self.browser.notification.dispatch(.page_remove, .{}); + std.debug.assert(self.page != null); // Reset all existing callbacks. self.browser.app.loop.resetJS(); diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index d0a551a4..5c18a6c5 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -340,6 +340,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); errdefer self.deinit(); + try cdp.browser.notification.register(.page_remove, self, onPageRemove); + try cdp.browser.notification.register(.page_created, self, onPageCreated); try cdp.browser.notification.register(.page_navigate, self, onPageNavigate); try cdp.browser.notification.register(.page_navigated, self, onPageNavigated); } @@ -365,7 +367,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { self.node_search_list.reset(); } - pub fn createIsolatedWorld(self: *Self, page: *Page) !void { + pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { if (self.isolated_world != null) { return error.CurrentlyOnly1IsolatedWorldSupported; } @@ -374,19 +376,12 @@ pub fn BrowserContext(comptime CDP_T: type) type { errdefer executor.deinit(); self.isolated_world = .{ - .name = "", - .scope = undefined, + .name = try self.arena.dupe(u8, world_name), + .scope = null, .executor = executor, - .grant_universal_access = true, + .grant_universal_access = grant_universal_access, }; - var world = &self.isolated_world.?; - - // 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!). - // 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. - world.scope = try world.executor.startScope(&page.window, &page.state, {}, false); + return &self.isolated_world.?; } pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer { @@ -403,6 +398,16 @@ pub fn BrowserContext(comptime CDP_T: type) type { return if (raw_url.len == 0) null else raw_url; } + pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void { + const self: *Self = @alignCast(@ptrCast(ctx)); + return @import("domains/page.zig").pageRemove(self); + } + + pub fn onPageCreated(ctx: *anyopaque, page: *Page) !void { + const self: *Self = @alignCast(@ptrCast(ctx)); + return @import("domains/page.zig").pageCreated(self, page); + } + pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void { const self: *Self = @alignCast(@ptrCast(ctx)); return @import("domains/page.zig").pageNavigate(self, data); @@ -506,12 +511,28 @@ pub fn BrowserContext(comptime CDP_T: type) type { /// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts. const IsolatedWorld = struct { name: []const u8, - scope: *Env.Scope, + scope: ?*Env.Scope, executor: Env.Executor, grant_universal_access: bool, pub fn deinit(self: *IsolatedWorld) void { self.executor.deinit(); + self.scope = null; + } + pub fn removeContext(self: *IsolatedWorld) !void { + if (self.scope == null) return error.NoIsolatedContextToRemove; + self.executor.endScope(); + self.scope = null; + } + + // 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!). + // 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. + pub fn createContext(self: *IsolatedWorld, page: *Page) !void { + if (self.scope != null) return error.Only1IsolatedContextSupported; + self.scope = try self.executor.startScope(&page.window, &page.state, {}, false); } }; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index c37f59cb..6613e373 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -197,7 +197,7 @@ fn resolveNode(cmd: anytype) !void { if (params.executionContextId) |context_id| { if (scope.context.debugContextId() != context_id) { const isolated_world = bc.isolated_world orelse return error.ContextNotFound; - scope = isolated_world.scope; + scope = isolated_world.scope orelse return error.ContextNotFound; if (scope.context.debugContextId() != context_id) return error.ContextNotFound; } diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 676db373..b25cc080 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -20,6 +20,7 @@ const std = @import("std"); const runtime = @import("runtime.zig"); const URL = @import("../../url.zig").URL; const Notification = @import("../../notification.zig").Notification; +const Page = @import("../../browser/browser.zig").Page; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -112,15 +113,17 @@ fn createIsolatedWorld(cmd: anytype) !void { } const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const world = &bc.isolated_world.?; - world.name = try bc.arena.dupe(u8, params.worldName); - world.grant_universal_access = params.grantUniveralAccess; + const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess); + const page = bc.session.currentPage() orelse return error.PageNotLoaded; + try pageCreated(bc, page); + const scope = world.scope.?; + // 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.inspector.contextCreated(world.scope, world.name, "", aux_data, false); + bc.inspector.contextCreated(scope, world.name, "", aux_data, false); - return cmd.sendResult(.{ .executionContextId = world.scope.context.debugContextId() }, .{}); + return cmd.sendResult(.{ .executionContextId = scope.context.debugContextId() }, .{}); } fn navigate(cmd: anytype) !void { @@ -144,7 +147,7 @@ fn navigate(cmd: anytype) !void { const url = try URL.parse(params.url, "https"); - var page = bc.session.currentPage().?; + var page = bc.session.currentPage() orelse return error.PageNotLoaded; bc.loader_id = bc.cdp.loader_id_gen.next(); try cmd.sendResult(.{ .frameId = target_id, @@ -220,7 +223,7 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void var buffer: [512]u8 = undefined; { var fba = std.heap.FixedBufferAllocator.init(&buffer); - const page = bc.session.currentPage().?; + const page = bc.session.currentPage() orelse return error.PageNotLoaded; const aux_data = try std.fmt.allocPrint(fba.allocator(), "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); bc.inspector.contextCreated( page.scope, @@ -230,12 +233,11 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void 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 bc.inspector.contextCreated( - isolated_world.scope, + isolated_world.scope.?, isolated_world.name, "://", aux_json, @@ -244,6 +246,23 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void } } +pub fn pageRemove(bc: anytype) !void { + // The main page is going to be removed, we need to remove contexts from other worlds first. + if (bc.isolated_world) |*isolated_world| { + try isolated_world.removeContext(); + } +} + +pub fn pageCreated(bc: anytype, page: *Page) !void { + if (bc.isolated_world) |*isolated_world| { + // We need to recreate the isolated world context + try isolated_world.createContext(page); + + const polyfill = @import("../../browser/polyfill/polyfill.zig"); + try polyfill.load(bc.arena, isolated_world.scope.?); + } +} + pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void { // I don't think it's possible that we get these notifications and don't // have these things setup. diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 9df99bf0..fca78351 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -125,8 +125,6 @@ fn createTarget(cmd: anytype) !void { bc.target_id = target_id; var page = try bc.session.createPage(); - try bc.createIsolatedWorld(page); - { const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); bc.inspector.contextCreated( diff --git a/src/notification.zig b/src/notification.zig index 5d655348..ab8c2511 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -55,18 +55,24 @@ pub const Notification = struct { node_pool: std.heap.MemoryPool(Node), const EventListeners = struct { + page_remove: List = .{}, + page_created: List = .{}, page_navigate: List = .{}, page_navigated: List = .{}, notification_created: List = .{}, }; const Events = union(enum) { + page_remove: PageRemove, + page_created: *browser.Page, page_navigate: *const PageNavigate, page_navigated: *const PageNavigated, notification_created: *Notification, }; const EventType = std.meta.FieldEnum(Events); + pub const PageRemove = struct {}; + pub const PageNavigate = struct { timestamp: u32, url: *const URL, diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 706284fe..8ce3889d 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -301,22 +301,21 @@ pub fn Env(comptime S: type, comptime types: anytype) type { // no init, must be initialized via env.newExecutor() pub fn deinit(self: *Executor) void { - if (self.scope) |scope| { - const isolate = scope.isolate; + if (self.scope != null) { self.endScope(); + } - // V8 doesn't immediately free memory associated with - // a Context, it's managed by the garbage collector. So, when the - // `gc_hints` option is enabled, we'll use the `lowMemoryNotification` - // call on the isolate to encourage v8 to free any contexts which - // have been freed. - if (self.env.gc_hints) { - var handle_scope: v8.HandleScope = undefined; - v8.HandleScope.init(&handle_scope, isolate); - defer handle_scope.deinit(); + // V8 doesn't immediately free memory associated with + // a Context, it's managed by the garbage collector. So, when the + // `gc_hints` option is enabled, we'll use the `lowMemoryNotification` + // call on the isolate to encourage v8 to free any contexts which + // have been freed. + if (self.env.gc_hints) { + var handle_scope: v8.HandleScope = undefined; + v8.HandleScope.init(&handle_scope, self.env.isolate); + defer handle_scope.deinit(); - self.env.isolate.lowMemoryNotification(); - } + self.env.isolate.lowMemoryNotification(); // TODO we only need to call this for the main World Executor } self.call_arena.deinit(); self.scope_arena.deinit();