diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 67f349c6..ec0f3916 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -19,6 +19,8 @@ const std = @import("std"); const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; + const Types = @import("root").Types; const parser = @import("netsurf"); @@ -57,30 +59,44 @@ pub const user_agent = "Lightpanda/1.0"; // A browser contains only one session. // TODO allow multiple sessions per browser. pub const Browser = struct { - session: Session = undefined, - agent: []const u8 = user_agent, + loop: *Loop, + session: ?*Session, + allocator: Allocator, + session_pool: SessionPool, - const uri = "about:blank"; + const SessionPool = std.heap.MemoryPool(Session); - pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void { - // We want to ensure the caller initialised a VM, but the browser - // doesn't use it directly... - _ = vm; + const uri = "about:blank"; - try Session.init(&self.session, alloc, loop, uri); + pub fn init(allocator: Allocator, loop: *Loop) Browser { + return .{ + .loop = loop, + .session = null, + .allocator = allocator, + .session_pool = SessionPool.init(allocator), + }; } pub fn deinit(self: *Browser) void { - self.session.deinit(); + self.closeSession(); + self.session_pool.deinit(); } - pub fn newSession( - self: *Browser, - alloc: std.mem.Allocator, - loop: *jsruntime.Loop, - ) !void { - self.session.deinit(); - try Session.init(&self.session, alloc, loop, uri); + pub fn newSession(self: *Browser, ctx: anytype) !*Session { + self.closeSession(); + + const session = try self.session_pool.create(); + try Session.init(session, self.allocator, ctx, self.loop, uri); + self.session = session; + return session; + } + + fn closeSession(self: *Browser) void { + if (self.session) |session| { + session.deinit(); + self.session_pool.destroy(session); + self.session = null; + } } pub fn currentPage(self: *Browser) ?*Page { @@ -96,7 +112,7 @@ pub const Browser = struct { // deinit a page before running another one. pub const Session = struct { // allocator used to init the arena. - alloc: std.mem.Allocator, + allocator: Allocator, // The arena is used only to bound the js env init b/c it leaks memory. // see https://github.com/lightpanda-io/jsruntime-lib/issues/181 @@ -109,8 +125,9 @@ pub const Session = struct { // TODO handle proxy loader: Loader, - env: Env = undefined, - inspector: ?jsruntime.Inspector = null, + + env: Env, + inspector: jsruntime.Inspector, window: Window, @@ -121,20 +138,54 @@ pub const Session = struct { jstypes: [Types.len]usize = undefined, - fn init(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void { - self.* = Session{ + fn init(self: *Session, allocator: Allocator, ctx: anytype, loop: *Loop, uri: []const u8) !void { + self.* = .{ .uri = uri, - .alloc = alloc, - .arena = std.heap.ArenaAllocator.init(alloc), + .env = undefined, + .inspector = undefined, + .allocator = allocator, + .loader = Loader.init(allocator), + .httpClient = .{ .allocator = allocator }, + .storageShed = storage.Shed.init(allocator), + .arena = std.heap.ArenaAllocator.init(allocator), .window = Window.create(null, .{ .agent = user_agent }), - .loader = Loader.init(alloc), - .storageShed = storage.Shed.init(alloc), - .httpClient = undefined, }; - Env.init(&self.env, self.arena.allocator(), loop, null); - self.httpClient = .{ .allocator = alloc }; + const arena = self.arena.allocator(); + + Env.init(&self.env, arena, loop, null); + errdefer self.env.deinit(); try self.env.load(&self.jstypes); + + const ContextT = @TypeOf(ctx); + const InspectorContainer = switch (@typeInfo(ContextT)) { + .Struct => ContextT, + .Pointer => |ptr| ptr.child, + .Void => NoopInspector, + else => @compileError("invalid context type"), + }; + + // const ctx_opaque = @as(*anyopaque, @ptrCast(ctx)); + self.inspector = try jsruntime.Inspector.init( + arena, + self.env, + if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx, + InspectorContainer.onInspectorResponse, + InspectorContainer.onInspectorEvent, + ); + self.env.setInspector(self.inspector); + } + + fn deinit(self: *Session) void { + if (self.page) |*p| { + p.deinit(); + } + + self.env.deinit(); + self.arena.deinit(); + self.httpClient.deinit(); + self.loader.deinit(); + self.storageShed.deinit(); } fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module { @@ -152,49 +203,21 @@ pub const Session = struct { return self.env.compileModule(body, specifier); } - fn deinit(self: *Session) void { - if (self.page) |*p| p.deinit(); - - if (self.inspector) |inspector| { - inspector.deinit(self.alloc); - } - - self.env.deinit(); - self.arena.deinit(); - - self.httpClient.deinit(); - self.loader.deinit(); - self.storageShed.deinit(); - } - - pub fn initInspector( - self: *Session, - ctx: anytype, - onResp: jsruntime.InspectorOnResponseFn, - onEvent: jsruntime.InspectorOnEventFn, - ) !void { - const ctx_opaque = @as(*anyopaque, @ptrCast(ctx)); - self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx_opaque, onResp, onEvent); - self.env.setInspector(self.inspector.?); - } - pub fn callInspector(self: *Session, msg: []const u8) void { - if (self.inspector) |inspector| { - inspector.send(msg, self.env); - } else { - @panic("No Inspector"); - } + self.inspector.send(self.env, msg); } // NOTE: the caller is not the owner of the returned value, // the pointer on Page is just returned as a convenience pub fn createPage(self: *Session) !*Page { if (self.page != null) return error.SessionPageExists; - const p: Page = undefined; - self.page = p; - Page.init(&self.page.?, self.alloc, self); + self.page = Page.init(self.allocator, self); return &self.page.?; } + + pub fn currentPage(self: *Session) ?*Page { + return &(self.page orelse return null); + } }; // Page navigates to an url. @@ -203,8 +226,8 @@ pub const Session = struct { // The page handle all its memory in an arena allocator. The arena is reseted // when end() is called. pub const Page = struct { - arena: std.heap.ArenaAllocator, session: *Session, + arena: std.heap.ArenaAllocator, doc: ?*parser.Document = null, // handle url @@ -218,17 +241,19 @@ pub const Page = struct { raw_data: ?[]const u8 = null, - fn init( - self: *Page, - alloc: std.mem.Allocator, - session: *Session, - ) void { - self.* = .{ - .arena = std.heap.ArenaAllocator.init(alloc), + fn init(allocator: Allocator, session: *Session) Page { + return .{ .session = session, + .arena = std.heap.ArenaAllocator.init(allocator), }; } + pub fn deinit(self: *Page) void { + self.end(); + self.arena.deinit(); + self.session.page = null; + } + // start js env. // - auxData: extra data forwarded to the Inspector // see Inspector.contextCreated @@ -253,10 +278,8 @@ pub const Page = struct { try polyfill.load(self.arena.allocator(), self.session.env); // inspector - if (self.session.inspector) |inspector| { - log.debug("inspector context created", .{}); - inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData); - } + log.debug("inspector context created", .{}); + self.session.inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData); } // reset js env and mem arena. @@ -264,7 +287,6 @@ pub const Page = struct { self.session.env.stop(); // TODO unload document: https://html.spec.whatwg.org/#unloading-documents - if (self.url) |*u| u.deinit(self.arena.allocator()); self.url = null; self.location.url = null; self.session.window.replaceLocation(&self.location) catch |e| { @@ -278,14 +300,8 @@ pub const Page = struct { _ = self.arena.reset(.free_all); } - pub fn deinit(self: *Page) void { - self.end(); - self.arena.deinit(); - self.session.page = null; - } - // dump writes the page content into the given file. - pub fn dump(self: *Page, out: std.fs.File) !void { + pub fn dump(self: *const Page, out: std.fs.File) !void { // if no HTML document pointer available, dump the data content only. if (self.doc == null) { @@ -333,11 +349,9 @@ pub const Page = struct { } // own the url - if (self.rawuri) |prev| alloc.free(prev); self.rawuri = try alloc.dupe(u8, uri); self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?); - if (self.url) |*prev| prev.deinit(alloc); self.url = try URL.constructor(alloc, self.rawuri.?, null); self.location.url = &self.url.?; try self.session.window.replaceLocation(&self.location); @@ -435,9 +449,7 @@ pub const Page = struct { // https://html.spec.whatwg.org/#read-html // inspector - if (self.session.inspector) |inspector| { - inspector.contextCreated(self.session.env, "", self.origin.?, auxData); - } + self.session.inspector.contextCreated(self.session.env, "", self.origin.?, auxData); // replace the user context document with the new one. try self.session.env.setUserContext(.{ @@ -596,7 +608,7 @@ pub const Page = struct { }; // the caller owns the returned string - fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 { + fn fetchData(self: *Page, alloc: Allocator, src: []const u8) ![]const u8 { log.debug("starting fetch {s}", .{src}); var buffer: [1024]u8 = undefined; @@ -671,7 +683,7 @@ pub const Page = struct { return .unknown; } - fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void { + fn eval(self: Script, alloc: Allocator, env: Env, body: []const u8) !void { var try_catch: jsruntime.TryCatch = undefined; try_catch.init(env); defer try_catch.deinit(); @@ -696,3 +708,8 @@ pub const Page = struct { } }; }; + +const NoopInspector = struct { + pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {} + pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {} +}; diff --git a/src/cdp/browser.zig b/src/cdp/browser.zig index 12f882e2..72b0f44d 100644 --- a/src/cdp/browser.zig +++ b/src/cdp/browser.zig @@ -17,132 +17,66 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Methods = enum { - getVersion, - setDownloadBehavior, - getWindowForTarget, - setWindowBounds, -}; - -pub fn browser( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - return switch (method) { - .getVersion => getVersion(alloc, msg, ctx), - .setDownloadBehavior => setDownloadBehavior(alloc, msg, ctx), - .getWindowForTarget => getWindowForTarget(alloc, msg, ctx), - .setWindowBounds => setWindowBounds(alloc, msg, ctx), - }; -} // TODO: hard coded data -const ProtocolVersion = "1.3"; -const Product = "Chrome/124.0.6367.29"; -const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4"; -const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; -const JsVersion = "12.4.254.8"; - -fn getVersion( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getVersion" }); +const PROTOCOL_VERSION = "1.3"; +const PRODUCT = "Chrome/124.0.6367.29"; +const REVISION = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4"; +const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; +const JS_VERSION = "12.4.254.8"; +const DEV_TOOLS_WINDOW_ID = 1923710101; + +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + getVersion, + setDownloadBehavior, + getWindowForTarget, + setWindowBounds, + }, cmd.action) orelse return error.UnknownMethod; + + switch (action) { + .getVersion => return getVersion(cmd), + .setDownloadBehavior => return setDownloadBehavior(cmd), + .getWindowForTarget => return getWindowForTarget(cmd), + .setWindowBounds => return setWindowBounds(cmd), + } +} - // ouput - const Res = struct { - protocolVersion: []const u8 = ProtocolVersion, - product: []const u8 = Product, - revision: []const u8 = Revision, - userAgent: []const u8 = UserAgent, - jsVersion: []const u8 = JsVersion, - }; - return result(alloc, input.id, Res, .{}, null); +fn getVersion(cmd: anytype) !void { + // TODO: pre-serialize? + return cmd.sendResult(.{ + .protocolVersion = PROTOCOL_VERSION, + .product = PRODUCT, + .revision = REVISION, + .userAgent = USER_AGENT, + .jsVersion = JS_VERSION, + }, .{ .include_session_id = false }); } // TODO: noop method -fn setDownloadBehavior( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const Params = struct { - behavior: []const u8, - browserContextId: ?[]const u8 = null, - downloadPath: ?[]const u8 = null, - eventsEnabled: ?bool = null, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("REQ > id {d}, method {s}", .{ input.id, "browser.setDownloadBehavior" }); - - // output - return result(alloc, input.id, null, null, null); +fn setDownloadBehavior(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // behavior: []const u8, + // browserContextId: ?[]const u8 = null, + // downloadPath: ?[]const u8 = null, + // eventsEnabled: ?bool = null, + // })) orelse return error.InvalidParams; + + return cmd.sendResult(null, .{ .include_session_id = false }); } -// TODO: hard coded ID -const DevToolsWindowID = 1923710101; - -fn getWindowForTarget( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { +fn getWindowForTarget(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // targetId: ?[]const u8 = null, + // })) orelse return error.InvalidParams; - // input - const Params = struct { - targetId: ?[]const u8 = null, - }; - const input = try Input(?Params).get(alloc, msg); - defer input.deinit(); - std.debug.assert(input.sessionId != null); - log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getWindowForTarget" }); - - // output - const Resp = struct { - windowId: u64 = DevToolsWindowID, - bounds: struct { - left: ?u64 = null, - top: ?u64 = null, - width: ?u64 = null, - height: ?u64 = null, - windowState: []const u8 = "normal", - } = .{}, - }; - return result(alloc, input.id, Resp, Resp{}, input.sessionId); + return cmd.sendResult(.{ .windowId = DEV_TOOLS_WINDOW_ID, .bounds = .{ + .windowState = "normal", + } }, .{}); } // TODO: noop method -fn setWindowBounds( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "browser.setWindowBounds" }); - - // output - return result(alloc, input.id, null, null, input.sessionId); +fn setWindowBounds(cmd: anytype) !void { + return cmd.sendResult(null, .{}); } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 0ba40531..cfd65029 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -17,237 +17,408 @@ // along with this program. If not, see . const std = @import("std"); +const Allocator = std.mem.Allocator; +const json = std.json; -const server = @import("../server.zig"); -const Ctx = server.Ctx; - -const browser = @import("browser.zig").browser; -const target = @import("target.zig").target; -const page = @import("page.zig").page; -const log = @import("log.zig").log; -const runtime = @import("runtime.zig").runtime; -const network = @import("network.zig").network; -const emulation = @import("emulation.zig").emulation; -const fetch = @import("fetch.zig").fetch; -const performance = @import("performance.zig").performance; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; -const inspector = @import("inspector.zig").inspector; -const dom = @import("dom.zig").dom; -const cdpdom = @import("dom.zig"); -const css = @import("css.zig").css; -const security = @import("security.zig").security; - -const log_cdp = std.log.scoped(.cdp); - -pub const Error = error{ - UnknonwDomain, - UnknownMethod, - NoResponse, - RequestWithoutID, +const dom = @import("dom.zig"); +const Loop = @import("jsruntime").Loop; +const Client = @import("../server.zig").Client; +const asUint = @import("../str/parser.zig").asUint; +const Browser = @import("../browser/browser.zig").Browser; +const Session = @import("../browser/browser.zig").Session; + +const log = std.log.scoped(.cdp); + +pub const URL_BASE = "chrome://newtab/"; +pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C"; +pub const FRAME_ID = "FRAMEIDD8AED408A0467AC93100BCDBE"; +pub const BROWSER_SESSION_ID = @tagName(SessionID.BROWSERSESSIONID597D9875C664CAC0); +pub const CONTEXT_SESSION_ID = @tagName(SessionID.CONTEXTSESSIONID0497A05C95417CF4); + +pub const TimestampEvent = struct { + timestamp: f64, }; -pub fn isCdpError(err: anyerror) ?Error { - // see https://github.com/ziglang/zig/issues/2473 - const errors = @typeInfo(Error).ErrorSet.?; - inline for (errors) |e| { - if (std.mem.eql(u8, e.name, @errorName(err))) { - return @errorCast(err); +pub const CDP = struct { + // Used for sending message to the client and closing on error + client: *Client, + + // The active browser + browser: Browser, + + // The active browser session + session: ?*Session, + + allocator: Allocator, + + // Re-used arena for processing a message. We're assuming that we're getting + // 1 message at a time. + message_arena: std.heap.ArenaAllocator, + + // State + url: []const u8, + frame_id: []const u8, + loader_id: []const u8, + session_id: SessionID, + context_id: ?[]const u8, + execution_context_id: u32, + security_origin: []const u8, + page_life_cycle_events: bool, + secure_context_type: []const u8, + node_list: dom.NodeList, + node_search_list: dom.NodeSearchList, + + pub fn init(allocator: Allocator, client: *Client, loop: *Loop) CDP { + return .{ + .client = client, + .browser = Browser.init(allocator, loop), + .session = null, + .allocator = allocator, + .url = URL_BASE, + .execution_context_id = 0, + .context_id = null, + .frame_id = FRAME_ID, + .session_id = .CONTEXTSESSIONID0497A05C95417CF4, + .security_origin = URL_BASE, + .secure_context_type = "Secure", // TODO = enum + .loader_id = LOADER_ID, + .message_arena = std.heap.ArenaAllocator.init(allocator), + .page_life_cycle_events = false, // TODO; Target based value + .node_list = dom.NodeList.init(allocator), + .node_search_list = dom.NodeSearchList.init(allocator), + }; + } + + pub fn deinit(self: *CDP) void { + self.node_list.deinit(); + for (self.node_search_list.items) |*s| { + s.deinit(); } + self.node_search_list.deinit(); + + self.browser.deinit(); + self.message_arena.deinit(); } - return null; -} -const Domains = enum { - Browser, - Target, - Page, - Log, - Runtime, - Network, - DOM, - CSS, - Inspector, - Emulation, - Fetch, - Performance, - Security, -}; + pub fn reset(self: *CDP) void { + self.node_list.reset(); -// The caller is responsible for calling `free` on the returned slice. -pub fn do( - alloc: std.mem.Allocator, - s: []const u8, - ctx: *Ctx, -) anyerror![]const u8 { + // deinit all node searches. + for (self.node_search_list.items) |*s| { + s.deinit(); + } + self.node_search_list.clearAndFree(); + } - // incoming message parser - var msg = IncomingMessage.init(alloc, s); - defer msg.deinit(); + pub fn newSession(self: *CDP) !void { + self.session = try self.browser.newSession(self); + } - return dispatch(alloc, &msg, ctx); -} + pub fn processMessage(self: *CDP, msg: []const u8) bool { + const arena = &self.message_arena; + defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 }); -pub fn dispatch( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) anyerror![]const u8 { - const method = try msg.getMethod(); - - // retrieve domain from method - var iter = std.mem.splitScalar(u8, method, '.'); - const domain = std.meta.stringToEnum(Domains, iter.first()) orelse - return error.UnknonwDomain; - - // select corresponding domain - const action = iter.next() orelse return error.BadMethod; - return switch (domain) { - .Browser => browser(alloc, msg, action, ctx), - .Target => target(alloc, msg, action, ctx), - .Page => page(alloc, msg, action, ctx), - .Log => log(alloc, msg, action, ctx), - .Runtime => runtime(alloc, msg, action, ctx), - .Network => network(alloc, msg, action, ctx), - .DOM => dom(alloc, msg, action, ctx), - .CSS => css(alloc, msg, action, ctx), - .Inspector => inspector(alloc, msg, action, ctx), - .Emulation => emulation(alloc, msg, action, ctx), - .Fetch => fetch(alloc, msg, action, ctx), - .Performance => performance(alloc, msg, action, ctx), - .Security => security(alloc, msg, action, ctx), - }; -} + self.dispatch(arena.allocator(), self, msg) catch |err| { + log.err("failed to process message: {}\n{s}", .{ err, msg }); + return false; + }; + return true; + } -pub const State = struct { - executionContextId: u32 = 0, - contextID: ?[]const u8 = null, - sessionID: SessionID = .CONTEXTSESSIONID0497A05C95417CF4, - frameID: []const u8 = FrameID, - url: []const u8 = URLBase, - securityOrigin: []const u8 = URLBase, - secureContextType: []const u8 = "Secure", // TODO: enum - loaderID: []const u8 = LoaderID, + // Called from above, in processMessage which handles client messages + // but can also be called internally. For example, Target.sendMessageToTarget + // calls back into dispatch. + pub fn dispatch( + self: *CDP, + arena: Allocator, + sender: anytype, + str: []const u8, + ) anyerror!void { + const input = try json.parseFromSliceLeaky(InputMessage, arena, str, .{ + .ignore_unknown_fields = true, + }); + + const domain, const action = blk: { + const method = input.method; + const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { + return error.InvalidMethod; + }; + break :blk .{ method[0..i], method[i + 1 ..] }; + }; - page_life_cycle_events: bool = false, // TODO; Target based value + var command = Command(@TypeOf(sender)){ + .json = str, + .cdp = self, + .id = input.id, + .arena = arena, + .action = action, + ._params = input.params, + .session_id = input.sessionId, + .sender = sender, + .session = self.session orelse blk: { + try self.newSession(); + break :blk self.session.?; + }, + }; + + switch (domain.len) { + 3 => switch (@as(u24, @bitCast(domain[0..3].*))) { + asUint("DOM") => return @import("dom.zig").processMessage(&command), + asUint("Log") => return @import("log.zig").processMessage(&command), + asUint("CSS") => return @import("css.zig").processMessage(&command), + else => {}, + }, + 4 => switch (@as(u32, @bitCast(domain[0..4].*))) { + asUint("Page") => return @import("page.zig").processMessage(&command), + else => {}, + }, + 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { + asUint("Fetch") => return @import("fetch.zig").processMessage(&command), + else => {}, + }, + 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { + asUint("Target") => return @import("target.zig").processMessage(&command), + else => {}, + }, + 7 => switch (@as(u56, @bitCast(domain[0..7].*))) { + asUint("Browser") => return @import("browser.zig").processMessage(&command), + asUint("Runtime") => return @import("runtime.zig").processMessage(&command), + asUint("Network") => return @import("network.zig").processMessage(&command), + else => {}, + }, + 8 => switch (@as(u64, @bitCast(domain[0..8].*))) { + asUint("Security") => return @import("security.zig").processMessage(&command), + else => {}, + }, + 9 => switch (@as(u72, @bitCast(domain[0..9].*))) { + asUint("Emulation") => return @import("emulation.zig").processMessage(&command), + asUint("Inspector") => return @import("inspector.zig").processMessage(&command), + else => {}, + }, + 11 => switch (@as(u88, @bitCast(domain[0..11].*))) { + asUint("Performance") => return @import("performance.zig").processMessage(&command), + else => {}, + }, + else => {}, + } + return error.UnknownDomain; + } - // DOM - nodelist: cdpdom.NodeList, - nodesearchlist: cdpdom.NodeSearchList, + fn sendJSON(self: *CDP, message: anytype) !void { + return self.client.sendJSON(message, .{ + .emit_null_optional_fields = false, + }); + } - pub fn init(alloc: std.mem.Allocator) State { - return .{ - .nodelist = cdpdom.NodeList.init(alloc), - .nodesearchlist = cdpdom.NodeSearchList.init(alloc), + pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { + if (std.log.defaultLogEnabled(.debug)) { + // msg should be {"id":,... + std.debug.assert(std.mem.startsWith(u8, msg, "{\"id\":")); + + const id_end = std.mem.indexOfScalar(u8, msg, ',') orelse { + log.warn("invalid inspector response message: {s}", .{msg}); + return; + }; + const id = msg[6..id_end]; + log.debug("Res (inspector) > id {s}", .{id}); + } + sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| { + log.err("Failed to send inspector response: {any}", .{err}); }; } - pub fn deinit(self: *State) void { - self.nodelist.deinit(); + pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void { + if (std.log.defaultLogEnabled(.debug)) { + // msg should be {"method":,... + std.debug.assert(std.mem.startsWith(u8, msg, "{\"method\":")); + const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse { + log.warn("invalid inspector event message: {s}", .{msg}); + return; + }; + const method = msg[10..method_end]; + log.debug("Event (inspector) > method {s}", .{method}); + } - // deinit all node searches. - for (self.nodesearchlist.items) |*s| s.deinit(); - self.nodesearchlist.deinit(); + sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| { + log.err("Failed to send inspector event: {any}", .{err}); + }; } - pub fn reset(self: *State) void { - self.nodelist.reset(); + // This is hacky * 2. First, we have the JSON payload by gluing our + // session_id onto it. Second, we're much more client/websocket aware than + // we should be. + fn sendInspectorMessage(self: *CDP, msg: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(self.allocator); + errdefer arena.deinit(); - // deinit all node searches. - for (self.nodesearchlist.items) |*s| s.deinit(); - self.nodesearchlist.clearAndFree(); + const field = ",\"sessionId\":\""; + const session_id = @tagName(self.session_id); + + // + 1 for the closing quote after the session id + // + 10 for the max websocket header + + const message_len = msg.len + session_id.len + 1 + field.len + 10; + + var buf: std.ArrayListUnmanaged(u8) = .{}; + buf.ensureTotalCapacity(arena.allocator(), message_len) catch |err| { + log.err("Failed to expand inspector buffer: {any}", .{err}); + return; + }; + + // reserve 10 bytes for websocket header + buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); + + // -1 because we dont' want the closing brace '}' + buf.appendSliceAssumeCapacity(msg[0 .. msg.len - 1]); + buf.appendSliceAssumeCapacity(field); + buf.appendSliceAssumeCapacity(session_id); + buf.appendSliceAssumeCapacity("\"}"); + std.debug.assert(buf.items.len == message_len); + + try self.client.sendJSONRaw(arena, buf); } }; -// Utils -// ----- +// This is a generic because when we send a result we have two different +// behaviors. Normally, we're sending the result to the client. But in some cases +// we want to capture the result. So we want the command.sendResult to be +// generic. +pub fn Command(comptime Sender: type) type { + return struct { + // refernece to our CDP instance + cdp: *CDP, -pub fn dumpFile( - alloc: std.mem.Allocator, - id: u16, - script: []const u8, -) !void { - const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id}); - defer alloc.free(name); - var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{}); - defer dir.close(); - const f = try dir.createFile(name, .{}); - defer f.close(); - const nb = try f.write(script); - std.debug.assert(nb == script.len); - const p = try dir.realpathAlloc(alloc, name); - defer alloc.free(p); -} + // Comes directly from the input.id field + id: ?i64, -// caller owns the slice returned -pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 { - var out = std.ArrayList(u8).init(alloc); - defer out.deinit(); + // A misc arena that can be used for any allocation for processing + // the message + arena: Allocator, - // Do not emit optional null fields - const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false }; + // the browser session + session: *Session, - try std.json.stringify(res, options, out.writer()); - const ret = try alloc.alloc(u8, out.items.len); - @memcpy(ret, out.items); - return ret; -} + // The "action" of the message.Given a method of "LOG.enable", the + // action is "enable" + action: []const u8, + + // Comes directly from the input.sessionId field + session_id: ?[]const u8, -const resultNull = "{{\"id\": {d}, \"result\": {{}}}}"; -const resultNullSession = "{{\"id\": {d}, \"result\": {{}}, \"sessionId\": \"{s}\"}}"; - -// caller owns the slice returned -pub fn result( - alloc: std.mem.Allocator, - id: u16, - comptime T: ?type, - res: anytype, - sessionID: ?[]const u8, -) ![]const u8 { - log_cdp.debug( - "Res > id {d}, sessionID {?s}, result {any}", - .{ id, sessionID, res }, - ); - if (T == null) { - // No need to stringify a custom JSON msg, just use string templates - if (sessionID) |sID| { - return try std.fmt.allocPrint(alloc, resultNullSession, .{ id, sID }); + // Unparsed / untyped input.params. + _params: ?InputParams, + + // The full raw json input + json: []const u8, + + sender: Sender, + + const Self = @This(); + + pub fn params(self: *const Self, comptime T: type) !?T { + if (self._params) |p| { + return try json.parseFromSliceLeaky( + T, + self.arena, + p.raw, + .{ .ignore_unknown_fields = true }, + ); + } + return null; } - return try std.fmt.allocPrint(alloc, resultNull, .{id}); - } - const Resp = struct { - id: u16, - result: T.?, - sessionId: ?[]const u8, - }; - const resp = Resp{ .id = id, .result = res, .sessionId = sessionID }; + const SendResultOpts = struct { + include_session_id: bool = true, + }; + pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void { + return self.sender.sendJSON(.{ + .id = self.id, + .result = if (comptime @typeInfo(@TypeOf(result)) == .Null) struct {}{} else result, + .sessionId = if (opts.include_session_id) self.session_id else null, + }); + } + const SendEventOpts = struct { + session_id: ?[]const u8 = null, + }; - return stringify(alloc, resp); + pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void { + // Events ALWAYS go to the client. self.sender should not be used + return self.cdp.sendJSON(.{ + .method = method, + .params = if (comptime @typeInfo(@TypeOf(p)) == .Null) struct {}{} else p, + .sessionId = opts.session_id, + }); + } + }; } -pub fn sendEvent( - alloc: std.mem.Allocator, - ctx: *Ctx, - name: []const u8, - comptime T: type, - params: T, - sessionID: ?[]const u8, -) !void { - // some clients like chromedp expects empty parameters structs. - if (T == void) @compileError("sendEvent: use struct{} instead of void for empty parameters"); - - log_cdp.debug("Event > method {s}, sessionID {?s}", .{ name, sessionID }); - const Resp = struct { - method: []const u8, - params: T, - sessionId: ?[]const u8, - }; - const resp = Resp{ .method = name, .params = params, .sessionId = sessionID }; +// When we parse a JSON message from the client, this is the structure +// we always expect +const InputMessage = struct { + id: ?i64, + method: []const u8, + params: ?InputParams = null, + sessionId: ?[]const u8 = null, +}; - const event_msg = try stringify(alloc, resp); - try ctx.send(event_msg); -} +// The JSON "params" field changes based on the "method". Initially, we just +// capture the raw json object (including the opening and closing braces). +// Then, when we're processing the message, and we know what type it is, we +// can parse it (in Disaptch(T).params). +const InputParams = struct { + raw: []const u8, + + pub fn jsonParse( + _: Allocator, + scanner: *json.Scanner, + _: json.ParseOptions, + ) !InputParams { + const height = scanner.stackHeight(); + + const start = scanner.cursor; + if (try scanner.next() != .object_begin) { + return error.UnexpectedToken; + } + try scanner.skipUntilStackHeight(height); + const end = scanner.cursor; + + return .{ .raw = scanner.input[start..end] }; + } +}; + +// Utils +// ----- + +// pub fn dumpFile( +// alloc: std.mem.Allocator, +// id: u16, +// script: []const u8, +// ) !void { +// const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id}); +// defer alloc.free(name); +// var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{}); +// defer dir.close(); +// const f = try dir.createFile(name, .{}); +// defer f.close(); +// const nb = try f.write(script); +// std.debug.assert(nb == script.len); +// const p = try dir.realpathAlloc(alloc, name); +// defer alloc.free(p); +// } + +// // caller owns the slice returned +// pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 { +// var out = std.ArrayList(u8).init(alloc); +// defer out.deinit(); + +// // Do not emit optional null fields +// const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false }; + +// try std.json.stringify(res, options, out.writer()); +// const ret = try alloc.alloc(u8, out.items.len); +// @memcpy(ret, out.items); +// return ret; +// } // Common // ------ @@ -258,20 +429,9 @@ pub const SessionID = enum { CONTEXTSESSIONID0497A05C95417CF4, pub fn parse(str: []const u8) !SessionID { - inline for (@typeInfo(SessionID).Enum.fields) |enumField| { - if (std.mem.eql(u8, str, enumField.name)) { - return @field(SessionID, enumField.name); - } - } - return error.InvalidSessionID; + return std.meta.stringToEnum(SessionID, str) orelse { + log.err("parse sessionID: {s}", .{str}); + return error.InvalidSessionID; + }; } }; -pub const BrowserSessionID = @tagName(SessionID.BROWSERSESSIONID597D9875C664CAC0); -pub const ContextSessionID = @tagName(SessionID.CONTEXTSESSIONID0497A05C95417CF4); -pub const URLBase = "chrome://newtab/"; -pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C"; -pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE"; - -pub const TimestampEvent = struct { - timestamp: f64, -}; diff --git a/src/cdp/css.zig b/src/cdp/css.zig index 542a2400..21834d83 100644 --- a/src/cdp/css.zig +++ b/src/cdp/css.zig @@ -17,43 +17,14 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Methods = enum { - enable, -}; - -pub fn css( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .enable => enable(alloc, msg, ctx), - }; -} -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" }); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + }, cmd.action) orelse return error.UnknownMethod; - return result(alloc, input.id, null, null, input.sessionId); + switch (action) { + .enable => return cmd.sendResult(null, .{}), + } } diff --git a/src/cdp/dom.zig b/src/cdp/dom.zig index d5c661f6..2bcacd41 100644 --- a/src/cdp/dom.zig +++ b/src/cdp/dom.zig @@ -17,56 +17,27 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; const css = @import("../dom/css.zig"); const parser = @import("netsurf"); -const log = std.log.scoped(.cdp); - -const Methods = enum { - enable, - getDocument, - performSearch, - getSearchResults, - discardSearchResults, -}; - -pub fn dom( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .enable => enable(alloc, msg, ctx), - .getDocument => getDocument(alloc, msg, ctx), - .performSearch => performSearch(alloc, msg, ctx), - .getSearchResults => getSearchResults(alloc, msg, ctx), - .discardSearchResults => discardSearchResults(alloc, msg, ctx), - }; -} - -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" }); - - return result(alloc, input.id, null, null, input.sessionId); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + getDocument, + performSearch, + getSearchResults, + discardSearchResults, + }, cmd.action) orelse return error.UnknownMethod; + + switch (action) { + .enable => return cmd.sendResult(null, .{}), + .getDocument => return getDocument(cmd), + .performSearch => return performSearch(cmd), + .getSearchResults => return getSearchResults(cmd), + .discardSearchResults => return discardSearchResults(cmd), + } } // NodeList references tree nodes with an array id. @@ -90,12 +61,15 @@ pub const NodeList = struct { } pub fn set(self: *NodeList, node: *parser.Node) !NodeId { - for (self.coll.items, 0..) |n, i| { - if (n == node) return @intCast(i); + const coll = &self.coll; + for (coll.items, 0..) |n, i| { + if (n == node) { + return @intCast(i); + } } - try self.coll.append(node); - return @intCast(self.coll.items.len); + try coll.append(node); + return @intCast(coll.items.len); } }; @@ -141,11 +115,9 @@ const Node = struct { var list = try std.ArrayList(Node).initCapacity(alloc, ln); - var i: u32 = 0; - while (i < ln) { - defer i += 1; - const child = try parser.nodeListItem(children, i) orelse continue; - try list.append(try Node.init(child, nlist)); + for (0..ln) |i| { + const child = try parser.nodeListItem(children, @intCast(i)) orelse continue; + list.appendAssumeCapacity(try Node.init(child, nlist)); } self.children = list.items; @@ -155,43 +127,24 @@ const Node = struct { }; // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument -fn getDocument( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { - depth: ?u32 = null, - pierce: ?bool = null, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - std.debug.assert(input.sessionId != null); - log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.getDocument" }); +fn getDocument(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // depth: ?u32 = null, + // pierce: ?bool = null, + // })) orelse return error.InvalidParams; // retrieve the root node - const page = ctx.browser.currentPage() orelse return error.NoPage; - - if (page.doc == null) return error.NoDocument; + const page = cmd.session.page orelse return error.NoPage; + const doc = page.doc orelse return error.NoDocument; - const node = parser.documentToNode(page.doc.?); - var n = try Node.init(node, &ctx.state.nodelist); - var list = try n.initChildren(alloc, node, &ctx.state.nodelist); - defer list.deinit(); + const state = cmd.cdp; + const node = parser.documentToNode(doc); + var n = try Node.init(node, &state.node_list); + _ = try n.initChildren(cmd.arena, node, &state.node_list); - // output - const Resp = struct { - root: Node, - }; - const resp: Resp = .{ + return cmd.sendResult(.{ .root = n, - }; - - const res = try result(alloc, input.id, Resp, resp, input.sessionId); - try ctx.send(res); - - return ""; + }, .{}); } pub const NodeSearch = struct { @@ -225,118 +178,82 @@ pub const NodeSearch = struct { pub const NodeSearchList = std.ArrayList(NodeSearch); // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch -fn performSearch( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn performSearch(cmd: anytype) !void { + const params = (try cmd.params(struct { query: []const u8, includeUserAgentShadowDOM: ?bool = null, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - std.debug.assert(input.sessionId != null); - log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.performSearch" }); + })) orelse return error.InvalidParams; // retrieve the root node - const page = ctx.browser.currentPage() orelse return error.NoPage; - - if (page.doc == null) return error.NoDocument; + const page = cmd.session.page orelse return error.NoPage; + const doc = page.doc orelse return error.NoDocument; - const list = try css.querySelectorAll(alloc, parser.documentToNode(page.doc.?), input.params.query); + const list = try css.querySelectorAll(cmd.cdp.allocator, parser.documentToNode(doc), params.query); const ln = list.nodes.items.len; - var ns = try NodeSearch.initCapacity(alloc, ln); + var ns = try NodeSearch.initCapacity(cmd.cdp.allocator, ln); + var state = cmd.cdp; for (list.nodes.items) |n| { - const id = try ctx.state.nodelist.set(n); + const id = try state.node_list.set(n); try ns.append(id); } - try ctx.state.nodesearchlist.append(ns); + try state.node_search_list.append(ns); - // output - const Resp = struct { - searchId: []const u8, - resultCount: u32, - }; - const resp: Resp = .{ + return cmd.sendResult(.{ .searchId = ns.name, - .resultCount = @intCast(ln), - }; - - return result(alloc, input.id, Resp, resp, input.sessionId); + .resultCount = @as(u32, @intCast(ln)), + }, .{}); } // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults -fn discardSearchResults( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn discardSearchResults(cmd: anytype) !void { + const params = (try cmd.params(struct { searchId: []const u8, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - std.debug.assert(input.sessionId != null); - log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.discardSearchResults" }); + })) orelse return error.InvalidParams; + var state = cmd.cdp; // retrieve the search from context - for (ctx.state.nodesearchlist.items, 0..) |*s, i| { - if (!std.mem.eql(u8, s.name, input.params.searchId)) continue; + for (state.node_search_list.items, 0..) |*s, i| { + if (!std.mem.eql(u8, s.name, params.searchId)) continue; s.deinit(); - _ = ctx.state.nodesearchlist.swapRemove(i); + _ = state.node_search_list.swapRemove(i); break; } - return result(alloc, input.id, null, null, input.sessionId); + return cmd.sendResult(null, .{}); } // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults -fn getSearchResults( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn getSearchResults(cmd: anytype) !void { + const params = (try cmd.params(struct { searchId: []const u8, fromIndex: u32, toIndex: u32, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - std.debug.assert(input.sessionId != null); - log.debug("Req > id {d}, method {s}", .{ input.id, "DOM.getSearchResults" }); + })) orelse return error.InvalidParams; - if (input.params.fromIndex >= input.params.toIndex) return error.BadIndices; + if (params.fromIndex >= params.toIndex) { + return error.BadIndices; + } + const state = cmd.cdp; // retrieve the search from context var ns: ?*const NodeSearch = undefined; - for (ctx.state.nodesearchlist.items) |s| { - if (!std.mem.eql(u8, s.name, input.params.searchId)) continue; - + for (state.node_search_list.items) |s| { + if (!std.mem.eql(u8, s.name, params.searchId)) continue; ns = &s; break; } - if (ns == null) return error.searchResultNotFound; - const items = ns.?.coll.items; + if (ns == null) { + return error.searchResultNotFound; + } - if (input.params.fromIndex >= items.len) return error.BadFromIndex; - if (input.params.toIndex > items.len) return error.BadToIndex; + const items = ns.?.coll.items; - // output - const Resp = struct { - nodeIds: []NodeId, - }; - const resp: Resp = .{ - .nodeIds = ns.?.coll.items[input.params.fromIndex..input.params.toIndex], - }; + if (params.fromIndex >= items.len) return error.BadFromIndex; + if (params.toIndex > items.len) return error.BadToIndex; - return result(alloc, input.id, Resp, resp, input.sessionId); + return cmd.sendResult(.{ .nodeIds = ns.?.coll.items[params.fromIndex..params.toIndex] }, .{}); } diff --git a/src/cdp/emulation.zig b/src/cdp/emulation.zig index 3fe75fb8..88c5ddf7 100644 --- a/src/cdp/emulation.zig +++ b/src/cdp/emulation.zig @@ -17,107 +17,52 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const stringify = cdp.stringify; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); +const Runtime = @import("runtime.zig"); -const Methods = enum { - setEmulatedMedia, - setFocusEmulationEnabled, - setDeviceMetricsOverride, - setTouchEmulationEnabled, -}; +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + setEmulatedMedia, + setFocusEmulationEnabled, + setDeviceMetricsOverride, + setTouchEmulationEnabled, + }, cmd.action) orelse return error.UnknownMethod; -pub fn emulation( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - return switch (method) { - .setEmulatedMedia => setEmulatedMedia(alloc, msg, ctx), - .setFocusEmulationEnabled => setFocusEmulationEnabled(alloc, msg, ctx), - .setDeviceMetricsOverride => setDeviceMetricsOverride(alloc, msg, ctx), - .setTouchEmulationEnabled => setTouchEmulationEnabled(alloc, msg, ctx), - }; + switch (action) { + .setEmulatedMedia => return setEmulatedMedia(cmd), + .setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd), + .setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd), + .setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd), + } } -const MediaFeature = struct { - name: []const u8, - value: []const u8, -}; - // TODO: noop method -fn setEmulatedMedia( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - - // input - const Params = struct { - media: ?[]const u8 = null, - features: ?[]MediaFeature = null, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setEmulatedMedia" }); +fn setEmulatedMedia(cmd: anytype) !void { + // const input = (try const incoming.params(struct { + // media: ?[]const u8 = null, + // features: ?[]struct{ + // name: []const u8, + // value: [] const u8 + // } = null, + // })) orelse return error.InvalidParams; - // output - return result(alloc, input.id, null, null, input.sessionId); + return cmd.sendResult(null, .{}); } // TODO: noop method -fn setFocusEmulationEnabled( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const Params = struct { - enabled: bool, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setFocusEmulationEnabled" }); - - // output - return result(alloc, input.id, null, null, input.sessionId); +fn setFocusEmulationEnabled(cmd: anytype) !void { + // const input = (try const incoming.params(struct { + // enabled: bool, + // })) orelse return error.InvalidParams; + return cmd.sendResult(null, .{}); } // TODO: noop method -fn setDeviceMetricsOverride( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setDeviceMetricsOverride" }); - - // output - return result(alloc, input.id, null, null, input.sessionId); +fn setDeviceMetricsOverride(cmd: anytype) !void { + return cmd.sendResult(null, .{}); } // TODO: noop method -fn setTouchEmulationEnabled( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setTouchEmulationEnabled" }); - - return result(alloc, input.id, null, null, input.sessionId); +fn setTouchEmulationEnabled(cmd: anytype) !void { + return cmd.sendResult(null, .{}); } diff --git a/src/cdp/fetch.zig b/src/cdp/fetch.zig index d56e4067..0a9a8cae 100644 --- a/src/cdp/fetch.zig +++ b/src/cdp/fetch.zig @@ -17,43 +17,14 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Methods = enum { - disable, -}; - -pub fn fetch( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .disable => disable(alloc, msg, ctx), - }; -} -// TODO: noop method -fn disable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "fetch.disable" }); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + disable, + }, cmd.action) orelse return error.UnknownMethod; - return result(alloc, input.id, null, null, input.sessionId); + switch (action) { + .disable => return cmd.sendResult(null, .{}), + } } diff --git a/src/cdp/inspector.zig b/src/cdp/inspector.zig index 9d67fa74..21834d83 100644 --- a/src/cdp/inspector.zig +++ b/src/cdp/inspector.zig @@ -17,43 +17,14 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Methods = enum { - enable, -}; - -pub fn inspector( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .enable => enable(alloc, msg, ctx), - }; -} -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" }); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + }, cmd.action) orelse return error.UnknownMethod; - return result(alloc, input.id, null, null, input.sessionId); + switch (action) { + .enable => return cmd.sendResult(null, .{}), + } } diff --git a/src/cdp/log.zig b/src/cdp/log.zig index d54487b2..21834d83 100644 --- a/src/cdp/log.zig +++ b/src/cdp/log.zig @@ -17,43 +17,14 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; -const stringify = cdp.stringify; - -const log_cdp = std.log.scoped(.cdp); - -const Methods = enum { - enable, -}; - -pub fn log( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .enable => enable(alloc, msg, ctx), - }; -} -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log_cdp.debug("Req > id {d}, method {s}", .{ input.id, "log.enable" }); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + }, cmd.action) orelse return error.UnknownMethod; - return result(alloc, input.id, null, null, input.sessionId); + switch (action) { + .enable => return cmd.sendResult(null, .{}), + } } diff --git a/src/cdp/msg.zig b/src/cdp/msg.zig deleted file mode 100644 index fdc364c5..00000000 --- a/src/cdp/msg.zig +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); - -// Parse incoming protocol message in json format. -pub const IncomingMessage = struct { - scanner: std.json.Scanner, - json: []const u8, - - obj_begin: bool = false, - obj_end: bool = false, - - id: ?u16 = null, - scan_sessionId: bool = false, - sessionId: ?[]const u8 = null, - method: ?[]const u8 = null, - params_skip: bool = false, - - pub fn init(alloc: std.mem.Allocator, json: []const u8) IncomingMessage { - return .{ - .json = json, - .scanner = std.json.Scanner.initCompleteInput(alloc, json), - }; - } - - pub fn deinit(self: *IncomingMessage) void { - self.scanner.deinit(); - } - - fn scanUntil(self: *IncomingMessage, key: []const u8) !void { - while (true) { - switch (try self.scanner.next()) { - .end_of_document => return error.EndOfDocument, - .object_begin => { - if (self.obj_begin) return error.InvalidObjectBegin; - self.obj_begin = true; - }, - .object_end => { - if (!self.obj_begin) return error.InvalidObjectEnd; - if (self.obj_end) return error.InvalidObjectEnd; - self.obj_end = true; - }, - .string => |s| { - // is the key what we expects? - if (std.mem.eql(u8, s, key)) return; - - // save other known keys - if (std.mem.eql(u8, s, "id")) try self.scanId(); - if (std.mem.eql(u8, s, "sessionId")) try self.scanSessionId(); - if (std.mem.eql(u8, s, "method")) try self.scanMethod(); - if (std.mem.eql(u8, s, "params")) try self.scanParams(); - - // TODO should we skip unknown key? - }, - else => return error.InvalidToken, - } - } - } - - fn scanId(self: *IncomingMessage) !void { - const t = try self.scanner.next(); - if (t != .number) return error.InvalidId; - self.id = try std.fmt.parseUnsigned(u16, t.number, 10); - } - - fn getId(self: *IncomingMessage) !u16 { - if (self.id != null) return self.id.?; - - try self.scanUntil("id"); - try self.scanId(); - return self.id.?; - } - - fn scanSessionId(self: *IncomingMessage) !void { - switch (try self.scanner.next()) { - // session id can be null. - .null => return, - .string => |s| self.sessionId = s, - else => return error.InvalidSessionId, - } - - self.scan_sessionId = true; - } - - fn getSessionId(self: *IncomingMessage) !?[]const u8 { - if (self.scan_sessionId) return self.sessionId; - - self.scanUntil("sessionId") catch |err| { - if (err != error.EndOfDocument) return err; - // if the document doesn't contains any session id key, we must - // return null value. - self.scan_sessionId = true; - return null; - }; - try self.scanSessionId(); - return self.sessionId; - } - - fn scanMethod(self: *IncomingMessage) !void { - const t = try self.scanner.next(); - if (t != .string) return error.InvalidMethod; - self.method = t.string; - } - - pub fn getMethod(self: *IncomingMessage) ![]const u8 { - if (self.method != null) return self.method.?; - - try self.scanUntil("method"); - try self.scanMethod(); - return self.method.?; - } - - // scanParams skip found parameters b/c if we encounter params *before* - // asking for getParams, we don't know how to parse them. - fn scanParams(self: *IncomingMessage) !void { - const tt = try self.scanner.peekNextTokenType(); - // accept object begin or null JSON value. - if (tt != .object_begin and tt != .null) return error.InvalidParams; - try self.scanner.skipValue(); - self.params_skip = true; - } - - // getParams restart the JSON parsing - fn getParams(self: *IncomingMessage, alloc: ?std.mem.Allocator, T: type) !T { - if (T == void) return void{}; - std.debug.assert(alloc != null); // if T is not void, alloc should not be null - - if (self.params_skip) { - // TODO if the params have been skipped, we have to retart the - // parsing from start. - return error.SkippedParams; - } - - self.scanUntil("params") catch |err| { - // handle nullable type - if (@typeInfo(T) == .Optional) { - if (err == error.InvalidToken or err == error.EndOfDocument) { - return null; - } - } - return err; - }; - - // parse "params" - const options = std.json.ParseOptions{ - .ignore_unknown_fields = true, - .max_value_len = self.scanner.input.len, - .allocate = .alloc_always, - }; - return try std.json.innerParse(T, alloc.?, &self.scanner, options); - } -}; - -pub fn Input(T: type) type { - return struct { - arena: ?*std.heap.ArenaAllocator = null, - id: u16, - params: T, - sessionId: ?[]const u8, - - const Self = @This(); - - pub fn get(alloc: std.mem.Allocator, msg: *IncomingMessage) !Self { - var arena: ?*std.heap.ArenaAllocator = null; - var allocator: ?std.mem.Allocator = null; - - if (T != void) { - arena = try alloc.create(std.heap.ArenaAllocator); - arena.?.* = std.heap.ArenaAllocator.init(alloc); - allocator = arena.?.allocator(); - } - - errdefer { - if (arena) |_arena| { - _arena.deinit(); - alloc.destroy(_arena); - } - } - - return .{ - .arena = arena, - .params = try msg.getParams(allocator, T), - .id = try msg.getId(), - .sessionId = try msg.getSessionId(), - }; - } - - pub fn deinit(self: Self) void { - if (self.arena) |arena| { - const allocator = arena.child_allocator; - arena.deinit(); - allocator.destroy(arena); - } - } - }; -} - -test "read incoming message" { - const inputs = [_][]const u8{ - \\{"id":1,"method":"foo","sessionId":"bar","params":{"bar":"baz"}} - , - \\{"params":{"bar":"baz"},"id":1,"method":"foo","sessionId":"bar"} - , - \\{"sessionId":"bar","params":{"bar":"baz"},"id":1,"method":"foo"} - , - \\{"method":"foo","sessionId":"bar","params":{"bar":"baz"},"id":1} - , - }; - - for (inputs) |input| { - var msg = IncomingMessage.init(std.testing.allocator, input); - defer msg.deinit(); - - try std.testing.expectEqual(1, try msg.getId()); - try std.testing.expectEqualSlices(u8, "foo", try msg.getMethod()); - try std.testing.expectEqualSlices(u8, "bar", (try msg.getSessionId()).?); - - const T = struct { bar: []const u8 }; - const in = Input(T).get(std.testing.allocator, &msg) catch |err| { - if (err != error.SkippedParams) return err; - // TODO remove this check when params in the beginning is handled. - continue; - }; - defer in.deinit(); - try std.testing.expectEqualSlices(u8, "baz", in.params.bar); - } -} - -test "read incoming message with null session id" { - const inputs = [_][]const u8{ - \\{"id":1} - , - \\{"params":{"bar":"baz"},"id":1,"method":"foo"} - , - \\{"sessionId":null,"params":{"bar":"baz"},"id":1,"method":"foo"} - , - }; - - for (inputs) |input| { - var msg = IncomingMessage.init(std.testing.allocator, input); - defer msg.deinit(); - - try std.testing.expect(try msg.getSessionId() == null); - try std.testing.expectEqual(1, try msg.getId()); - } -} - -test "message with nullable params" { - const T = struct { - bar: []const u8, - }; - - // nullable type, params is present => value - const not_null = - \\{"id": 1,"method":"foo","params":{"bar":"baz"}} - ; - var msg = IncomingMessage.init(std.testing.allocator, not_null); - defer msg.deinit(); - const input = try Input(?T).get(std.testing.allocator, &msg); - defer input.deinit(); - try std.testing.expectEqualStrings(input.params.?.bar, "baz"); - - // nullable type, params is not present => null - const is_null = - \\{"id": 1,"method":"foo","sessionId":"AAA"} - ; - var msg_null = IncomingMessage.init(std.testing.allocator, is_null); - defer msg_null.deinit(); - const input_null = try Input(?T).get(std.testing.allocator, &msg_null); - defer input_null.deinit(); - try std.testing.expectEqual(null, input_null.params); - try std.testing.expectEqualStrings("AAA", input_null.sessionId.?); - - // not nullable type, params is not present => error - const params_or_error = msg_null.getParams(std.testing.allocator, T); - try std.testing.expectError(error.EndOfDocument, params_or_error); -} diff --git a/src/cdp/network.zig b/src/cdp/network.zig index c7c599a6..60d9cbbb 100644 --- a/src/cdp/network.zig +++ b/src/cdp/network.zig @@ -17,59 +17,16 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Methods = enum { - enable, - setCacheDisabled, -}; - -pub fn network( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .enable => enable(alloc, msg, ctx), - .setCacheDisabled => setCacheDisabled(alloc, msg, ctx), - }; -} - -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "network.enable" }); - - return result(alloc, input.id, null, null, input.sessionId); -} -// TODO: noop method -fn setCacheDisabled( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "network.setCacheDisabled" }); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + setCacheDisabled, + }, cmd.action) orelse return error.UnknownMethod; - return result(alloc, input.id, null, null, input.sessionId); + switch (action) { + .enable => return cmd.sendResult(null, .{}), + .setCacheDisabled => return cmd.sendResult(null, .{}), + } } diff --git a/src/cdp/page.zig b/src/cdp/page.zig index 8b1470b9..6ca9655d 100644 --- a/src/cdp/page.zig +++ b/src/cdp/page.zig @@ -17,58 +17,27 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const stringify = cdp.stringify; -const sendEvent = cdp.sendEvent; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Runtime = @import("runtime.zig"); - -const Methods = enum { - enable, - getFrameTree, - setLifecycleEventsEnabled, - addScriptToEvaluateOnNewDocument, - createIsolatedWorld, - navigate, -}; - -pub fn page( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - return switch (method) { - .enable => enable(alloc, msg, ctx), - .getFrameTree => getFrameTree(alloc, msg, ctx), - .setLifecycleEventsEnabled => setLifecycleEventsEnabled(alloc, msg, ctx), - .addScriptToEvaluateOnNewDocument => addScriptToEvaluateOnNewDocument(alloc, msg, ctx), - .createIsolatedWorld => createIsolatedWorld(alloc, msg, ctx), - .navigate => navigate(alloc, msg, ctx), - }; -} - -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "page.enable" }); - - return result(alloc, input.id, null, null, input.sessionId); +const runtime = @import("runtime.zig"); + +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + getFrameTree, + setLifecycleEventsEnabled, + addScriptToEvaluateOnNewDocument, + createIsolatedWorld, + navigate, + }, cmd.action) orelse return error.UnknownMethod; + + switch (action) { + .enable => return cmd.sendResult(null, .{}), + .getFrameTree => return getFrameTree(cmd), + .setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd), + .addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd), + .createIsolatedWorld => return createIsolatedWorld(cmd), + .navigate => return navigate(cmd), + } } const Frame = struct { @@ -86,16 +55,7 @@ const Frame = struct { gatedAPIFeatures: [][]const u8 = &[0][]const u8{}, }; -fn getFrameTree( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "page.getFrameTree" }); - +fn getFrameTree(cmd: anytype) !void { // output const FrameTree = struct { frameTree: struct { @@ -112,6 +72,7 @@ fn getFrameTree( try writer.writeAll("cdp.page.getFrameTree { "); try writer.writeAll(".frameTree = { "); try writer.writeAll(".frame = { "); + const frame = self.frameTree.frame; try writer.writeAll(".id = "); try std.fmt.formatText(frame.id, "s", options, writer); @@ -122,65 +83,40 @@ fn getFrameTree( try writer.writeAll(" } } }"); } }; - const frameTree = FrameTree{ + + const state = cmd.cdp; + return cmd.sendResult(FrameTree{ .frameTree = .{ .frame = .{ - .id = ctx.state.frameID, - .url = ctx.state.url, - .securityOrigin = ctx.state.securityOrigin, - .secureContextType = ctx.state.secureContextType, - .loaderId = ctx.state.loaderID, + .id = state.frame_id, + .url = state.url, + .securityOrigin = state.security_origin, + .secureContextType = state.secure_context_type, + .loaderId = state.loader_id, }, }, - }; - return result(alloc, input.id, FrameTree, frameTree, input.sessionId); + }, .{}); } -fn setLifecycleEventsEnabled( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { - enabled: bool, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "page.setLifecycleEventsEnabled" }); - - ctx.state.page_life_cycle_events = true; +fn setLifecycleEventsEnabled(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // enabled: bool, + // })) orelse return error.InvalidParams; - // output - return result(alloc, input.id, null, null, input.sessionId); + cmd.cdp.page_life_cycle_events = true; + return cmd.sendResult(null, .{}); } -const LifecycleEvent = struct { - frameId: []const u8, - loaderId: ?[]const u8, - name: []const u8 = undefined, - timestamp: f32 = undefined, -}; - // TODO: hard coded method -fn addScriptToEvaluateOnNewDocument( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const Params = struct { - source: []const u8, - worldName: ?[]const u8 = null, - includeCommandLineAPI: bool = false, - runImmediately: bool = false, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "page.addScriptToEvaluateOnNewDocument" }); - - // output - const Res = struct { +fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // source: []const u8, + // worldName: ?[]const u8 = null, + // includeCommandLineAPI: bool = false, + // runImmediately: bool = false, + // })) orelse return error.InvalidParams; + + const Response = struct { identifier: []const u8 = "1", pub fn format( @@ -195,110 +131,85 @@ fn addScriptToEvaluateOnNewDocument( try writer.writeAll(" }"); } }; - return result(alloc, input.id, Res, Res{}, input.sessionId); + return cmd.sendResult(Response{}, .{}); } // TODO: hard coded method -fn createIsolatedWorld( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn createIsolatedWorld(cmd: anytype) !void { + const session_id = cmd.session_id orelse return error.SessionIdRequired; + + const params = (try cmd.params(struct { frameId: []const u8, worldName: []const u8, grantUniveralAccess: bool, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - std.debug.assert(input.sessionId != null); - log.debug("Req > id {d}, method {s}", .{ input.id, "page.createIsolatedWorld" }); + })) orelse return error.InvalidParams; // noop executionContextCreated event - try Runtime.executionContextCreated( - alloc, - ctx, - 0, - "", - input.params.worldName, - // TODO: hard coded ID - "7102379147004877974.3265385113993241162", - .{ - .isDefault = false, - .type = "isolated", - .frameId = input.params.frameId, + try cmd.sendEvent("Runtime.executionContextCreated", .{ + .context = runtime.ExecutionContextCreated{ + .id = 0, + .origin = "", + .name = params.worldName, + // TODO: hard coded ID + .uniqueId = "7102379147004877974.3265385113993241162", + .auxData = .{ + .isDefault = false, + .type = "isolated", + .frameId = params.frameId, + }, }, - input.sessionId, - ); - - // output - const Resp = struct { - executionContextId: u8 = 0, - }; + }, .{ .session_id = session_id }); - return result(alloc, input.id, Resp, .{}, input.sessionId); + return cmd.sendResult(.{ + .executionContextId = 0, + }, .{}); } -fn navigate( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn navigate(cmd: anytype) !void { + const session_id = cmd.session_id orelse return error.SessionIdRequired; + + const params = (try cmd.params(struct { url: []const u8, referrer: ?[]const u8 = null, transitionType: ?[]const u8 = null, // TODO: enum frameId: ?[]const u8 = null, referrerPolicy: ?[]const u8 = null, // TODO: enum - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - std.debug.assert(input.sessionId != null); - log.debug("Req > id {d}, method {s}", .{ input.id, "page.navigate" }); + })) orelse return error.InvalidParams; // change state - ctx.state.reset(); - ctx.state.url = input.params.url; + var state = cmd.cdp; + state.reset(); + state.url = params.url; + // TODO: hard coded ID - ctx.state.loaderID = "AF8667A203C5392DBE9AC290044AA4C2"; + state.loader_id = "AF8667A203C5392DBE9AC290044AA4C2"; + + const LifecycleEvent = struct { + frameId: []const u8, + loaderId: ?[]const u8, + name: []const u8, + timestamp: f32, + }; var life_event = LifecycleEvent{ - .frameId = ctx.state.frameID, - .loaderId = ctx.state.loaderID, + .frameId = state.frame_id, + .loaderId = state.loader_id, + .name = "init", + .timestamp = 343721.796037, }; - var ts_event: cdp.TimestampEvent = undefined; // frameStartedLoading event // TODO: event partially hard coded - const FrameStartedLoading = struct { - frameId: []const u8, - }; - const frame_started_loading = FrameStartedLoading{ .frameId = ctx.state.frameID }; - try sendEvent( - alloc, - ctx, - "Page.frameStartedLoading", - FrameStartedLoading, - frame_started_loading, - input.sessionId, - ); - if (ctx.state.page_life_cycle_events) { - life_event.name = "init"; - life_event.timestamp = 343721.796037; - try sendEvent( - alloc, - ctx, - "Page.lifecycleEvent", - LifecycleEvent, - life_event, - input.sessionId, - ); + try cmd.sendEvent("Page.frameStartedLoading", .{ + .frameId = state.frame_id, + }, .{ .session_id = session_id }); + + if (state.page_life_cycle_events) { + try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); } // output - const Resp = struct { + const Response = struct { frameId: []const u8, loaderId: ?[]const u8, errorText: ?[]const u8 = null, @@ -319,146 +230,89 @@ fn navigate( try writer.writeAll(" }"); } }; - const resp = Resp{ - .frameId = ctx.state.frameID, - .loaderId = ctx.state.loaderID, - }; - const res = try result(alloc, input.id, Resp, resp, input.sessionId); - try ctx.send(res); + + try cmd.sendResult(Response{ + .frameId = state.frame_id, + .loaderId = state.loader_id, + }, .{}); // TODO: at this point do we need async the following actions to be async? // Send Runtime.executionContextsCleared event // TODO: noop event, we have no env context at this point, is it necesarry? - try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", struct {}, .{}, input.sessionId); + try cmd.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id }); // Launch navigate, the page must have been created by a // target.createTarget. - var p = ctx.browser.currentPage() orelse return error.NoPage; - ctx.state.executionContextId += 1; - const auxData = try std.fmt.allocPrint( - alloc, + var p = cmd.session.currentPage() orelse return error.NoPage; + state.execution_context_id += 1; + + const aux_data = try std.fmt.allocPrint( + cmd.arena, // NOTE: we assume this is the default web page "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", - .{ctx.state.frameID}, + .{state.frame_id}, ); - defer alloc.free(auxData); - try p.navigate(input.params.url, auxData); + try p.navigate(params.url, aux_data); // Events // lifecycle init event // TODO: partially hard coded - if (ctx.state.page_life_cycle_events) { + if (state.page_life_cycle_events) { life_event.name = "init"; life_event.timestamp = 343721.796037; - try sendEvent( - alloc, - ctx, - "Page.lifecycleEvent", - LifecycleEvent, - life_event, - input.sessionId, - ); + try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); } - // DOM.documentUpdated - try sendEvent( - alloc, - ctx, - "DOM.documentUpdated", - struct {}, - .{}, - input.sessionId, - ); + try cmd.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id }); // frameNavigated event - const FrameNavigated = struct { - frame: Frame, - type: []const u8 = "Navigation", - }; - const frame_navigated = FrameNavigated{ - .frame = .{ - .id = ctx.state.frameID, - .url = ctx.state.url, - .securityOrigin = ctx.state.securityOrigin, - .secureContextType = ctx.state.secureContextType, - .loaderId = ctx.state.loaderID, + try cmd.sendEvent("Page.frameNavigated", .{ + .type = "Navigation", + .frame = Frame{ + .id = state.frame_id, + .url = state.url, + .securityOrigin = state.security_origin, + .secureContextType = state.secure_context_type, + .loaderId = state.loader_id, }, - }; - try sendEvent( - alloc, - ctx, - "Page.frameNavigated", - FrameNavigated, - frame_navigated, - input.sessionId, - ); + }, .{ .session_id = session_id }); // domContentEventFired event // TODO: partially hard coded - ts_event = .{ .timestamp = 343721.803338 }; - try sendEvent( - alloc, - ctx, + try cmd.sendEvent( "Page.domContentEventFired", - cdp.TimestampEvent, - ts_event, - input.sessionId, + cdp.TimestampEvent{ .timestamp = 343721.803338 }, + .{ .session_id = session_id }, ); // lifecycle DOMContentLoaded event // TODO: partially hard coded - if (ctx.state.page_life_cycle_events) { + if (state.page_life_cycle_events) { life_event.name = "DOMContentLoaded"; life_event.timestamp = 343721.803338; - try sendEvent( - alloc, - ctx, - "Page.lifecycleEvent", - LifecycleEvent, - life_event, - input.sessionId, - ); + try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); } // loadEventFired event // TODO: partially hard coded - ts_event = .{ .timestamp = 343721.824655 }; - try sendEvent( - alloc, - ctx, + try cmd.sendEvent( "Page.loadEventFired", - cdp.TimestampEvent, - ts_event, - input.sessionId, + cdp.TimestampEvent{ .timestamp = 343721.824655 }, + .{ .session_id = session_id }, ); // lifecycle DOMContentLoaded event // TODO: partially hard coded - if (ctx.state.page_life_cycle_events) { + if (state.page_life_cycle_events) { life_event.name = "load"; life_event.timestamp = 343721.824655; - try sendEvent( - alloc, - ctx, - "Page.lifecycleEvent", - LifecycleEvent, - life_event, - input.sessionId, - ); + try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); } // frameStoppedLoading - const FrameStoppedLoading = struct { frameId: []const u8 }; - try sendEvent( - alloc, - ctx, - "Page.frameStoppedLoading", - FrameStoppedLoading, - .{ .frameId = ctx.state.frameID }, - input.sessionId, - ); - - return ""; + return cmd.sendEvent("Page.frameStoppedLoading", .{ + .frameId = state.frame_id, + }, .{ .session_id = session_id }); } diff --git a/src/cdp/performance.zig b/src/cdp/performance.zig index bf210c36..8db70ed4 100644 --- a/src/cdp/performance.zig +++ b/src/cdp/performance.zig @@ -17,43 +17,15 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Methods = enum { - enable, -}; - -pub fn performance( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .enable => enable(alloc, msg, ctx), - }; -} +const asUint = @import("../str/parser.zig").asUint; -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "performance.enable" }); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + }, cmd.action) orelse return error.UnknownMethod; - return result(alloc, input.id, null, null, input.sessionId); + switch (action) { + .enable => return cmd.sendResult(null, .{}), + } } diff --git a/src/cdp/runtime.zig b/src/cdp/runtime.zig index 054d5a78..3da66105 100644 --- a/src/cdp/runtime.zig +++ b/src/cdp/runtime.zig @@ -17,179 +17,106 @@ // along with this program. If not, see . const std = @import("std"); -const builtin = @import("builtin"); - -const jsruntime = @import("jsruntime"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; -const stringify = cdp.stringify; -const target = @import("target.zig"); - -const log = std.log.scoped(.cdp); - -const Methods = enum { - enable, - runIfWaitingForDebugger, - evaluate, - addBinding, - callFunctionOn, - releaseObject, -}; -pub fn runtime( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - // NOTE: we could send it anyway to the JS runtime but it's good to check it - return error.UnknownMethod; - return switch (method) { - .runIfWaitingForDebugger => runIfWaitingForDebugger(alloc, msg, ctx), - else => sendInspector(alloc, method, msg, ctx), - }; +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + runIfWaitingForDebugger, + evaluate, + addBinding, + callFunctionOn, + releaseObject, + }, cmd.action) orelse return error.UnknownMethod; + + switch (action) { + .runIfWaitingForDebugger => return cmd.sendResult(null, .{}), + else => return sendInspector(cmd, action), + } } -fn sendInspector( - alloc: std.mem.Allocator, - method: Methods, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - +fn sendInspector(cmd: anytype, action: anytype) !void { // save script in file at debug mode if (std.log.defaultLogEnabled(.debug)) { - - // input - var id: u16 = undefined; - var script: ?[]const u8 = null; - - if (method == .evaluate) { - const Params = struct { - expression: []const u8, - contextId: ?u8 = null, - returnByValue: ?bool = null, - awaitPromise: ?bool = null, - userGesture: ?bool = null, - }; - - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.evaluate" }); - const params = input.params; - const func = try alloc.alloc(u8, params.expression.len); - @memcpy(func, params.expression); - script = func; - id = input.id; - } else if (method == .callFunctionOn) { - const Params = struct { - functionDeclaration: []const u8, - objectId: ?[]const u8 = null, - executionContextId: ?u8 = null, - arguments: ?[]struct { - value: ?[]const u8 = null, - objectId: ?[]const u8 = null, - } = null, - returnByValue: ?bool = null, - awaitPromise: ?bool = null, - userGesture: ?bool = null, - }; - - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.callFunctionOn" }); - const params = input.params; - const func = try alloc.alloc(u8, params.functionDeclaration.len); - @memcpy(func, params.functionDeclaration); - script = func; - id = input.id; - } - - if (script) |src| { - try cdp.dumpFile(alloc, id, src); - alloc.free(src); - } + try logInspector(cmd, action); } - if (msg.sessionId) |s| { - ctx.state.sessionID = cdp.SessionID.parse(s) catch |err| { - log.err("parse sessionID: {s} {any}", .{ s, err }); - return err; - }; + if (cmd.session_id) |s| { + cmd.cdp.session_id = try cdp.SessionID.parse(s); } // remove awaitPromise true params // TODO: delete when Promise are correctly handled by zig-js-runtime - if (method == .callFunctionOn or method == .evaluate) { - if (std.mem.indexOf(u8, msg.json, "\"awaitPromise\":true")) |_| { - const buf = try alloc.alloc(u8, msg.json.len + 1); - defer alloc.free(buf); - _ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf); - try ctx.sendInspector(buf); - return ""; + if (action == .callFunctionOn or action == .evaluate) { + const json = cmd.json; + if (std.mem.indexOf(u8, json, "\"awaitPromise\":true")) |_| { + // +1 because we'll be turning a true -> false + const buf = try cmd.arena.alloc(u8, json.len + 1); + _ = std.mem.replace(u8, json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf); + cmd.session.callInspector(buf); + return; } } - try ctx.sendInspector(msg.json); + cmd.session.callInspector(cmd.json); - if (msg.id == null) return ""; - - return result(alloc, msg.id.?, null, null, msg.sessionId); + if (cmd.id != null) { + return cmd.sendResult(null, .{}); + } } -pub const AuxData = struct { - isDefault: bool = true, - type: []const u8 = "default", - frameId: []const u8 = cdp.FrameID, -}; - -pub fn executionContextCreated( - alloc: std.mem.Allocator, - ctx: *Ctx, - id: u16, +pub const ExecutionContextCreated = struct { + id: u64, origin: []const u8, name: []const u8, - uniqueID: []const u8, - auxData: ?AuxData, - sessionID: ?[]const u8, -) !void { - const Params = struct { - context: struct { - id: u64, - origin: []const u8, - name: []const u8, - uniqueId: []const u8, - auxData: ?AuxData = null, - }, + uniqueId: []const u8, + auxData: ?AuxData = null, + + pub const AuxData = struct { + isDefault: bool = true, + type: []const u8 = "default", + frameId: []const u8 = cdp.FRAME_ID, }; - const params = Params{ - .context = .{ - .id = id, - .origin = origin, - .name = name, - .uniqueId = uniqueID, - .auxData = auxData, +}; + +fn logInspector(cmd: anytype, action: anytype) !void { + const script = switch (action) { + .evaluate => blk: { + const params = (try cmd.params(struct { + expression: []const u8, + // contextId: ?u8 = null, + // returnByValue: ?bool = null, + // awaitPromise: ?bool = null, + // userGesture: ?bool = null, + })) orelse return error.InvalidParams; + + break :blk params.expression; }, + .callFunctionOn => blk: { + const params = (try cmd.params(struct { + functionDeclaration: []const u8, + // objectId: ?[]const u8 = null, + // executionContextId: ?u8 = null, + // arguments: ?[]struct { + // value: ?[]const u8 = null, + // objectId: ?[]const u8 = null, + // } = null, + // returnByValue: ?bool = null, + // awaitPromise: ?bool = null, + // userGesture: ?bool = null, + })) orelse return error.InvalidParams; + + break :blk params.functionDeclaration; + }, + else => return, }; - try cdp.sendEvent(alloc, ctx, "Runtime.executionContextCreated", Params, params, sessionID); -} + const id = cmd.id orelse return error.RequiredId; + const name = try std.fmt.allocPrint(cmd.arena, "id_{d}.js", .{id}); -// TODO: noop method -// should we be passing this also to the JS Inspector? -fn runIfWaitingForDebugger( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "runtime.runIfWaitingForDebugger" }); + var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{}); + defer dir.close(); - return result(alloc, input.id, null, null, input.sessionId); + const f = try dir.createFile(name, .{}); + defer f.close(); + try f.writeAll(script); } diff --git a/src/cdp/security.zig b/src/cdp/security.zig index 42912544..21834d83 100644 --- a/src/cdp/security.zig +++ b/src/cdp/security.zig @@ -17,43 +17,14 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; - -const log = std.log.scoped(.cdp); - -const Methods = enum { - enable, -}; - -pub fn security( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - - return switch (method) { - .enable => enable(alloc, msg, ctx), - }; -} -fn enable( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "security.enable" }); +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + enable, + }, cmd.action) orelse return error.UnknownMethod; - return result(alloc, input.id, null, null, input.sessionId); + switch (action) { + .enable => return cmd.sendResult(null, .{}), + } } diff --git a/src/cdp/target.zig b/src/cdp/target.zig index ca9ae90f..815741d9 100644 --- a/src/cdp/target.zig +++ b/src/cdp/target.zig @@ -17,267 +17,174 @@ // along with this program. If not, see . const std = @import("std"); - -const server = @import("../server.zig"); -const Ctx = server.Ctx; const cdp = @import("cdp.zig"); -const result = cdp.result; -const stringify = cdp.stringify; -const IncomingMessage = @import("msg.zig").IncomingMessage; -const Input = @import("msg.zig").Input; const log = std.log.scoped(.cdp); -const Methods = enum { - setDiscoverTargets, - setAutoAttach, - attachToTarget, - getTargetInfo, - getBrowserContexts, - createBrowserContext, - disposeBrowserContext, - createTarget, - closeTarget, - sendMessageToTarget, - detachFromTarget, -}; - -pub fn target( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - action: []const u8, - ctx: *Ctx, -) ![]const u8 { - const method = std.meta.stringToEnum(Methods, action) orelse - return error.UnknownMethod; - return switch (method) { - .setDiscoverTargets => setDiscoverTargets(alloc, msg, ctx), - .setAutoAttach => setAutoAttach(alloc, msg, ctx), - .attachToTarget => attachToTarget(alloc, msg, ctx), - .getTargetInfo => getTargetInfo(alloc, msg, ctx), - .getBrowserContexts => getBrowserContexts(alloc, msg, ctx), - .createBrowserContext => createBrowserContext(alloc, msg, ctx), - .disposeBrowserContext => disposeBrowserContext(alloc, msg, ctx), - .createTarget => createTarget(alloc, msg, ctx), - .closeTarget => closeTarget(alloc, msg, ctx), - .sendMessageToTarget => sendMessageToTarget(alloc, msg, ctx), - .detachFromTarget => detachFromTarget(alloc, msg, ctx), - }; -} - // TODO: hard coded IDs -pub const PageTargetID = "PAGETARGETIDB638E9DC0F52DDC"; -pub const BrowserTargetID = "browser9-targ-et6f-id0e-83f3ab73a30c"; -pub const BrowserContextID = "BROWSERCONTEXTIDA95049E9DFE95EA9"; - +const CONTEXT_ID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89"; +const PAGE_TARGET_ID = "PAGETARGETIDB638E9DC0F52DDC"; +const BROWSER_TARGET_ID = "browser9-targ-et6f-id0e-83f3ab73a30c"; +const BROWER_CONTEXT_ID = "BROWSERCONTEXTIDA95049E9DFE95EA9"; +const TARGET_ID = "TARGETID460A8F29706A2ADF14316298"; +const LOADER_ID = "LOADERID42AA389647D702B4D805F49A"; + +pub fn processMessage(cmd: anytype) !void { + const action = std.meta.stringToEnum(enum { + setDiscoverTargets, + setAutoAttach, + attachToTarget, + getTargetInfo, + getBrowserContexts, + createBrowserContext, + disposeBrowserContext, + createTarget, + closeTarget, + sendMessageToTarget, + detachFromTarget, + }, cmd.action) orelse return error.UnknownMethod; + + switch (action) { + .setDiscoverTargets => return setDiscoverTargets(cmd), + .setAutoAttach => return setAutoAttach(cmd), + .attachToTarget => return attachToTarget(cmd), + .getTargetInfo => return getTargetInfo(cmd), + .getBrowserContexts => return getBrowserContexts(cmd), + .createBrowserContext => return createBrowserContext(cmd), + .disposeBrowserContext => return disposeBrowserContext(cmd), + .createTarget => return createTarget(cmd), + .closeTarget => return closeTarget(cmd), + .sendMessageToTarget => return sendMessageToTarget(cmd), + .detachFromTarget => return detachFromTarget(cmd), + } +} // TODO: noop method -fn setDiscoverTargets( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.setDiscoverTargets" }); - - // output - return result(alloc, input.id, null, null, input.sessionId); +fn setDiscoverTargets(cmd: anytype) !void { + return cmd.sendResult(null, .{}); } const AttachToTarget = struct { sessionId: []const u8, - targetInfo: struct { - targetId: []const u8, - type: []const u8 = "page", - title: []const u8, - url: []const u8, - attached: bool = true, - canAccessOpener: bool = false, - browserContextId: []const u8, - }, + targetInfo: TargetInfo, waitingForDebugger: bool = false, }; const TargetCreated = struct { sessionId: []const u8, - targetInfo: struct { - targetId: []const u8, - type: []const u8 = "page", - title: []const u8, - url: []const u8, - attached: bool = true, - canAccessOpener: bool = false, - browserContextId: []const u8, - }, + targetInfo: TargetInfo, }; -const TargetFilter = struct { - type: ?[]const u8 = null, - exclude: ?bool = null, +const TargetInfo = struct { + targetId: []const u8, + type: []const u8 = "page", + title: []const u8, + url: []const u8, + attached: bool = true, + canAccessOpener: bool = false, + browserContextId: []const u8, }; // TODO: noop method -fn setAutoAttach( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { - autoAttach: bool, - waitForDebuggerOnStart: bool, - flatten: bool = true, - filter: ?[]TargetFilter = null, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.setAutoAttach" }); +fn setAutoAttach(cmd: anytype) !void { + // const TargetFilter = struct { + // type: ?[]const u8 = null, + // exclude: ?bool = null, + // }; + + // const params = (try cmd.params(struct { + // autoAttach: bool, + // waitForDebuggerOnStart: bool, + // flatten: bool = true, + // filter: ?[]TargetFilter = null, + // })) orelse return error.InvalidParams; // attachedToTarget event - if (input.sessionId == null) { - const attached = AttachToTarget{ - .sessionId = cdp.BrowserSessionID, + if (cmd.session_id == null) { + try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ + .sessionId = cdp.BROWSER_SESSION_ID, .targetInfo = .{ - .targetId = PageTargetID, + .targetId = PAGE_TARGET_ID, .title = "about:blank", - .url = cdp.URLBase, - .browserContextId = BrowserContextID, + .url = cdp.URL_BASE, + .browserContextId = BROWER_CONTEXT_ID, }, - }; - try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null); + }, .{}); } - // output - return result(alloc, input.id, null, null, input.sessionId); + return cmd.sendResult(null, .{}); } // TODO: noop method -fn attachToTarget( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - - // input - const Params = struct { +fn attachToTarget(cmd: anytype) !void { + const params = (try cmd.params(struct { targetId: []const u8, flatten: bool = true, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.attachToTarget" }); + })) orelse return error.InvalidParams; // attachedToTarget event - if (input.sessionId == null) { - const attached = AttachToTarget{ - .sessionId = cdp.BrowserSessionID, + if (cmd.session_id == null) { + try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ + .sessionId = cdp.BROWSER_SESSION_ID, .targetInfo = .{ - .targetId = input.params.targetId, + .targetId = params.targetId, .title = "about:blank", - .url = cdp.URLBase, - .browserContextId = BrowserContextID, + .url = cdp.URL_BASE, + .browserContextId = BROWER_CONTEXT_ID, }, - }; - try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null); + }, .{}); } - // output - const SessionId = struct { - sessionId: []const u8, - }; - const output = SessionId{ - .sessionId = input.sessionId orelse cdp.BrowserSessionID, - }; - return result(alloc, input.id, SessionId, output, null); + return cmd.sendResult( + .{ .sessionId = cmd.session_id orelse cdp.BROWSER_SESSION_ID }, + .{ .include_session_id = false }, + ); } -fn getTargetInfo( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const Params = struct { - targetId: ?[]const u8 = null, - }; - const input = try Input(?Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" }); +fn getTargetInfo(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // targetId: ?[]const u8 = null, + // })) orelse return error.InvalidParams; - // output - const TargetInfo = struct { - targetId: []const u8, - type: []const u8, - title: []const u8 = "", - url: []const u8 = "", - attached: bool = true, - openerId: ?[]const u8 = null, - canAccessOpener: bool = false, - openerFrameId: ?[]const u8 = null, - browserContextId: ?[]const u8 = null, - subtype: ?[]const u8 = null, - }; - const targetInfo = TargetInfo{ - .targetId = BrowserTargetID, + return cmd.sendResult(.{ + .targetId = BROWSER_TARGET_ID, .type = "browser", - }; - return result(alloc, input.id, TargetInfo, targetInfo, null); + .title = "", + .url = "", + .attached = true, + .canAccessOpener = false, + }, .{ .include_session_id = false }); } // Browser context are not handled and not in the roadmap for now // The following methods are "fake" // TODO: noop method -fn getBrowserContexts( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.getBrowserContexts" }); - - // ouptut - const Resp = struct { - browserContextIds: [][]const u8, - }; - var resp: Resp = undefined; - if (ctx.state.contextID) |contextID| { - var contextIDs = [1][]const u8{contextID}; - resp = .{ .browserContextIds = &contextIDs }; +fn getBrowserContexts(cmd: anytype) !void { + var context_ids: []const []const u8 = undefined; + if (cmd.cdp.context_id) |context_id| { + context_ids = &.{context_id}; } else { - const contextIDs = [0][]const u8{}; - resp = .{ .browserContextIds = &contextIDs }; + context_ids = &.{}; } - return result(alloc, input.id, Resp, resp, null); -} -const ContextID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89"; + return cmd.sendResult(.{ + .browserContextIds = context_ids, + }, .{ .include_session_id = false }); +} // TODO: noop method -fn createBrowserContext( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { - disposeOnDetach: bool = false, - proxyServer: ?[]const u8 = null, - proxyBypassList: ?[]const u8 = null, - originsWithUniversalNetworkAccess: ?[][]const u8 = null, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.createBrowserContext" }); +fn createBrowserContext(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // disposeOnDetach: bool = false, + // proxyServer: ?[]const u8 = null, + // proxyBypassList: ?[]const u8 = null, + // originsWithUniversalNetworkAccess: ?[][]const u8 = null, + // })) orelse return error.InvalidParams; - ctx.state.contextID = ContextID; + cmd.cdp.context_id = CONTEXT_ID; - // output - const Resp = struct { - browserContextId: []const u8 = ContextID, + const Response = struct { + browserContextId: []const u8, pub fn format( self: @This(), @@ -291,40 +198,26 @@ fn createBrowserContext( try writer.writeAll(" }"); } }; - return result(alloc, input.id, Resp, Resp{}, input.sessionId); -} -fn disposeBrowserContext( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { - browserContextId: []const u8, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.disposeBrowserContext" }); + return cmd.sendResult(Response{ + .browserContextId = CONTEXT_ID, + }, .{}); +} - // output - const res = try result(alloc, input.id, null, .{}, null); - try ctx.send(res); +fn disposeBrowserContext(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // browserContextId: []const u8, + // proxyServer: ?[]const u8 = null, + // proxyBypassList: ?[]const u8 = null, + // originsWithUniversalNetworkAccess: ?[][]const u8 = null, + // })) orelse return error.InvalidParams; - return error.DisposeBrowserContext; + try cmd.cdp.newSession(); + try cmd.sendResult(null, .{}); } -// TODO: hard coded IDs -const TargetID = "TARGETID460A8F29706A2ADF14316298"; -const LoaderID = "LOADERID42AA389647D702B4D805F49A"; - -fn createTarget( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn createTarget(cmd: anytype) !void { + const params = (try cmd.params(struct { url: []const u8, width: ?u64 = null, height: ?u64 = null, @@ -333,71 +226,67 @@ fn createTarget( newWindow: bool = false, background: bool = false, forTab: ?bool = null, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.createTarget" }); + })) orelse return error.InvalidParams; // change CDP state - ctx.state.frameID = TargetID; - ctx.state.url = "about:blank"; - ctx.state.securityOrigin = "://"; - ctx.state.secureContextType = "InsecureScheme"; - ctx.state.loaderID = LoaderID; - - if (msg.sessionId) |s| { - ctx.state.sessionID = cdp.SessionID.parse(s) catch |err| { - log.err("parse sessionID: {s} {any}", .{ s, err }); - return err; - }; + var state = cmd.cdp; + state.frame_id = TARGET_ID; + state.url = "about:blank"; + state.security_origin = "://"; + state.secure_context_type = "InsecureScheme"; + state.loader_id = LOADER_ID; + + if (cmd.session_id) |s| { + state.session_id = try cdp.SessionID.parse(s); } // TODO stop the previous page instead? - if (ctx.browser.currentPage() != null) return error.pageAlreadyExists; + if (cmd.session.page != null) { + return error.pageAlreadyExists; + } // create the page - const p = try ctx.browser.session.createPage(); - ctx.state.executionContextId += 1; + const p = try cmd.session.createPage(); + state.execution_context_id += 1; + // start the js env - const auxData = try std.fmt.allocPrint( - alloc, + const aux_data = try std.fmt.allocPrint( + cmd.arena, // NOTE: we assume this is the default web page "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", - .{ctx.state.frameID}, + .{state.frame_id}, ); - defer alloc.free(auxData); - try p.start(auxData); + try p.start(aux_data); + + const browser_context_id = params.browserContextId orelse CONTEXT_ID; // send targetCreated event - const created = TargetCreated{ - .sessionId = cdp.ContextSessionID, + try cmd.sendEvent("Target.targetCreated", TargetCreated{ + .sessionId = cdp.CONTEXT_SESSION_ID, .targetInfo = .{ - .targetId = ctx.state.frameID, + .targetId = state.frame_id, .title = "about:blank", - .url = ctx.state.url, - .browserContextId = input.params.browserContextId orelse ContextID, + .url = state.url, + .browserContextId = browser_context_id, .attached = true, }, - }; - try cdp.sendEvent(alloc, ctx, "Target.targetCreated", TargetCreated, created, input.sessionId); + }, .{ .session_id = cmd.session_id }); // send attachToTarget event - const attached = AttachToTarget{ - .sessionId = cdp.ContextSessionID, + try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ + .sessionId = cdp.CONTEXT_SESSION_ID, + .waitingForDebugger = true, .targetInfo = .{ - .targetId = ctx.state.frameID, + .targetId = state.frame_id, .title = "about:blank", - .url = ctx.state.url, - .browserContextId = input.params.browserContextId orelse ContextID, + .url = state.url, + .browserContextId = browser_context_id, .attached = true, }, - .waitingForDebugger = true, - }; - try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, input.sessionId); + }, .{ .session_id = cmd.session_id }); - // output - const Resp = struct { - targetId: []const u8 = TargetID, + const Response = struct { + targetId: []const u8 = TARGET_ID, pub fn format( self: @This(), @@ -411,119 +300,71 @@ fn createTarget( try writer.writeAll(" }"); } }; - return result(alloc, input.id, Resp, Resp{}, input.sessionId); + return cmd.sendResult(Response{}, .{}); } -fn closeTarget( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn closeTarget(cmd: anytype) !void { + const params = (try cmd.params(struct { targetId: []const u8, - }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.closeTarget" }); + })) orelse return error.InvalidParams; - // output - const Resp = struct { - success: bool = true, - }; - const res = try result(alloc, input.id, Resp, Resp{}, null); - try ctx.send(res); + try cmd.sendResult(.{ + .success = true, + }, .{ .include_session_id = false }); + + const session_id = cmd.session_id orelse cdp.CONTEXT_SESSION_ID; // Inspector.detached event - const InspectorDetached = struct { - reason: []const u8 = "Render process gone.", - }; - try cdp.sendEvent( - alloc, - ctx, - "Inspector.detached", - InspectorDetached, - .{}, - input.sessionId orelse cdp.ContextSessionID, - ); + try cmd.sendEvent("Inspector.detached", .{ + .reason = "Render process gone.", + }, .{ .session_id = session_id }); // detachedFromTarget event - const TargetDetached = struct { - sessionId: []const u8, - targetId: []const u8, - }; - try cdp.sendEvent( - alloc, - ctx, - "Target.detachedFromTarget", - TargetDetached, - .{ - .sessionId = input.sessionId orelse cdp.ContextSessionID, - .targetId = input.params.targetId, - }, - null, - ); - - if (ctx.browser.currentPage()) |page| page.end(); - - return ""; + try cmd.sendEvent("Target.detachedFromTarget", .{ + .sessionId = session_id, + .targetId = params.targetId, + .reason = "Render process gone.", + }, .{}); + + if (cmd.session.page) |*page| { + page.end(); + } } -fn sendMessageToTarget( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - ctx: *Ctx, -) ![]const u8 { - // input - const Params = struct { +fn sendMessageToTarget(cmd: anytype) !void { + const params = (try cmd.params(struct { message: []const u8, sessionId: []const u8, + })) orelse return error.InvalidParams; + + const Capture = struct { + allocator: std.mem.Allocator, + buf: std.ArrayListUnmanaged(u8), + + pub fn sendJSON(self: *@This(), message: anytype) !void { + return std.json.stringify(message, .{ + .emit_null_optional_fields = false, + }, self.buf.writer(self.allocator)); + } }; - const input = try Input(Params).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s} ({s})", .{ input.id, "target.sendMessageToTarget", input.params.message }); - - // get the wrapped message. - var wmsg = IncomingMessage.init(alloc, input.params.message); - defer wmsg.deinit(); - - const res = cdp.dispatch(alloc, &wmsg, ctx) catch |e| { - log.err("send message {d} ({s}): {any}", .{ input.id, input.params.message, e }); - // TODO dispatch error correctly. - return e; + + var capture = Capture{ + .buf = .{}, + .allocator = cmd.arena, }; - // receivedMessageFromTarget event - const ReceivedMessageFromTarget = struct { - message: []const u8, - sessionId: []const u8, + cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| { + log.err("send message {d} ({s}): {any}", .{ cmd.id orelse -1, params.message, err }); + return err; }; - try cdp.sendEvent( - alloc, - ctx, - "Target.receivedMessageFromTarget", - ReceivedMessageFromTarget, - .{ - .message = res, - .sessionId = input.params.sessionId, - }, - null, - ); - return ""; + try cmd.sendEvent("Target.receivedMessageFromTarget", .{ + .message = capture.buf.items, + .sessionId = params.sessionId, + }, .{}); } // noop -fn detachFromTarget( - alloc: std.mem.Allocator, - msg: *IncomingMessage, - _: *Ctx, -) ![]const u8 { - // input - const input = try Input(void).get(alloc, msg); - defer input.deinit(); - log.debug("Req > id {d}, method {s}", .{ input.id, "target.detachFromTarget" }); - - // output - return result(alloc, input.id, bool, true, input.sessionId); +fn detachFromTarget(cmd: anytype) !void { + return cmd.sendResult(null, .{}); } diff --git a/src/main.zig b/src/main.zig index 31743c2a..ede338ca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -251,12 +251,13 @@ pub fn main() !void { defer loop.deinit(); // browser - var browser = Browser{}; - try Browser.init(&browser, alloc, &loop, vm); + var browser = Browser.init(alloc, &loop); defer browser.deinit(); + var session = try browser.newSession({}); + // page - const page = try browser.session.createPage(); + const page = try session.createPage(); try page.start(null); defer page.end(); diff --git a/src/main_tests.zig b/src/main_tests.zig index 53967905..e82d3f6d 100644 --- a/src/main_tests.zig +++ b/src/main_tests.zig @@ -336,7 +336,6 @@ test { std.testing.refAllDecls(queryTest); std.testing.refAllDecls(@import("generate.zig")); - std.testing.refAllDecls(@import("cdp/msg.zig")); } fn testJSRuntime(alloc: std.mem.Allocator) !void { diff --git a/src/server.zig b/src/server.zig index eeeb1934..f6af58fb 100644 --- a/src/server.zig +++ b/src/server.zig @@ -23,6 +23,7 @@ const net = std.net; const posix = std.posix; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const jsruntime = @import("jsruntime"); const Completion = jsruntime.IO.Completion; @@ -33,31 +34,7 @@ const CloseError = jsruntime.IO.CloseError; const CancelError = jsruntime.IO.CancelOneError; const TimeoutError = jsruntime.IO.TimeoutError; -const Browser = @import("browser/browser.zig").Browser; -const cdp = @import("cdp/cdp.zig"); - -const IOError = AcceptError || RecvError || SendError || CloseError || TimeoutError || CancelError; -const HTTPError = error{ - OutOfMemory, - RequestTooLarge, - NotFound, - InvalidRequest, - MissingHeaders, - InvalidProtocol, - InvalidUpgradeHeader, - InvalidVersionHeader, - InvalidConnectionHeader, -}; -const WebSocketError = error{ - OutOfMemory, - ReservedFlags, - NotMasked, - TooLarge, - InvalidMessageType, - InvalidContinuation, - NestedFragementation, -}; -const Error = IOError || cdp.Error || HTTPError || WebSocketError; +const CDP = @import("cdp/cdp.zig").CDP; const TimeoutCheck = std.time.ns_per_ms * 100; @@ -70,10 +47,7 @@ const MAX_HTTP_REQUEST_SIZE = 2048; // +140 for the max control packet that might be interleaved in a message const MAX_MESSAGE_SIZE = 256 * 1024 + 14; -// For now, cdp does @import("server.zig").Ctx. Could change cdp to use "Server" -// but I rather try to decouple the CDP code from the server, so a quick -// stopgap is fine. TODO: Decouple cdp from the server -pub const Ctx = Server; +pub const Client = ClientT(*Server, CDP); const Server = struct { allocator: Allocator, @@ -81,33 +55,27 @@ const Server = struct { // internal fields listener: posix.socket_t, - client: ?Client(*Server) = null, + client: ?*Client = null, timeout: u64, // a memory poor for our Send objects send_pool: std.heap.MemoryPool(Send), + // a memory poor for our Clietns + client_pool: std.heap.MemoryPool(Client), + // I/O fields conn_completion: Completion, close_completion: Completion, accept_completion: Completion, timeout_completion: Completion, - // used when gluing the session id to the inspector message - scrap: std.ArrayListUnmanaged(u8) = .{}, - // The response to send on a GET /json/version request json_version_response: []const u8, - // CDP - state: cdp.State = undefined, - - // JS fields - browser: *Browser, // TODO: is pointer mandatory here? - - pub fn deinit(self: *Ctx) void { - self.state.deinit(); + fn deinit(self: *Server) void { self.send_pool.deinit(); + self.client_pool.deinit(); self.allocator.free(self.json_version_response); } @@ -127,6 +95,7 @@ const Server = struct { completion: *Completion, result: AcceptError!posix.socket_t, ) void { + std.debug.assert(self.client == null); std.debug.assert(completion == &self.accept_completion); const socket = result catch |err| { @@ -135,14 +104,18 @@ const Server = struct { return; }; - self.newSession() catch |err| { - log.err("new session error: {any}", .{err}); - self.queueClose(socket); + const client = self.client_pool.create() catch |err| { + log.err("failed to create client: {any}", .{err}); + posix.close(socket); return; }; + errdefer self.client_pool.destroy(client); + + client.* = Client.init(socket, self); + + self.client = client; log.info("client connected", .{}); - self.client = Client(*Server).init(socket, self); self.queueRead(); self.queueTimeout(); } @@ -164,7 +137,7 @@ const Server = struct { ) void { std.debug.assert(completion == &self.timeout_completion); - const client = &(self.client orelse return); + const client = self.client orelse return; if (result) |_| { if (now().since(client.last_active) > self.timeout) { @@ -185,7 +158,7 @@ const Server = struct { } fn queueRead(self: *Server) void { - if (self.client) |*client| { + if (self.client) |client| { self.loop.io.recv( *Server, self, @@ -204,11 +177,11 @@ const Server = struct { ) void { std.debug.assert(completion == &self.conn_completion); - var client = &(self.client orelse return); + var client = self.client orelse return; const size = result catch |err| { log.err("read error: {any}", .{err}); - self.queueClose(client.socket); + client.close(null); return; }; if (size == 0) { @@ -220,7 +193,7 @@ const Server = struct { } const more = client.processData(size) catch |err| { - log.err("Client Processing Error: {}\n", .{err}); + log.err("Client Processing Error: {any}\n", .{err}); return; }; @@ -233,19 +206,18 @@ const Server = struct { fn queueSend( self: *Server, socket: posix.socket_t, + arena: ?ArenaAllocator, data: []const u8, - free_when_done: bool, ) !void { const sd = try self.send_pool.create(); errdefer self.send_pool.destroy(sd); sd.* = .{ - .data = data, .unsent = data, .server = self, .socket = socket, .completion = undefined, - .free_when_done = free_when_done, + .arena = arena, }; sd.queueSend(); } @@ -258,132 +230,16 @@ const Server = struct { &self.close_completion, socket, ); + var client = self.client.?; + client.deinit(); + self.client_pool.destroy(client); + self.client = null; } fn callbackClose(self: *Server, completion: *Completion, _: CloseError!void) void { std.debug.assert(completion == &self.close_completion); - if (self.client != null) { - self.client = null; - } self.queueAccept(); } - - fn handleCDP(self: *Server, cmd: []const u8) !void { - const res = cdp.do(self.allocator, cmd, self) catch |err| { - - // cdp end cmd - if (err == error.DisposeBrowserContext) { - // restart a new browser session - std.log.scoped(.cdp).debug("end cmd, restarting a new session...", .{}); - try self.newSession(); - return; - } - - return err; - }; - - // send result - if (res.len != 0) { - return self.send(res); - } - } - - // called from CDP - pub fn send(self: *Server, data: []const u8) !void { - if (self.client) |*client| { - try client.sendWS(data); - } - } - - fn newSession(self: *Server) !void { - try self.browser.newSession(self.allocator, self.loop); - try self.browser.session.initInspector( - self, - inspectorResponse, - inspectorEvent, - ); - } - - // // inspector - // // --------- - - // called by cdp - pub fn sendInspector(self: *Server, msg: []const u8) !void { - const env = self.browser.session.env; - if (env.getInspector()) |inspector| { - inspector.send(env, msg); - return; - } - return error.InspectNotSet; - } - - fn inspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { - if (std.log.defaultLogEnabled(.debug)) { - // msg should be {"id":,... - std.debug.assert(std.mem.startsWith(u8, msg, "{\"id\":")); - - const id_end = std.mem.indexOfScalar(u8, msg, ',') orelse { - log.warn("invalid inspector response message: {s}", .{msg}); - return; - }; - - const id = msg[6..id_end]; - std.log.scoped(.cdp).debug("Res (inspector) > id {s}", .{id}); - } - sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg); - } - - fn inspectorEvent(ctx: *anyopaque, msg: []const u8) void { - if (std.log.defaultLogEnabled(.debug)) { - // msg should be {"method":,... - std.debug.assert(std.mem.startsWith(u8, msg, "{\"method\":")); - const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse { - log.warn("invalid inspector event message: {s}", .{msg}); - return; - }; - const method = msg[10..method_end]; - std.log.scoped(.cdp).debug("Event (inspector) > method {s}", .{method}); - } - - sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg); - } - - fn sendInspectorMessage(self: *Server, msg: []const u8) void { - var client = &(self.client orelse return); - - var scrap = &self.scrap; - scrap.clearRetainingCapacity(); - - const field = ",\"sessionId\":"; - const sessionID = @tagName(self.state.sessionID); - - // + 2 for the quotes around the session - const message_len = msg.len + sessionID.len + 2 + field.len; - - scrap.ensureTotalCapacity(self.allocator, message_len) catch |err| { - log.err("Failed to expand inspector buffer: {}", .{err}); - return; - }; - - // -1 because we dont' want the closing brace '}' - scrap.appendSliceAssumeCapacity(msg[0 .. msg.len - 1]); - scrap.appendSliceAssumeCapacity(field); - scrap.appendAssumeCapacity('"'); - scrap.appendSliceAssumeCapacity(sessionID); - scrap.appendSliceAssumeCapacity("\"}"); - std.debug.assert(scrap.items.len == message_len); - - // TODO: Remove when we clean up ownership of messages between - // CDD and sending. - const owned = self.allocator.dupe(u8, scrap.items) catch return; - - client.sendWS(owned) catch |err| { - log.debug("Failed to write inspector message to client: {}", .{err}); - // don't bother trying to cleanly close the client, if sendWS fails - // we're almost certainly in a non-recoverable state (i.e. OOM) - self.queueClose(client.socket); - }; - } }; // I/O Send @@ -393,27 +249,20 @@ const Server = struct { // (with its own completion), allocated on the heap. // After the send (on the sendCbk) the dedicated context will be destroy // and the data slice will be free. -const Send = struct { - // The full data to be sent - data: []const u8, - - // Whether or not to free the data once the message is sent (or fails to) - // send. This is false in cases where the message is comptime known - free_when_done: bool, - - // Any unsent data we have. Initially unsent == data, but as part of the - // message is succesfully sent, unsent becomes a smaller and smaller slice - // of data +const Send = struct { // Any unsent data we have. unsent: []const u8, server: *Server, completion: Completion, socket: posix.socket_t, + // If we need to free anything when we're done + arena: ?ArenaAllocator, + fn deinit(self: *Send) void { var server = self.server; - if (self.free_when_done) { - server.allocator.free(self.data); + if (self.arena) |arena| { + arena.deinit(); } server.send_pool.destroy(self); } @@ -429,15 +278,11 @@ const Send = struct { ); } - fn sendCallback( - self: *Send, - _: *Completion, - result: SendError!usize, - ) void { + fn sendCallback(self: *Send, _: *Completion, result: SendError!usize) void { const sent = result catch |err| { - log.err("send error: {any}", .{err}); - if (self.server.client) |*client| { - self.server.queueClose(client.socket); + log.info("send error: {any}", .{err}); + if (self.server.client) |client| { + client.close(null); } self.deinit(); return; @@ -461,19 +306,27 @@ const Send = struct { // and when we send a message, we'll use server.send(...) to send via the server's // IO loop. During tests, we can inject a simple mock to record (and then verify) // the send message -fn Client(comptime S: type) type { +fn ClientT(comptime S: type, comptime C: type) type { const EMPTY_PONG = [_]u8{ 138, 0 }; // CLOSE, 2 length, code const CLOSE_NORMAL = [_]u8{ 136, 2, 3, 232 }; // code: 1000 const CLOSE_TOO_BIG = [_]u8{ 136, 2, 3, 241 }; // 1009 const CLOSE_PROTOCOL_ERROR = [_]u8{ 136, 2, 3, 234 }; //code: 1002 + // "private-use" close codes must be from 4000-49999 const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000 return struct { // The client is initially serving HTTP requests but, under normal circumstances // should eventually be upgraded to a websocket connections mode: Mode, + + // The CDP instance that processes messages from this client + // (a generic so we can test with a mock + // null until mode == .websocket + cdp: ?C, + + // Our Server (a generic so we can test with a mock) server: S, reader: Reader, socket: posix.socket_t, @@ -488,6 +341,7 @@ fn Client(comptime S: type) type { fn init(socket: posix.socket_t, server: S) Self { return .{ + .cdp = null, .mode = .http, .socket = socket, .server = server, @@ -496,14 +350,22 @@ fn Client(comptime S: type) type { }; } - fn close(self: *Self, close_code: CloseCode) void { - if (self.mode == .websocket) { - switch (close_code) { - .timeout => self.send(&CLOSE_TIMEOUT, false) catch {}, + pub fn deinit(self: *Self) void { + self.reader.deinit(); + if (self.cdp) |*cdp| { + cdp.deinit(); + } + } + + pub fn close(self: *Self, close_code: ?CloseCode) void { + if (close_code) |code| { + if (self.mode == .websocket) { + switch (code) { + .timeout => self.send(&CLOSE_TIMEOUT) catch {}, + } } } self.server.queueClose(self.socket); - self.reader.deinit(); } fn readBuf(self: *Self) []u8 { @@ -523,7 +385,7 @@ fn Client(comptime S: type) type { } } - fn processHTTPRequest(self: *Self) HTTPError!void { + fn processHTTPRequest(self: *Self) !void { std.debug.assert(self.reader.pos == 0); const request = self.reader.buf[0..self.reader.len]; @@ -550,7 +412,7 @@ fn Client(comptime S: type) type { error.InvalidVersionHeader => self.writeHTTPErrorResponse(400, "Invalid websocket version"), error.InvalidConnectionHeader => self.writeHTTPErrorResponse(400, "Invalid connection header"), else => { - log.err("error processing HTTP request: {}", .{err}); + log.err("error processing HTTP request: {any}", .{err}); self.writeHTTPErrorResponse(500, "Internal Server Error"); }, } @@ -582,7 +444,7 @@ fn Client(comptime S: type) type { } if (std.mem.eql(u8, url, "/json/version")) { - return self.send(self.server.json_version_response, false); + return self.send(self.server.json_version_response); } return error.NotFound; @@ -648,6 +510,9 @@ fn Client(comptime S: type) type { // our caller has already made sure this request ended in \r\n\r\n // so it isn't something we need to check again + var arena = ArenaAllocator.init(self.server.allocator); + errdefer arena.deinit(); + const response = blk: { // Response to an ugprade request is always this, with // the Sec-Websocket-Accept value a spacial sha1 hash of the @@ -661,8 +526,7 @@ fn Client(comptime S: type) type { // The response will be sent via the IO Loop and thus has to have its // own lifetime. - const res = try self.server.allocator.dupe(u8, template); - errdefer self.server.allocator.free(res); + const res = try arena.allocator().dupe(u8, template); // magic response const key_pos = res.len - 32; @@ -679,26 +543,36 @@ fn Client(comptime S: type) type { }; self.mode = .websocket; - return self.send(response, true); + self.cdp = C.init(self.server.allocator, self, self.server.loop); + return self.sendAlloc(arena, response); + } + + fn writeHTTPErrorResponse(self: *Self, comptime status: u16, comptime body: []const u8) void { + const response = std.fmt.comptimePrint( + "HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}", + .{ status, body.len, body }, + ); + + // we're going to close this connection anyways, swallowing any + // error seems safe + self.send(response) catch {}; } fn processWebsocketMessage(self: *Self) !bool { - var reader = &self.reader; + errdefer self.server.queueClose(self.socket); - errdefer { - reader.cleanup(); - self.server.queueClose(self.socket); - } + var reader = &self.reader; while (true) { const msg = reader.next() catch |err| { switch (err) { - error.TooLarge => self.send(&CLOSE_TOO_BIG, false) catch {}, - error.NotMasked => self.send(&CLOSE_PROTOCOL_ERROR, false) catch {}, - error.ReservedFlags => self.send(&CLOSE_PROTOCOL_ERROR, false) catch {}, - error.InvalidMessageType => self.send(&CLOSE_PROTOCOL_ERROR, false) catch {}, - error.InvalidContinuation => self.send(&CLOSE_PROTOCOL_ERROR, false) catch {}, - error.NestedFragementation => self.send(&CLOSE_PROTOCOL_ERROR, false) catch {}, + error.TooLarge => self.send(&CLOSE_TOO_BIG) catch {}, + error.NotMasked => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, + error.ReservedFlags => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, + error.InvalidMessageType => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, + error.ControlTooLarge => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, + error.InvalidContinuation => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, + error.NestedFragementation => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, error.OutOfMemory => {}, // don't borther trying to send an error in this case } return err; @@ -708,11 +582,14 @@ fn Client(comptime S: type) type { .pong => {}, .ping => try self.sendPong(msg.data), .close => { - self.send(&CLOSE_NORMAL, false) catch {}; + self.send(&CLOSE_NORMAL) catch {}; self.server.queueClose(self.socket); return false; }, - .text, .binary => try self.server.handleCDP(msg.data), + .text, .binary => if (self.cdp.?.processMessage(msg.data) == false) { + self.close(null); + return false; + }, } if (msg.cleanup_fragment) { reader.cleanup(); @@ -727,84 +604,60 @@ fn Client(comptime S: type) type { fn sendPong(self: *Self, data: []const u8) !void { if (data.len == 0) { - return self.send(&EMPTY_PONG, false); + return self.send(&EMPTY_PONG); } + var header_buf: [10]u8 = undefined; + const header = websocketHeader(&header_buf, .pong, data.len); - return self.sendFrame(data, .pong); - } - - fn sendWS(self: *Self, data: []const u8) !void { - std.debug.assert(data.len < 4294967296); + var arena = ArenaAllocator.init(self.server.allocator); + errdefer arena.deinit(); - // for now, we're going to dupe this before we send it, so we don't need - // to keep this around. - defer self.server.allocator.free(data); - return self.sendFrame(data, .text); + var framed = try arena.allocator().alloc(u8, header.len + data.len); + @memcpy(framed[0..header.len], header); + @memcpy(framed[header.len..], data); + return self.sendAlloc(arena, framed); } - // We need to append the websocket header to data. If our IO loop supported - // a writev call, this would be simple. - // For now, we'll just have to dupe data into a larger message. - // TODO: Remove this awful allocation (probably by passing a websocket-aware - // Writer into CDP) - fn sendFrame(self: *Self, data: []const u8, op_code: OpCode) !void { - if (comptime builtin.is_test == false) { - std.debug.assert(self.mode == .websocket); - } + // called by CDP + // Websocket frames have a variable lenght header. For server-client, + // it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have + // writev, so we need to get creative. We'll JSON serialize to a + // buffer, where the first 10 bytes are reserved. We can then backfill + // the header and send the slice. + pub fn sendJSON(self: *Self, message: anytype, opts: std.json.StringifyOptions) !void { + var arena = ArenaAllocator.init(self.server.allocator); + errdefer arena.deinit(); - // 10 is the max possible length of our header - // server->client has no mask, so it's 4 fewer bytes than the reader overhead - var header_buf: [10]u8 = undefined; + const allocator = arena.allocator(); - const header: []const u8 = blk: { - const len = data.len; - header_buf[0] = 128 | @intFromEnum(op_code); // fin | opcode + var buf: std.ArrayListUnmanaged(u8) = .{}; + try buf.ensureTotalCapacity(allocator, 512); - if (len <= 125) { - header_buf[1] = @intCast(len); - break :blk header_buf[0..2]; - } - - if (len < 65536) { - header_buf[1] = 126; - header_buf[2] = @intCast((len >> 8) & 0xFF); - header_buf[3] = @intCast(len & 0xFF); - break :blk header_buf[0..4]; - } - - header_buf[1] = 127; - header_buf[2] = 0; - header_buf[3] = 0; - header_buf[4] = 0; - header_buf[5] = 0; - header_buf[6] = @intCast((len >> 24) & 0xFF); - header_buf[7] = @intCast((len >> 16) & 0xFF); - header_buf[8] = @intCast((len >> 8) & 0xFF); - header_buf[9] = @intCast(len & 0xFF); - break :blk header_buf[0..10]; - }; + // reserve space for the maximum possible header + buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); - const allocator = self.server.allocator; - const full = try allocator.alloc(u8, header.len + data.len); - errdefer allocator.free(full); - @memcpy(full[0..header.len], header); - @memcpy(full[header.len..], data); - try self.send(full, true); + try std.json.stringify(message, opts, buf.writer(allocator)); + const framed = fillWebsocketHeader(buf); + return self.sendAlloc(arena, framed); } - fn writeHTTPErrorResponse(self: *Self, comptime status: u16, comptime body: []const u8) void { - const response = std.fmt.comptimePrint( - "HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}", - .{ status, body.len, body }, - ); + pub fn sendJSONRaw( + self: *Self, + arena: ArenaAllocator, + buf: std.ArrayListUnmanaged(u8), + ) !void { + // Dangerous API!. We assume the caller has reserved the first 10 + // bytes in `buf`. + const framed = fillWebsocketHeader(buf); + return self.sendAlloc(arena, framed); + } - // we're going to close this connection anyways, swallowing any - // error seems safe - self.send(response, false) catch {}; + fn send(self: *Self, data: []const u8) !void { + return self.server.queueSend(self.socket, null, data); } - fn send(self: *Self, data: []const u8, free_when_done: bool) !void { - return self.server.queueSend(self.socket, data, free_when_done); + fn sendAlloc(self: *Self, arena: ArenaAllocator, data: []const u8) !void { + return self.server.queueSend(self.socket, arena, data); } }; } @@ -865,19 +718,33 @@ const Reader = struct { return error.NotMasked; } + var is_control = false; var is_continuation = false; var message_type: Message.Type = undefined; switch (byte1 & 15) { 0 => is_continuation = true, 1 => message_type = .text, 2 => message_type = .binary, - 8 => message_type = .close, - 9 => message_type = .ping, - 10 => message_type = .pong, + 8 => { + is_control = true; + message_type = .close; + }, + 9 => { + is_control = true; + message_type = .ping; + }, + 10 => { + is_control = true; + message_type = .pong; + }, else => return error.InvalidMessageType, } - if (message_len > MAX_MESSAGE_SIZE) { + if (is_control) { + if (message_len > 125) { + return error.ControlTooLarge; + } + } else if (message_len > MAX_MESSAGE_SIZE) { return error.TooLarge; } @@ -1049,11 +916,61 @@ const OpCode = enum(u8) { pong = 128 | 10, }; -// "private-use" close codes must be from 4000-49999 const CloseCode = enum { timeout, }; +fn fillWebsocketHeader(buf: std.ArrayListUnmanaged(u8)) []const u8 { + // can't use buf[0..10] here, because the header length + // is variable. If it's just 2 bytes, for example, we need the + // framed message to be: + // h1, h2, data + // If we use buf[0..10], we'd get: + // h1, h2, 0, 0, 0, 0, 0, 0, 0, 0, data + + var header_buf: [10]u8 = undefined; + + // -10 because we reserved 10 bytes for the header above + const header = websocketHeader(&header_buf, .text, buf.items.len - 10); + const start = 10 - header.len; + + const message = buf.items; + @memcpy(message[start..10], header); + return message[start..]; +} + +// makes the assumption that our caller reserved the first +// 10 bytes for the header +fn websocketHeader(buf: []u8, op_code: OpCode, payload_len: usize) []const u8 { + std.debug.assert(buf.len == 10); + + const len = payload_len; + buf[0] = 128 | @intFromEnum(op_code); // fin | opcode + + if (len <= 125) { + buf[1] = @intCast(len); + return buf[0..2]; + } + + if (len < 65536) { + buf[1] = 126; + buf[2] = @intCast((len >> 8) & 0xFF); + buf[3] = @intCast(len & 0xFF); + return buf[0..4]; + } + + buf[1] = 127; + buf[2] = 0; + buf[3] = 0; + buf[4] = 0; + buf[5] = 0; + buf[6] = @intCast((len >> 24) & 0xFF); + buf[7] = @intCast((len >> 16) & 0xFF); + buf[8] = @intCast((len >> 8) & 0xFF); + buf[9] = @intCast(len & 0xFF); + return buf[0..10]; +} + pub fn run( allocator: Allocator, address: net.Address, @@ -1081,31 +998,23 @@ pub fn run( const vm = jsruntime.VM.init(); defer vm.deinit(); - // browser - var browser: Browser = undefined; - try Browser.init(&browser, allocator, loop, vm); - defer browser.deinit(); - const json_version_response = try buildJSONVersionResponse(allocator, address); var server = Server{ .loop = loop, .timeout = timeout, - .browser = &browser, .listener = listener, .allocator = allocator, .conn_completion = undefined, .close_completion = undefined, .accept_completion = undefined, .timeout_completion = undefined, - .state = cdp.State.init(browser.session.alloc), .json_version_response = json_version_response, .send_pool = std.heap.MemoryPool(Send).init(allocator), + .client_pool = std.heap.MemoryPool(Client).init(allocator), }; defer server.deinit(); - try browser.session.initInspector(&server, Server.inspectorResponse, Server.inspectorEvent); - // accept an connection server.queueAccept(); @@ -1268,7 +1177,8 @@ test "Client: http valid handshake" { var ms = MockServer{}; defer ms.deinit(); - var client = Client(*MockServer).init(0, &ms); + var client = ClientT(*MockServer, MockCDP).init(0, &ms); + defer client.deinit(); const request = "GET / HTTP/1.1\r\n" ++ @@ -1295,7 +1205,8 @@ test "Client: http get json version" { var ms = MockServer{}; defer ms.deinit(); - var client = Client(*MockServer).init(0, &ms); + var client = ClientT(*MockServer, MockCDP).init(0, &ms); + defer client.deinit(); const request = "GET /json/version HTTP/1.1\r\n\r\n"; @@ -1310,18 +1221,19 @@ test "Client: http get json version" { test "Client: write websocket message" { const cases = [_]struct { expected: []const u8, message: []const u8 }{ - .{ .expected = &.{ 129, 0 }, .message = "" }, - .{ .expected = [_]u8{ 129, 12 } ++ "hello world!", .message = "hello world!" }, - .{ .expected = [_]u8{ 129, 126, 0, 130 } ++ ("A" ** 130), .message = "A" ** 130 }, + .{ .expected = &.{ 129, 2, '"', '"' }, .message = "" }, + .{ .expected = [_]u8{ 129, 14 } ++ "\"hello world!\"", .message = "hello world!" }, + .{ .expected = [_]u8{ 129, 126, 0, 132 } ++ "\"" ++ ("A" ** 130) ++ "\"", .message = "A" ** 130 }, }; for (cases) |c| { var ms = MockServer{}; defer ms.deinit(); - var client = Client(*MockServer).init(0, &ms); + var client = ClientT(*MockServer, MockCDP).init(0, &ms); + defer client.deinit(); - try client.sendWS(try testing.allocator.dupe(u8, c.message)); + try client.sendJSON(c.message, .{}); try testing.expectEqual(1, ms.sent.items.len); try testing.expectEqualSlices(u8, c.expected, ms.sent.items[0]); } @@ -1362,6 +1274,16 @@ test "Client: read invalid websocket message" { &.{ 129, 1, 'a' }, ); + // control types (ping/ping/close) can't be > 125 bytes + for ([_]u8{ 136, 137, 138 }) |op| { + try assertWebSocketError( + error.ControlTooLarge, + 1002, + "", + &.{ op, 254, 1, 1 }, + ); + } + // length of message is 0000 0401, i.e: 1024 * 256 + 1 try assertWebSocketError( error.TooLarge, @@ -1549,7 +1471,8 @@ test "Client: fuzz" { var ms = MockServer{}; defer ms.deinit(); - var client = Client(*MockServer).init(0, &ms); + var client = ClientT(*MockServer, MockCDP).init(0, &ms); + defer client.deinit(); try SendRandom.send(&client, random, "GET /json/version HTTP/1.1\r\nContent-Length: 0\r\n\r\n"); try SendRandom.send(&client, random, "GET / HTTP/1.1\r\n" ++ @@ -1591,23 +1514,24 @@ test "Client: fuzz" { ms.sent.items[4], ); - try testing.expectEqual(3, ms.cdp.items.len); + const received = client.cdp.?.messages.items; + try testing.expectEqual(3, received.len); try testing.expectEqualSlices( u8, &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, - ms.cdp.items[0], + received[0], ); try testing.expectEqualSlices( u8, &([_]u8{ 64, 67, 66, 69 } ** 171 ++ [_]u8{ 64, 67, 66 }), - ms.cdp.items[1], + received[1], ); try testing.expectEqualSlices( u8, &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, - ms.cdp.items[2], + received[2], ); try testing.expectEqual(true, ms.closed); @@ -1674,7 +1598,7 @@ test "server: get /json/version" { } fn assertHTTPError( - expected_error: HTTPError, + expected_error: anyerror, comptime expected_status: u16, comptime expected_body: []const u8, input: []const u8, @@ -1682,7 +1606,9 @@ fn assertHTTPError( var ms = MockServer{}; defer ms.deinit(); - var client = Client(*MockServer).init(0, &ms); + var client = ClientT(*MockServer, MockCDP).init(0, &ms); + defer client.deinit(); + @memcpy(client.reader.buf[0..input.len], input); try testing.expectError(expected_error, client.processData(input.len)); @@ -1696,7 +1622,7 @@ fn assertHTTPError( } fn assertWebSocketError( - expected_error: WebSocketError, + expected_error: anyerror, close_code: u16, close_payload: []const u8, input: []const u8, @@ -1704,7 +1630,9 @@ fn assertWebSocketError( var ms = MockServer{}; defer ms.deinit(); - var client = Client(*MockServer).init(0, &ms); + var client = ClientT(*MockServer, MockCDP).init(0, &ms); + defer client.deinit(); + client.mode = .websocket; // force websocket message processing @memcpy(client.reader.buf[0..input.len], input); @@ -1734,7 +1662,8 @@ fn assertWebSocketMessage( var ms = MockServer{}; defer ms.deinit(); - var client = Client(*MockServer).init(0, &ms); + var client = ClientT(*MockServer, MockCDP).init(0, &ms); + defer client.deinit(); client.mode = .websocket; // force websocket message processing @memcpy(client.reader.buf[0..input.len], input); @@ -1755,14 +1684,12 @@ fn assertWebSocketMessage( } const MockServer = struct { + loop: *jsruntime.Loop = undefined, closed: bool = false, // record the messages we sent to the client sent: std.ArrayListUnmanaged([]const u8) = .{}, - // record the CDP messages we need to process - cdp: std.ArrayListUnmanaged([]const u8) = .{}, - allocator: Allocator = testing.allocator, json_version_response: []const u8 = "the json version response", @@ -1774,34 +1701,50 @@ const MockServer = struct { allocator.free(msg); } self.sent.deinit(allocator); - - for (self.cdp.items) |msg| { - allocator.free(msg); - } - self.cdp.deinit(allocator); } fn queueClose(self: *MockServer, _: anytype) void { self.closed = true; } - fn handleCDP(self: *MockServer, message: []const u8) !void { - const owned = try self.allocator.dupe(u8, message); - try self.cdp.append(self.allocator, owned); - } - fn queueSend( self: *MockServer, socket: posix.socket_t, + arena: ?ArenaAllocator, data: []const u8, - free_when_done: bool, ) !void { _ = socket; const owned = try self.allocator.dupe(u8, data); try self.sent.append(self.allocator, owned); - if (free_when_done) { - testing.allocator.free(data); + if (arena) |a| { + a.deinit(); + } + } +}; + +const MockCDP = struct { + messages: std.ArrayListUnmanaged([]const u8) = .{}, + + allocator: Allocator = testing.allocator, + + fn init(_: Allocator, client: anytype, loop: *jsruntime.Loop) MockCDP { + _ = loop; + _ = client; + return .{}; + } + + fn deinit(self: *MockCDP) void { + const allocator = self.allocator; + for (self.messages.items) |msg| { + allocator.free(msg); } + self.messages.deinit(allocator); + } + + fn processMessage(self: *MockCDP, message: []const u8) bool { + const owned = self.allocator.dupe(u8, message) catch unreachable; + self.messages.append(self.allocator, owned) catch unreachable; + return true; } }; diff --git a/src/unit_tests.zig b/src/unit_tests.zig index e4f1ac3e..94c10913 100644 --- a/src/unit_tests.zig +++ b/src/unit_tests.zig @@ -41,9 +41,8 @@ pub fn main() !void { try parser.init(); defer parser.deinit(); - var mem: [8192]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&mem); - const allocator = fba.allocator(); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); var loop = try jsruntime.Loop.init(allocator); defer loop.deinit(); @@ -369,7 +368,6 @@ test { std.testing.refAllDecls(@import("browser/dump.zig")); std.testing.refAllDecls(@import("browser/loader.zig")); std.testing.refAllDecls(@import("browser/mime.zig")); - std.testing.refAllDecls(@import("cdp/msg.zig")); std.testing.refAllDecls(@import("css/css.zig")); std.testing.refAllDecls(@import("css/libdom_test.zig")); std.testing.refAllDecls(@import("css/match_test.zig"));