Rework Inspector usage

V8's inspector world is made up of 4 components: Inspector, Client, Channel and
Session. Currently, we treat all 4 components as a single unit which is tied to
the lifetime of CDP BrowserContext - or, loosely speaking, 1 "Inspector Unit"
per page / v8::Context.

According to https://web.archive.org/web/20210622022956/https://hyperandroid.com/2020/02/12/v8-inspector-from-an-embedder-standpoint/
and conversation with Gemini, it's more typical to have 1 inspector per isolate.
The general breakdown is the Inspector is the top-level manager, the Client is
our implementation which control how the Inspector works (its function we expose
that v8 calls into). These should be tied to the Isolate. Channels and Sessions
are more closely tied to Context, where the Channel is v8->zig and the Session
us zig->v8.

This PR does a few things
1 - It creates 1 Inspector and Client per Isolate (Env.js)
2 - It creates 1 Session/Channel per BrowserContext
3 - It merges v8::Session and v8::Channel into Inspector.Session
4 - It moves the Inspector instance directly into the Env
5 - BrowserContext interacts with the Inspector.Session, not the Inspector

4 is arguably unnecessary with respect to the main goal of this commit, but
the end-goal is to tighten the integration. Specifically, rather than CDP having
to inform the inspector that a context was created/destroyed, the Env which
manages Contexts directly (https://github.com/lightpanda-io/browser/pull/1432)
and which now has direct access to the Inspector, is now equipped to keep this
in sync.
This commit is contained in:
Karl Seguin
2026-01-30 15:35:18 +08:00
parent 34dda780d9
commit 181f265de5
11 changed files with 216 additions and 273 deletions

View File

@@ -50,10 +50,14 @@ session_arena: ArenaAllocator,
transfer_arena: ArenaAllocator,
notification: *Notification,
pub fn init(app: *App) !Browser {
const InitOpts = struct {
env: js.Env.InitOpts = .{},
};
pub fn init(app: *App, opts: InitOpts) !Browser {
const allocator = app.allocator;
var env = try js.Env.init(app);
var env = try js.Env.init(app, opts.env);
errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification);

View File

@@ -68,7 +68,14 @@ templates: []*const v8.FunctionTemplate,
// Global template created once per isolate and reused across all contexts
global_template: v8.Eternal,
pub fn init(app: *App) !Env {
// Inspector associated with the Isolate. Exists when CDP is being used.
inspector: ?*Inspector,
pub const InitOpts = struct {
with_inspector: bool = false,
};
pub fn init(app: *App, opts: InitOpts) !Env {
const allocator = app.allocator;
const snapshot = &app.snapshot;
@@ -84,17 +91,18 @@ pub fn init(app: *App) !Env {
var isolate = js.Isolate.init(params);
errdefer isolate.deinit();
const isolate_handle = isolate.handle;
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate.handle, Context.dynamicModuleCallback);
v8.v8__Isolate__SetPromiseRejectCallback(isolate.handle, promiseRejectCallback);
v8.v8__Isolate__SetMicrotasksPolicy(isolate.handle, v8.kExplicit);
v8.v8__Isolate__SetFatalErrorHandler(isolate.handle, fatalCallback);
v8.v8__Isolate__SetOOMErrorHandler(isolate.handle, oomCallback);
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate_handle, Context.dynamicModuleCallback);
v8.v8__Isolate__SetPromiseRejectCallback(isolate_handle, promiseRejectCallback);
v8.v8__Isolate__SetMicrotasksPolicy(isolate_handle, v8.kExplicit);
v8.v8__Isolate__SetFatalErrorHandler(isolate_handle, fatalCallback);
v8.v8__Isolate__SetOOMErrorHandler(isolate_handle, oomCallback);
isolate.enter();
errdefer isolate.exit();
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate.handle, Context.metaObjectCallback);
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate_handle, Context.metaObjectCallback);
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
@@ -111,19 +119,19 @@ pub fn init(app: *App) !Env {
inline for (JsApis, 0..) |JsApi, i| {
JsApi.Meta.class_id = i;
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate.handle, snapshot.data_start + i);
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
// Make function template eternal
v8.v8__Eternal__New(isolate.handle, @ptrCast(function_handle), &eternal_function_templates[i]);
v8.v8__Eternal__New(isolate_handle, @ptrCast(function_handle), &eternal_function_templates[i]);
// Extract the local handle from the global for easy access
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate.handle);
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate_handle);
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
}
// Create global template once per isolate
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate.handle);
const window_name = v8.v8__String__NewFromUtf8(isolate.handle, "Window", v8.kNormal, 6);
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle);
const window_name = v8.v8__String__NewFromUtf8(isolate_handle, "Window", v8.kNormal, 6);
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
// Find Window in JsApis by name (avoids circular import)
@@ -142,7 +150,12 @@ pub fn init(app: *App) !Env {
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
});
v8.v8__Eternal__New(isolate.handle, @ptrCast(global_template_local), &global_eternal);
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
}
var inspector: ?*js.Inspector = null;
if (opts.with_inspector) {
inspector = try Inspector.init(allocator, isolate_handle);
}
return .{
@@ -153,6 +166,7 @@ pub fn init(app: *App) !Env {
.platform = &app.platform,
.templates = templates,
.isolate_params = params,
.inspector = inspector,
.eternal_function_templates = eternal_function_templates,
.global_template = global_eternal,
};
@@ -167,6 +181,10 @@ pub fn deinit(self: *Env) void {
}
const allocator = self.app.allocator;
if (self.inspector) |i| {
i.deinit(allocator);
}
self.contexts.deinit(allocator);
allocator.free(self.templates);
@@ -220,8 +238,8 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
.arena = context_arena,
.handle = context_global,
.templates = self.templates,
.script_manager = &page._script_manager,
.call_arena = page.call_arena,
.script_manager = &page._script_manager,
};
try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
@@ -249,12 +267,6 @@ pub fn destroyContext(self: *Env, context: *Context) void {
self.isolate.notifyContextDisposed();
}
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector {
const inspector = try arena.create(Inspector);
try Inspector.init(inspector, self.isolate.handle, ctx);
return inspector;
}
pub fn runMicrotasks(self: *const Env) void {
self.isolate.performMicrotasksCheckpoint();
}

