diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index 625305db..42171d6e 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -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); diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index e0e2d2be..b45ba00a 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -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(); } diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index 13ad79ed..54038193 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -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( diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 48825573..812200f1 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -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. diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 8bb3ac5e..1e19b9d0 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -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; diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 065c9546..ce58a69d 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -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, "://", diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index a998fff0..7b623b89 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -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 diff --git a/src/lightpanda.zig b/src/lightpanda.zig index ade06ceb..f016b76d 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -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(); diff --git a/src/main_legacy_test.zig b/src/main_legacy_test.zig index 78b85472..beaacef3 100644 --- a/src/main_legacy_test.zig +++ b/src/main_legacy_test.zig @@ -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(); diff --git a/src/main_wpt.zig b/src/main_wpt.zig index dbf2eea9..45a6e26f 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -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. diff --git a/src/testing.zig b/src/testing.zig index 3a71a0ef..13fa9921 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -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();