Merge pull request #592 from lightpanda-io/isolated-polyfill-+-create-when-needed

Isolated polyfill & create world when needed
This commit is contained in:
Karl Seguin
2025-05-05 15:03:05 +08:00
committed by GitHub
7 changed files with 86 additions and 38 deletions

View File

@@ -164,11 +164,16 @@ pub const Session = struct {
// start JS env // start JS env
log.debug("start new js scope", .{}); 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; return page;
} }
pub fn removePage(self: *Session) void { 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); std.debug.assert(self.page != null);
// Reset all existing callbacks. // Reset all existing callbacks.
self.browser.app.loop.resetJS(); self.browser.app.loop.resetJS();

View File

@@ -340,6 +340,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
errdefer self.deinit(); 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_navigate, self, onPageNavigate);
try cdp.browser.notification.register(.page_navigated, self, onPageNavigated); 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(); 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) { if (self.isolated_world != null) {
return error.CurrentlyOnly1IsolatedWorldSupported; return error.CurrentlyOnly1IsolatedWorldSupported;
} }
@@ -374,19 +376,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
errdefer executor.deinit(); errdefer executor.deinit();
self.isolated_world = .{ self.isolated_world = .{
.name = "", .name = try self.arena.dupe(u8, world_name),
.scope = undefined, .scope = null,
.executor = executor, .executor = executor,
.grant_universal_access = true, .grant_universal_access = grant_universal_access,
}; };
var world = &self.isolated_world.?; return &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);
} }
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer { 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; 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 { pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void {
const self: *Self = @alignCast(@ptrCast(ctx)); const self: *Self = @alignCast(@ptrCast(ctx));
return @import("domains/page.zig").pageNavigate(self, data); 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. /// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
const IsolatedWorld = struct { const IsolatedWorld = struct {
name: []const u8, name: []const u8,
scope: *Env.Scope, scope: ?*Env.Scope,
executor: Env.Executor, executor: Env.Executor,
grant_universal_access: bool, grant_universal_access: bool,
pub fn deinit(self: *IsolatedWorld) void { pub fn deinit(self: *IsolatedWorld) void {
self.executor.deinit(); 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);
} }
}; };

View File

@@ -197,7 +197,7 @@ fn resolveNode(cmd: anytype) !void {
if (params.executionContextId) |context_id| { if (params.executionContextId) |context_id| {
if (scope.context.debugContextId() != context_id) { if (scope.context.debugContextId() != context_id) {
const isolated_world = bc.isolated_world orelse return error.ContextNotFound; 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; if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
} }

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const runtime = @import("runtime.zig"); const runtime = @import("runtime.zig");
const URL = @import("../../url.zig").URL; const URL = @import("../../url.zig").URL;
const Notification = @import("../../notification.zig").Notification; const Notification = @import("../../notification.zig").Notification;
const Page = @import("../../browser/browser.zig").Page;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { 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 bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const world = &bc.isolated_world.?; const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
world.name = try bc.arena.dupe(u8, params.worldName); const page = bc.session.currentPage() orelse return error.PageNotLoaded;
world.grant_universal_access = params.grantUniveralAccess; try pageCreated(bc, page);
const scope = world.scope.?;
// Create the auxdata json for the contextCreated event // Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send 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}); 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 { fn navigate(cmd: anytype) !void {
@@ -144,7 +147,7 @@ fn navigate(cmd: anytype) !void {
const url = try URL.parse(params.url, "https"); 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(); bc.loader_id = bc.cdp.loader_id_gen.next();
try cmd.sendResult(.{ try cmd.sendResult(.{
.frameId = target_id, .frameId = target_id,
@@ -220,7 +223,7 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
var buffer: [512]u8 = undefined; var buffer: [512]u8 = undefined;
{ {
var fba = std.heap.FixedBufferAllocator.init(&buffer); 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}); const aux_data = try std.fmt.allocPrint(fba.allocator(), "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated( bc.inspector.contextCreated(
page.scope, page.scope,
@@ -230,12 +233,11 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
true, true,
); );
} }
if (bc.isolated_world) |*isolated_world| { if (bc.isolated_world) |*isolated_world| {
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id}); 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 // Calling contextCreated will assign a new Id to the context and send the contextCreated event
bc.inspector.contextCreated( bc.inspector.contextCreated(
isolated_world.scope, isolated_world.scope.?,
isolated_world.name, isolated_world.name,
"://", "://",
aux_json, 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 { 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 // I don't think it's possible that we get these notifications and don't
// have these things setup. // have these things setup.

View File

@@ -125,8 +125,6 @@ fn createTarget(cmd: anytype) !void {
bc.target_id = target_id; bc.target_id = target_id;
var page = try bc.session.createPage(); 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}); const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated( bc.inspector.contextCreated(

View File

@@ -55,18 +55,24 @@ pub const Notification = struct {
node_pool: std.heap.MemoryPool(Node), node_pool: std.heap.MemoryPool(Node),
const EventListeners = struct { const EventListeners = struct {
page_remove: List = .{},
page_created: List = .{},
page_navigate: List = .{}, page_navigate: List = .{},
page_navigated: List = .{}, page_navigated: List = .{},
notification_created: List = .{}, notification_created: List = .{},
}; };
const Events = union(enum) { const Events = union(enum) {
page_remove: PageRemove,
page_created: *browser.Page,
page_navigate: *const PageNavigate, page_navigate: *const PageNavigate,
page_navigated: *const PageNavigated, page_navigated: *const PageNavigated,
notification_created: *Notification, notification_created: *Notification,
}; };
const EventType = std.meta.FieldEnum(Events); const EventType = std.meta.FieldEnum(Events);
pub const PageRemove = struct {};
pub const PageNavigate = struct { pub const PageNavigate = struct {
timestamp: u32, timestamp: u32,
url: *const URL, url: *const URL,

View File

@@ -301,22 +301,21 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
// no init, must be initialized via env.newExecutor() // no init, must be initialized via env.newExecutor()
pub fn deinit(self: *Executor) void { pub fn deinit(self: *Executor) void {
if (self.scope) |scope| { if (self.scope != null) {
const isolate = scope.isolate;
self.endScope(); self.endScope();
}
// V8 doesn't immediately free memory associated with // V8 doesn't immediately free memory associated with
// a Context, it's managed by the garbage collector. So, when the // a Context, it's managed by the garbage collector. So, when the
// `gc_hints` option is enabled, we'll use the `lowMemoryNotification` // `gc_hints` option is enabled, we'll use the `lowMemoryNotification`
// call on the isolate to encourage v8 to free any contexts which // call on the isolate to encourage v8 to free any contexts which
// have been freed. // have been freed.
if (self.env.gc_hints) { if (self.env.gc_hints) {
var handle_scope: v8.HandleScope = undefined; var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, isolate); v8.HandleScope.init(&handle_scope, self.env.isolate);
defer handle_scope.deinit(); 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.call_arena.deinit();
self.scope_arena.deinit(); self.scope_arena.deinit();