mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 14:33:47 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
"://",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user