diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 7494f5e22..5f0035f83 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -116,8 +116,7 @@ jobs: echo "value=$args" >> "$GITHUB_OUTPUT" - run: | - ./lightpanda serve --port 9222 --protocol cdp ${{ steps.args.outputs.value }} & - ./lightpanda serve --port 9223 --protocol webdriver ${{ steps.args.outputs.value }} & + ./lightpanda serve --port 9222 --protocol cdp --protocol webdriver ${{ steps.args.outputs.value }} & - run: | go run runner/main.go diff --git a/src/Config.zig b/src/Config.zig index 6d206e2ba..262ab020e 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -386,7 +386,8 @@ const Commands = cli.Builder(.{ .{ .name = "host", .type = []const u8, .default = "127.0.0.1" }, .{ .name = "port", .type = u16, .default = 9222 }, .{ .name = "advertise_host", .type = ?[]const u8 }, - .{ .name = "protocol", .type = Protocol, .default = Protocol.cdp }, + // Repeatable; one server can speak several on the same port. + .{ .name = "protocol", .type = Protocol, .multiple = true }, .{ .name = "cdp_max_connections", .type = u16, .default = 16 }, .{ .name = "cdp_max_pending_connections", .type = u16, .default = 128 }, .{ .name = "cdp_max_message_size", .type = u32, .default = 1024 * 1024 }, @@ -827,10 +828,10 @@ pub const Protocol = enum { webdriver, }; -pub fn protocol(self: *const Config) Protocol { +pub fn protocols(self: *const Config) []const Protocol { return switch (self.mode) { - .serve => |opts| opts.protocol, - .mcp => .cdp, + .serve => |opts| if (opts.protocol.items.len == 0) &.{.cdp} else opts.protocol.items, + .mcp => &.{.cdp}, else => unreachable, }; } diff --git a/src/browser/frame/user_input.zig b/src/browser/frame/user_input.zig index fe8ae8475..46c08d3d4 100644 --- a/src/browser/frame/user_input.zig +++ b/src/browser/frame/user_input.zig @@ -18,7 +18,7 @@ // Synthetic user input driving the DOM: mouse, wheel, keyboard, focus // navigation and text insertion. These are mostly fed by CDP's Input domain -// (src/cdp/domains/input.zig) and by EventManager's default activation +// (src/server/cdp/domains/input.zig) and by EventManager's default activation // behavior. Form submission itself lives on the Frame (it's a navigation // concern); the activation paths here call into it. diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 0a4e2c1ac..128739be2 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -21,7 +21,6 @@ const lp = @import("lightpanda"); const zenai = @import("zenai"); const NodeRegistry = @import("../NodeRegistry.zig"); -const LimitedWriter = @import("../LimitedWriter.zig"); const DOMNode = @import("webapi/Node.zig"); const Selector = @import("webapi/selector/Selector.zig"); diff --git a/src/help.zon b/src/help.zon index 9f238da58..d3aec0b9a 100644 --- a/src/help.zon +++ b/src/help.zon @@ -48,6 +48,14 @@ \\ --port \\ Port of the CDP server. \\ Defaults to 9222. + \\ --protocol + \\ Automation protocol to serve. Can be passed multiple times to + \\ serve several on the same port. + \\ Defaults to cdp. + \\ Allowed values: + \\ cdp Chrome DevTools Protocol (WebSocket on /). + \\ webdriver WebDriver BiDi (WebSocket on /session) plus the + \\ classic WebDriver session endpoints. , .fetch = \\usage: {0s} fetch ... [OPTIONS] [COMMON_OPTIONS] diff --git a/src/server/Server.zig b/src/server/Server.zig index 86a05e8ad..ec1ded986 100644 --- a/src/server/Server.zig +++ b/src/server/Server.zig @@ -160,6 +160,12 @@ pub fn init(app: *App, address: sys_net.IpAddress) !*Server { const bidi_session_url = try std.fmt.allocPrint(allocator, "ws://{s}:{d}/session/", .{ app.config.advertiseHost(), port }); errdefer allocator.free(bidi_session_url); + var protocols: Handshake.Protocols = .{}; + for (app.config.protocols()) |p| switch (p) { + .cdp => protocols.cdp = true, + .webdriver => protocols.webdriver = true, + }; + self.* = .{ .app = app, .cdp_pool = .empty, @@ -170,10 +176,7 @@ pub fn init(app: *App, address: sys_net.IpAddress) !*Server { .pollfds = pollfds, .wakeup_pipe = pipe, .poll_snapshot = poll_snapshot, - .protocols = switch (app.config.protocol()) { - .cdp => .{ .cdp = true }, - .webdriver => .{ .webdriver = true }, - }, + .protocols = protocols, }; return self; } @@ -1468,7 +1471,7 @@ test "server: classic session bootstrap errors" { } test "server: protocol gate" { - // The test server serves both; the CLI only ever enables one. + // The test server serves both; --protocol picks which a real server does. const protocols = &testing.test_cdp_server.?.protocols; defer protocols.* = .{ .cdp = true, .webdriver = true }; diff --git a/src/server/bidi/remote_value.zig b/src/server/bidi/remote_value.zig index 5167104e5..f67ec32ff 100644 --- a/src/server/bidi/remote_value.zig +++ b/src/server/bidi/remote_value.zig @@ -484,12 +484,13 @@ pub const Serializer = struct { defer self.unsee(); const next_depth = depth + 1; - var names = try object.nameIterator(); - var list: std.ArrayList(Property) = try .initCapacity(self.arena, names.count); - while (try names.next()) |name| { - const owned = try self.arena.dupe(u8, name); - const value = try object.get(owned); - list.appendAssumeCapacity(.{ .name = owned, .value = try self.remote(value, next_depth, false) }); + var it = try object.iterator(); + var list: std.ArrayList(Property) = try .initCapacity(self.arena, it.count); + while (try it.next()) |entry| { + list.appendAssumeCapacity(.{ + .name = try self.arena.dupe(u8, entry.name), + .value = try self.remote(entry.value, next_depth, false), + }); } return list.items; }