Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions src/browser/browser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,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();
Expand Down
47 changes: 34 additions & 13 deletions src/cdp/cdp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
}
Expand All @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/cdp/domains/dom.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,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;
}
Expand Down
37 changes: 28 additions & 9 deletions src/cdp/domains/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions src/cdp/domains/target.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions src/notification.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
27 changes: 13 additions & 14 deletions src/runtime/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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();

self.env.isolate.lowMemoryNotification();
}
// 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(); // TODO we only need to call this for the main World Executor
}
self.call_arena.deinit();
self.scope_arena.deinit();
Expand Down
Loading