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"));