View File

@@ -23,99 +23,76 @@ const v8 = js.v8;
const TaggedOpaque = @import("TaggedOpaque.zig");
const Allocator = std.mem.Allocator;
const RndGen = std.Random.DefaultPrng;
const CONTEXT_GROUP_ID = 1;
const CLIENT_TRUST_LEVEL = 1;
const IS_DEBUG = @import("builtin").mode == .Debug;
// Inspector exists for the lifetime of the Isolate/Env. 1 Isolate = 1 Inspector.
// It combines the v8.Inspector and the v8.InspectorClientImpl. The v8.InspectorClientImpl
// is our own implementation that fulfills the InspectorClient API, i.e. it's the
// mechanism v8 provides to let us tweak how the inspector works. For example, it
// Below, you'll find a few pub export fn v8_inspector__Client__IMPL__XYZ functions
// which is our implementation of what the v8::Inspector requires of our Client
// (not much at all)
const Inspector = @This();
handle: *v8.Inspector,
unique_id: i64,
isolate: *v8.Isolate,
client: Client,
channel: Channel,
session: Session,
rnd: RndGen = RndGen.init(0),
handle: *v8.Inspector,
client: *v8.InspectorClientImpl,
default_context: ?v8.Global,
session: ?Session,
// We expect allocator to be an arena
// Note: This initializes the pre-allocated inspector in-place
pub fn init(self: *Inspector, isolate: *v8.Isolate, ctx: anytype) !void {
const ContextT = @TypeOf(ctx);
pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
const self = try allocator.create(Inspector);
errdefer allocator.destroy(self);
const Container = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT,
.pointer => |ptr| ptr.child,
.void => NoopInspector,
else => @compileError("invalid context type"),
};
// If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
// Initialize the fields that callbacks need first
self.* = .{
.handle = undefined,
.unique_id = 1,
.session = null,
.isolate = isolate,
.client = undefined,
.channel = undefined,
.rnd = RndGen.init(0),
.handle = undefined,
.default_context = null,
.session = undefined,
};
// Create client and set inspector data BEFORE creating the inspector
// because V8 will call generateUniqueId during inspector creation
const client = Client.init();
self.client = client;
client.setInspector(self);
self.client = v8.v8_inspector__Client__IMPL__CREATE();
errdefer v8.v8_inspector__Client__IMPL__DELETE(self.client);
v8.v8_inspector__Client__IMPL__SET_DATA(self.client, self);
// Now create the inspector - generateUniqueId will work because data is set
const handle = v8.v8_inspector__Inspector__Create(isolate, client.handle).?;
self.handle = handle;
self.handle = v8.v8_inspector__Inspector__Create(isolate, self.client).?;
errdefer v8.v8_inspector__Inspector__DELETE(self.handle);
// Create the channel
const channel = Channel.init(
safe_context,
Container.onInspectorResponse,
Container.onInspectorEvent,
Container.onRunMessageLoopOnPause,
Container.onQuitMessageLoopOnPause,
isolate,
);
self.channel = channel;
channel.setInspector(self);
// Create the session
const session_handle = v8.v8_inspector__Inspector__Connect(
handle,
CONTEXT_GROUP_ID,
channel.handle,
CLIENT_TRUST_LEVEL,
).?;
self.session = .{ .handle = session_handle };
return self;
}
pub fn deinit(self: *const Inspector) void {
pub fn deinit(self: *const Inspector, allocator: Allocator) void {
var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&hs);
self.session.deinit();
self.client.deinit();
self.channel.deinit();
if (self.session) |*s| {
s.deinit();
}
v8.v8_inspector__Client__IMPL__DELETE(self.client);
v8.v8_inspector__Inspector__DELETE(self.handle);
allocator.destroy(self);
}
pub fn send(self: *const Inspector, msg: []const u8) void {
// Can't assume the main Context exists (with its HandleScope)
// available when doing this. Pages (and thus the HandleScope)
// comes and goes, but CDP can keep sending messages.
const isolate = self.isolate;
var temp_scope: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&temp_scope, isolate);
defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
pub fn startSession(self: *Inspector, ctx: anytype) *Session {
if (comptime IS_DEBUG) {
std.debug.assert(self.session == null);
}
self.session.dispatchProtocolMessage(isolate, msg);
self.session = undefined;
Session.init(&self.session.?, self, ctx);
return &self.session.?;
}
pub fn stopSession(self: *Inspector) void {
self.session.?.deinit();
self.session = null;
}
// From CDP docs
@@ -163,51 +140,6 @@ pub fn resetContextGroup(self: *const Inspector) void {
v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
// value before, we'll get the existing js.Global(js.Object) and if not
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Inspector,
local: *const js.Local,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_val = try local.zigValueToJs(value, .{});
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
local.isolate.handle,
local.handle,
js_val.handle,
group,
generate_preview,
);
}
// Gets a value by object ID regardless of which context it is in.
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
// the pointer to the Node, so we need to use the same resolution mechanism which
// is used when we're calling a function to turn the Div into a Node, which is
// what TaggedOpaque.fromJS does.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
// just to indicate that the caller is responsible for ensure there's a local environment
_ = local;
const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const js_val = unwrapped.value;
if (!v8.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode;
}
const Node = @import("../webapi/Node.zig");
// Cast to *const v8.Object for typeTaggedAnyOpaque
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
}
pub const RemoteObject = struct {
handle: *v8.RemoteObject,
@@ -254,20 +186,109 @@ pub const RemoteObject = struct {
}
};
const Session = struct {
// Combines a v8::InspectorSession and a v8::InspectorChannelImpl. The
// InspectorSession is for zig -> v8 (sending messages to the inspector). The
// Channel is for v8 -> zig, getting events from the Inspector (that we'll pass
// back ot some opaque context, i.e the CDP BrowserContext).
// The channel callbacks are defined below, as:
// pub export fn v8_inspector__Channel__IMPL__XYZ
pub const Session = struct {
inspector: *Inspector,
handle: *v8.InspectorSession,
channel: *v8.InspectorChannelImpl,
fn deinit(self: Session) void {
v8.v8_inspector__Session__DELETE(self.handle);
// callbacks
ctx: *anyopaque,
onNotif: *const fn (ctx: *anyopaque, msg: []const u8) void,
onResp: *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void,
fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {
const Container = @typeInfo(@TypeOf(ctx)).pointer.child;
const channel = v8.v8_inspector__Channel__IMPL__CREATE(inspector.isolate);
const handle = v8.v8_inspector__Inspector__Connect(
inspector.handle,
CONTEXT_GROUP_ID,
channel,
CLIENT_TRUST_LEVEL,
).?;
v8.v8_inspector__Channel__IMPL__SET_DATA(channel, self);
self.* = .{
.ctx = ctx,
.handle = handle,
.channel = channel,
.inspector = inspector,
.onResp = Container.onInspectorResponse,
.onNotif = Container.onInspectorEvent,
};
}
fn dispatchProtocolMessage(self: Session, isolate: *v8.Isolate, msg: []const u8) void {
fn deinit(self: *const Session) void {
v8.v8_inspector__Session__DELETE(self.handle);
v8.v8_inspector__Channel__IMPL__DELETE(self.channel);
}
pub fn send(self: *const Session, msg: []const u8) void {
const isolate = self.inspector.isolate;
var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&hs, isolate);
defer v8.v8__HandleScope__DESTRUCT(&hs);
v8.v8_inspector__Session__dispatchProtocolMessage(
self.handle,
isolate,
msg.ptr,
msg.len,
);
v8.v8__Isolate__PerformMicrotaskCheckpoint(isolate);
}
// Gets a value by object ID regardless of which context it is in.
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
// the pointer to the Node, so we need to use the same resolution mechanism which
// is used when we're calling a function to turn the Div into a Node, which is
// what TaggedOpaque.fromJS does.
pub fn getNodePtr(self: *const Session, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
// just to indicate that the caller is responsible for ensuring there's a local environment
_ = local;
const unwrapped = try self.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const js_val = unwrapped.value;
if (!v8.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode;
}
const Node = @import("../webapi/Node.zig");
// Cast to *const v8.Object for typeTaggedAnyOpaque
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
// value before, we'll get the existing js.Global(js.Object) and if not
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Session,
local: *const js.Local,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_val = try local.zigValueToJs(value, .{});
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.wrapObject(
local.isolate.handle,
local.handle,
js_val.handle,
group,
generate_preview,
);
}
fn wrapObject(
@@ -334,84 +355,6 @@ const UnwrappedObject = struct {
object_group: ?[]const u8,
};
const Channel = struct {
handle: *v8.InspectorChannelImpl,
// callbacks
ctx: *anyopaque,
onNotif: onNotifFn = undefined,
onResp: onRespFn = undefined,
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn = undefined,
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn = undefined,
pub const onNotifFn = *const fn (ctx: *anyopaque, msg: []const u8) void;
pub const onRespFn = *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void;
pub const onRunMessageLoopOnPauseFn = *const fn (ctx: *anyopaque, context_group_id: u32) void;
pub const onQuitMessageLoopOnPauseFn = *const fn (ctx: *anyopaque) void;
fn init(
ctx: *anyopaque,
onResp: onRespFn,
onNotif: onNotifFn,
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn,
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn,
isolate: *v8.Isolate,
) Channel {
const handle = v8.v8_inspector__Channel__IMPL__CREATE(isolate);
return .{
.handle = handle,
.ctx = ctx,
.onResp = onResp,
.onNotif = onNotif,
.onRunMessageLoopOnPause = onRunMessageLoopOnPause,
.onQuitMessageLoopOnPause = onQuitMessageLoopOnPause,
};
}
fn deinit(self: Channel) void {
v8.v8_inspector__Channel__IMPL__DELETE(self.handle);
}
fn setInspector(self: Channel, inspector: *anyopaque) void {
v8.v8_inspector__Channel__IMPL__SET_DATA(self.handle, inspector);
}
fn resp(self: Channel, call_id: u32, msg: []const u8) void {
self.onResp(self.ctx, call_id, msg);
}
fn notif(self: Channel, msg: []const u8) void {
self.onNotif(self.ctx, msg);
}
};
const Client = struct {
handle: *v8.InspectorClientImpl,
fn init() Client {
return .{ .handle = v8.v8_inspector__Client__IMPL__CREATE() };
}
fn deinit(self: Client) void {
v8.v8_inspector__Client__IMPL__DELETE(self.handle);
}
fn setInspector(self: Client, inspector: *anyopaque) void {
v8.v8_inspector__Client__IMPL__SET_DATA(self.handle, inspector);
}
};
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
};
fn fromData(data: *anyopaque) *Inspector {
return @ptrCast(@alignCast(data));
}
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
if (!v8.v8__Value__IsObject(value)) {
return null;
@@ -437,24 +380,25 @@ pub export fn v8_inspector__Client__IMPL__generateUniqueId(
data: *anyopaque,
) callconv(.c) i64 {
const inspector: *Inspector = @ptrCast(@alignCast(data));
return inspector.rnd.random().int(i64);
const unique_id = inspector.unique_id + 1;
inspector.unique_id = unique_id;
return unique_id;
}
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
_: *v8.InspectorClientImpl,
data: *anyopaque,
ctx_group_id: c_int,
context_group_id: c_int,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.onRunMessageLoopOnPause(inspector.channel.ctx, @intCast(ctx_group_id));
_ = data;
_ = context_group_id;
}
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
_: *v8.InspectorClientImpl,
data: *anyopaque,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.onQuitMessageLoopOnPause(inspector.channel.ctx);
_ = data;
}
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
@@ -493,8 +437,8 @@ pub export fn v8_inspector__Channel__IMPL__sendResponse(
msg: [*c]u8,
length: usize,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.resp(@as(u32, @intCast(call_id)), msg[0..length]);
const session: *Session = @ptrCast(@alignCast(data));
session.onResp(session.ctx, @intCast(call_id), msg[0..length]);
}
pub export fn v8_inspector__Channel__IMPL__sendNotification(
@@ -503,8 +447,8 @@ pub export fn v8_inspector__Channel__IMPL__sendNotification(
msg: [*c]u8,
length: usize,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.notif(msg[0..length]);
const session: *Session = @ptrCast(@alignCast(data));
session.onNotif(session.ctx, msg[0..length]);
}
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(

View File

@@ -80,7 +80,9 @@ pub fn CDPT(comptime TypeProvider: type) type {
pub fn init(app: *App, client: TypeProvider.Client) !Self {
const allocator = app.allocator;
const browser = try Browser.init(app);
const browser = try Browser.init(app, .{
.env = .{ .with_inspector = true },
});
errdefer browser.deinit();
return .{
@@ -352,7 +354,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
node_registry: Node.Registry,
node_search_list: Node.Search.List,
inspector: *js.Inspector,
inspector_session: *js.Inspector.Session,
isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld),
http_proxy_changed: bool = false,
@@ -381,7 +383,9 @@ pub fn BrowserContext(comptime CDP_T: type) type {
const session = try cdp.browser.newSession();
const arena = session.arena;
const inspector = try cdp.browser.env.newInspector(arena, self);
const browser = &cdp.browser;
const inspector_session = browser.env.inspector.?.startSession(self);
errdefer browser.env.inspector.?.stopSession();
var registry = Node.Registry.init(allocator);
errdefer registry.deinit();
@@ -400,7 +404,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
.node_registry = registry,
.node_search_list = undefined,
.isolated_worlds = .empty,
.inspector = inspector,
.inspector_session = inspector_session,
.notification_arena = cdp.notification_arena.allocator(),
.intercept_state = try InterceptState.init(allocator),
.captured_responses = .empty,
@@ -409,27 +413,28 @@ 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);
try browser.notification.register(.page_remove, self, onPageRemove);
try browser.notification.register(.page_created, self, onPageCreated);
try browser.notification.register(.page_navigate, self, onPageNavigate);
try browser.notification.register(.page_navigated, self, onPageNavigated);
}
pub fn deinit(self: *Self) void {
// safe to call even if never registered
log.unregisterInterceptor();
self.log_interceptor.deinit();
const browser = &self.cdp.browser;
// Drain microtasks makes sure we don't have inspector's callback
// in progress before deinit.
self.cdp.browser.env.runMicrotasks();
browser.env.runMicrotasks();
// resetContextGroup detach the inspector from all contexts.
// It append async tasks, so we make sure we run the message loop
// before deinit it.
self.inspector.resetContextGroup();
self.session.browser.runMessageLoop();
self.inspector.deinit();
browser.env.inspector.?.resetContextGroup();
browser.runMessageLoop();
browser.env.inspector.?.stopSession();
// abort all intercepted requests before closing the sesion/page
// since some of these might callback into the page/scriptmanager
@@ -445,16 +450,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
// If the session has a page, we need to clear it first. The page
// context is always nested inside of the isolated world context,
// so we need to shutdown the page one first.
self.cdp.browser.closeSession();
browser.closeSession();
self.node_registry.deinit();
self.node_search_list.deinit();
self.cdp.browser.notification.unregisterAll(self);
browser.notification.unregisterAll(self);
if (self.http_proxy_changed) {
// has to be called after browser.closeSession, since it won't
// work if there are active connections.
self.cdp.browser.http_client.restoreOriginalProxy() catch |err| {
browser.http_client.restoreOriginalProxy() catch |err| {
log.warn(.http, "restoreOriginalProxy", .{ .err = err });
};
}
@@ -650,9 +655,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
}
pub fn callInspector(self: *const Self, msg: []const u8) void {
self.inspector.send(msg);
// force running micro tasks after send input to the inspector.
self.cdp.browser.runMicrotasks();
self.inspector_session.send(msg);
}
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
@@ -678,18 +681,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
};
}
// debugger events
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {
// onRunMessageLoopOnPause is called when a breakpoint is hit.
// Until quit pause, we must continue to run a nested message loop
// to interact with the the debugger ony (ie. Chrome DevTools).
}
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {
// Quit breakpoint pause.
}
// This is hacky x 2. First, we create the JSON payload by gluing our
// session_id onto it. Second, we're much more client/websocket aware than
// we should be.

View File

@@ -305,7 +305,7 @@ fn resolveNode(cmd: anytype) !void {
// node._node is a *DOMNode 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.inspector.getRemoteObject(
const remote_object = try bc.inspector_session.getRemoteObject(
&ls.?.local,
params.objectGroup orelse "",
node.dom,
@@ -404,7 +404,7 @@ fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?N
defer ls.deinit();
// Retrieve the object from which ever context it is in.
const parser_node = try bc.inspector.getNodePtr(arena, object_id_, &ls.local);
const parser_node = try bc.inspector_session.getNodePtr(arena, object_id_, &ls.local);
return try bc.node_registry.register(@ptrCast(@alignCast(parser_node)));
}
return error.MissingParams;

View File

@@ -189,17 +189,9 @@ fn createIsolatedWorld(cmd: anytype) !void {
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
const js_context = try world.createContext(page);
// 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});
var ls: js.Local.Scope = undefined;
js_context.localScope(&ls);
defer ls.deinit();
bc.inspector.contextCreated(&ls.local, world.name, "", aux_data, false);
return cmd.sendResult(.{ .executionContextId = ls.local.debugContextId() }, .{});
return cmd.sendResult(.{ .executionContextId = js_context.id }, .{});
}
fn navigate(cmd: anytype) !void {
@@ -359,7 +351,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
page.js.localScope(&ls);
defer ls.deinit();
bc.inspector.contextCreated(
bc.inspector_session.inspector.contextCreated(
&ls.local,
"",
try page.getOrigin(arena) orelse "",
@@ -376,7 +368,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
(isolated_world.context orelse continue).localScope(&ls);
defer ls.deinit();
bc.inspector.contextCreated(
bc.inspector_session.inspector.contextCreated(
&ls.local,
isolated_world.name,
"://",

View File

@@ -182,7 +182,7 @@ fn createTarget(cmd: anytype) !void {
defer ls.deinit();
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated(
bc.inspector_session.inspector.contextCreated(
&ls.local,
"",
"", // @ZIGDOM

View File

@@ -37,7 +37,7 @@ pub const FetchOpts = struct {
writer: ?*std.Io.Writer = null,
};
pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
var browser = try Browser.init(app);
var browser = try Browser.init(app, .{});
defer browser.deinit();
var session = try browser.newSession();

View File

@@ -43,7 +43,7 @@ pub fn main() !void {
var test_arena = std.heap.ArenaAllocator.init(allocator);
defer test_arena.deinit();
var browser = try lp.Browser.init(app);
var browser = try lp.Browser.init(app, .{});
defer browser.deinit();
const session = try browser.newSession();

View File

@@ -65,7 +65,7 @@ pub fn main() !void {
});
defer app.deinit();
var browser = try lp.Browser.init(app);
var browser = try lp.Browser.init(app, .{});
defer browser.deinit();
// An arena for running each tests. Is reset after every test.

View File

@@ -460,7 +460,7 @@ test "tests:beforeAll" {
});
errdefer test_app.deinit();
test_browser = try Browser.init(test_app);
test_browser = try Browser.init(test_app, .{});
errdefer test_browser.deinit();
test_session = try test_browser.newSession();