mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 14:43:28 +00:00
Reduce copying of incoming and outgoing inspector messages.
When inspector emits a message, to be sent to the client, we copy those bytes a number of times. First, V8 serializes the message to CBOR. Next, it converts it to JSON. We then copy this into a C++ string, then into a Zig slice. We create one final copy (with websocket framing) to add to the write queue. Something similar, but a little less extreme, happens with incoming messages. By supporting CBOR messages directly, we not only reduce the amount of copying, but also leverage our [more tightly scoped and re-used] arenas. CBOR is essentially a standardized MessagePack. Two functions, jsonToCbor and cborToJson have been introduced to take our incoming JSON message and convert it to CBOR and, vice-versa. V8 automatically detects that the message is CBOR and, if the incoming message is CBOR, the outgoing message is CBOR also. While v8 is spec-compliant, it has specific expectations and behavior. For example, it never emits a fixed-length array / map - it's always an infinite array / map (with a special "break" code at the end). For this reason, our implementation is not complete, but rather designed to work with what v8 does and expects. Another example of this is, and I don't understand why, some of the incoming messages have a "params" field. V8 requires this to be a CBOR embedded data field (that is, CBOR embedded into CBOR). If we pass an array directly, while semantically the same, it'll fail. I guess this is how Chrome serializes the data, and rather than just reading the data as-is, v8 asserts that it's encoded in a particularly flavor. Weird. But we have to accommodate that.
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||
},
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/faab44996a5cb74c71592bda404208fde4bf2e63.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6xWyAwB_NFICSO4Q3O-c7gDKnYiwky5FhQzTZMIr",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/bf7ba696b3e819195f8fc349b2778c59aab81a61.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6xm3AwA287seRdWB_mIjZ9_Ayk-81z9uwWoag7Er",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
|
||||
52
src/cdp/cbor/cbor.zig
Normal file
52
src/cdp/cbor/cbor.zig
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub const jsonToCbor = @import("json_to_cbor.zig").jsonToCbor;
|
||||
pub const cborToJson = @import("cbor_to_json.zig").cborToJson;
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "cbor" {
|
||||
try testCbor("{\"x\":null}");
|
||||
try testCbor("{\"x\":true}");
|
||||
try testCbor("{\"x\":false}");
|
||||
try testCbor("{\"x\":0}");
|
||||
try testCbor("{\"x\":1}");
|
||||
try testCbor("{\"x\":-1}");
|
||||
try testCbor("{\"x\":4832839283}");
|
||||
try testCbor("{\"x\":-998128383}");
|
||||
try testCbor("{\"x\":48328.39283}");
|
||||
try testCbor("{\"x\":-9981.28383}");
|
||||
try testCbor("{\"x\":\"\"}");
|
||||
try testCbor("{\"x\":\"over 9000!\"}");
|
||||
|
||||
try testCbor("{\"x\":[]}");
|
||||
try testCbor("{\"x\":{}}");
|
||||
}
|
||||
|
||||
fn testCbor(json: []const u8) !void {
|
||||
const std = @import("std");
|
||||
|
||||
defer testing.reset();
|
||||
const encoded = try jsonToCbor(testing.arena_allocator, json);
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try cborToJson(encoded, arr.writer(testing.arena_allocator));
|
||||
|
||||
try testing.expectEqual(json, arr.items);
|
||||
}
|
||||
252
src/cdp/cbor/cbor_to_json.zig
Normal file
252
src/cdp/cbor/cbor_to_json.zig
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Error = error{
|
||||
EOSReadingFloat,
|
||||
UnknownTag,
|
||||
EOSReadingArray,
|
||||
UnterminatedArray,
|
||||
EOSReadingMap,
|
||||
UnterminatedMap,
|
||||
EOSReadingLength,
|
||||
InvalidLength,
|
||||
MissingData,
|
||||
EOSExpectedString,
|
||||
ExpectedString,
|
||||
OutOfMemory,
|
||||
EmbeddedDataIsShort,
|
||||
InvalidEmbeddedDataEnvelope,
|
||||
};
|
||||
|
||||
pub fn cborToJson(input: []const u8, writer: anytype) !void {
|
||||
if (input.len < 7) {
|
||||
return error.InvalidCBORMessage;
|
||||
}
|
||||
|
||||
var data = input;
|
||||
while (data.len > 0) {
|
||||
data = try writeValue(data, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeValue(data: []const u8, writer: anytype) Error![]const u8 {
|
||||
switch (data[0]) {
|
||||
0xf4 => {
|
||||
try writer.writeAll("false");
|
||||
return data[1..];
|
||||
},
|
||||
0xf5 => {
|
||||
try writer.writeAll("true");
|
||||
return data[1..];
|
||||
},
|
||||
0xf6, 0xf7 => { // 0xf7 is undefined
|
||||
try writer.writeAll("null");
|
||||
return data[1..];
|
||||
},
|
||||
0x9f => return writeInfiniteArray(data[1..], writer),
|
||||
0xbf => return writeInfiniteMap(data[1..], writer),
|
||||
0xd8 => {
|
||||
// This is major type 6, which is generic tagged data. We only
|
||||
// support 1 tag: embedded cbor data.
|
||||
if (data.len < 7) {
|
||||
return error.EmbeddedDataIsShort;
|
||||
}
|
||||
if (data[1] != 0x18 or data[2] != 0x5a) {
|
||||
return error.InvalidEmbeddedDataEnvelope;
|
||||
}
|
||||
// skip the length, we have the full paylaod
|
||||
return writeValue(data[7..], writer);
|
||||
},
|
||||
0xf9 => { // f16
|
||||
if (data.len < 3) {
|
||||
return error.EOSReadingFloat;
|
||||
}
|
||||
try writer.print("{d}", .{@as(f16, @bitCast(std.mem.readInt(u16, data[1..3], .big)))});
|
||||
return data[3..];
|
||||
},
|
||||
0xfa => { // f32
|
||||
if (data.len < 5) {
|
||||
return error.EOSReadingFloat;
|
||||
}
|
||||
try writer.print("{d}", .{@as(f32, @bitCast(std.mem.readInt(u32, data[1..5], .big)))});
|
||||
return data[5..];
|
||||
},
|
||||
0xfb => { // f64
|
||||
if (data.len < 9) {
|
||||
return error.EOSReadingFloat;
|
||||
}
|
||||
try writer.print("{d}", .{@as(f64, @bitCast(std.mem.readInt(u64, data[1..9], .big)))});
|
||||
return data[9..];
|
||||
},
|
||||
else => |b| {
|
||||
const major_type = b >> 5;
|
||||
switch (major_type) {
|
||||
0 => {
|
||||
const rest, const length = try parseLength(data);
|
||||
try writer.print("{d}", .{length});
|
||||
return rest;
|
||||
},
|
||||
1 => {
|
||||
const rest, const length = try parseLength(data);
|
||||
try writer.print("{d}", .{-@as(i64, @intCast(length)) - 1});
|
||||
return rest;
|
||||
},
|
||||
2 => {
|
||||
const rest, const str = try parseString(data);
|
||||
try writer.writeByte('"');
|
||||
try std.base64.standard.Encoder.encodeWriter(writer, str);
|
||||
try writer.writeByte('"');
|
||||
return rest;
|
||||
},
|
||||
3 => {
|
||||
const rest, const str = try parseString(data);
|
||||
try std.json.encodeJsonString(str, .{}, writer);
|
||||
return rest;
|
||||
},
|
||||
// 4 => unreachable, // fixed-length array
|
||||
// 5 => unreachable, // fixed-length map
|
||||
else => return error.UnknownTag,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// We expect every array from V8 to be an infinite-length array. That it, it
|
||||
// starts with the special tag: (4<<5) | 31 which an "array" with infinite
|
||||
// length.
|
||||
// Of course, it isn't infite, the end of the array happens when we hit a break
|
||||
// code which is FF (7 << 5) | 31
|
||||
fn writeInfiniteArray(d: []const u8, writer: anytype) ![]const u8 {
|
||||
if (d.len == 0) {
|
||||
return error.EOSReadingArray;
|
||||
}
|
||||
if (d[0] == 255) {
|
||||
try writer.writeAll("[]");
|
||||
return d[1..];
|
||||
}
|
||||
|
||||
try writer.writeByte('[');
|
||||
var data = try writeValue(d, writer);
|
||||
while (data.len > 0) {
|
||||
if (data[0] == 255) {
|
||||
try writer.writeByte(']');
|
||||
return data[1..];
|
||||
}
|
||||
try writer.writeByte(',');
|
||||
data = try writeValue(data, writer);
|
||||
}
|
||||
|
||||
// Reaching the end of the input is a mistake, should have reached the break
|
||||
// code
|
||||
return error.UnterminatedArray;
|
||||
}
|
||||
|
||||
// We expect every map from V8 to be an infinite-length map. That it, it
|
||||
// starts with the special tag: (5<<5) | 31 which an "map" with infinite
|
||||
// length.
|
||||
// Of course, it isn't infite, the end of the map happens when we hit a break
|
||||
// code which is FF (7 << 5) | 31
|
||||
fn writeInfiniteMap(d: []const u8, writer: anytype) ![]const u8 {
|
||||
if (d.len == 0) {
|
||||
return error.EOSReadingMap;
|
||||
}
|
||||
if (d[0] == 255) {
|
||||
try writer.writeAll("{}");
|
||||
return d[1..];
|
||||
}
|
||||
|
||||
try writer.writeByte('{');
|
||||
|
||||
var data = blk: {
|
||||
const data, const field = try maybeParseString(d);
|
||||
try std.json.encodeJsonString(field, .{}, writer);
|
||||
try writer.writeByte(':');
|
||||
break :blk try writeValue(data, writer);
|
||||
};
|
||||
|
||||
while (data.len > 0) {
|
||||
if (data[0] == 255) {
|
||||
try writer.writeByte('}');
|
||||
return data[1..];
|
||||
}
|
||||
try writer.writeByte(',');
|
||||
data, const field = try maybeParseString(data);
|
||||
try std.json.encodeJsonString(field, .{}, writer);
|
||||
try writer.writeByte(':');
|
||||
data = try writeValue(data, writer);
|
||||
}
|
||||
|
||||
// Reaching the end of the input is a mistake, should have reached the break
|
||||
// code
|
||||
return error.UnterminatedMap;
|
||||
}
|
||||
|
||||
fn parseLength(data: []const u8) !struct { []const u8, usize } {
|
||||
std.debug.assert(data.len > 0);
|
||||
switch (data[0] & 0b11111) {
|
||||
0...23 => |n| return .{ data[1..], n },
|
||||
24 => {
|
||||
if (data.len == 1) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[2..], @intCast(data[1]) };
|
||||
},
|
||||
25 => {
|
||||
if (data.len < 3) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[3..], @intCast(std.mem.readInt(u16, data[1..3], .big)) };
|
||||
},
|
||||
26 => {
|
||||
if (data.len < 5) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[5..], @intCast(std.mem.readInt(u32, data[1..5], .big)) };
|
||||
},
|
||||
27 => {
|
||||
if (data.len < 9) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[9..], @intCast(std.mem.readInt(u64, data[1..9], .big)) };
|
||||
},
|
||||
else => return error.InvalidLength,
|
||||
}
|
||||
}
|
||||
|
||||
fn parseString(data: []const u8) !struct { []const u8, []const u8 } {
|
||||
const rest, const length = try parseLength(data);
|
||||
if (rest.len < length) {
|
||||
return error.MissingData;
|
||||
}
|
||||
return .{ rest[length..], rest[0..length] };
|
||||
}
|
||||
|
||||
fn maybeParseString(data: []const u8) !struct { []const u8, []const u8 } {
|
||||
if (data.len == 0) {
|
||||
return error.EOSExpectedString;
|
||||
}
|
||||
const b = data[0];
|
||||
if (b >> 5 != 3) {
|
||||
return error.ExpectedString;
|
||||
}
|
||||
return parseString(data);
|
||||
}
|
||||
173
src/cdp/cbor/json_to_cbor.zig
Normal file
173
src/cdp/cbor/json_to_cbor.zig
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Error = error{
|
||||
InvalidJson,
|
||||
OutOfMemory,
|
||||
SyntaxError,
|
||||
UnexpectedEndOfInput,
|
||||
ValueTooLong,
|
||||
};
|
||||
|
||||
pub fn jsonToCbor(arena: Allocator, input: []const u8) ![]const u8 {
|
||||
var scanner = json.Scanner.initCompleteInput(arena, input);
|
||||
defer scanner.deinit();
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try writeNext(arena, &arr, &scanner);
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
fn writeNext(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner) Error!void {
|
||||
const token = scanner.nextAlloc(arena, .alloc_if_needed) catch return error.InvalidJson;
|
||||
return writeToken(arena, arr, scanner, token);
|
||||
}
|
||||
|
||||
fn writeToken(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner, token: json.Token) Error!void {
|
||||
switch (token) {
|
||||
.object_begin => return writeObject(arena, arr, scanner),
|
||||
.array_begin => return writeArray(arena, arr, scanner),
|
||||
.true => return arr.append(arena, 7 << 5 | 21),
|
||||
.false => return arr.append(arena, 7 << 5 | 20),
|
||||
.null => return arr.append(arena, 7 << 5 | 22),
|
||||
.allocated_string, .string => |key| return writeString(arena, arr, key),
|
||||
.allocated_number, .number => |s| {
|
||||
if (json.isNumberFormattedLikeAnInteger(s)) {
|
||||
return writeInteger(arena, arr, s);
|
||||
}
|
||||
const f = std.fmt.parseFloat(f64, s) catch unreachable;
|
||||
return writeHeader(arena, arr, 7, @intCast(@as(u64, @bitCast(f))));
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn writeObject(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner) !void {
|
||||
const envelope = try startEmbeddedMessage(arena, arr);
|
||||
|
||||
// MajorType 5 (map) | 5-byte infinite length
|
||||
try arr.append(arena, 5 << 5 | 31);
|
||||
|
||||
while (true) {
|
||||
switch (try scanner.nextAlloc(arena, .alloc_if_needed)) {
|
||||
.allocated_string, .string => |key| {
|
||||
try writeString(arena, arr, key);
|
||||
try writeNext(arena, arr, scanner);
|
||||
},
|
||||
.object_end => {
|
||||
// MajorType 7 (break) | 5-byte infinite length
|
||||
try arr.append(arena, 7 << 5 | 31);
|
||||
return finalizeEmbeddedMessage(arr, envelope);
|
||||
},
|
||||
else => return error.InvalidJson,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn writeArray(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner) !void {
|
||||
const envelope = try startEmbeddedMessage(arena, arr);
|
||||
|
||||
// MajorType 4 (array) | 5-byte infinite length
|
||||
try arr.append(arena, 4 << 5 | 31);
|
||||
while (true) {
|
||||
const token = scanner.nextAlloc(arena, .alloc_if_needed) catch return error.InvalidJson;
|
||||
switch (token) {
|
||||
.array_end => {
|
||||
// MajorType 7 (break) | 5-byte infinite length
|
||||
try arr.append(arena, 7 << 5 | 31);
|
||||
return finalizeEmbeddedMessage(arr, envelope);
|
||||
},
|
||||
else => try writeToken(arena, arr, scanner, token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn writeString(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), value: []const u8) !void {
|
||||
try writeHeader(arena, arr, 3, value.len);
|
||||
return arr.appendSlice(arena, value);
|
||||
}
|
||||
|
||||
fn writeInteger(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
|
||||
const n = std.fmt.parseInt(i64, s, 10) catch {
|
||||
return error.InvalidJson;
|
||||
};
|
||||
if (n >= 0) {
|
||||
return writeHeader(arena, arr, 0, @intCast(n));
|
||||
}
|
||||
return writeHeader(arena, arr, 1, @intCast(-1 - n));
|
||||
}
|
||||
|
||||
fn writeHeader(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), comptime typ: u8, count: usize) !void {
|
||||
switch (count) {
|
||||
0...23 => try arr.append(arena, typ << 5 | @as(u8, @intCast(count))),
|
||||
24...255 => {
|
||||
try arr.ensureUnusedCapacity(arena, 2);
|
||||
arr.appendAssumeCapacity(typ << 5 | 24);
|
||||
arr.appendAssumeCapacity(@intCast(count));
|
||||
},
|
||||
256...65535 => {
|
||||
try arr.ensureUnusedCapacity(arena, 3);
|
||||
arr.appendAssumeCapacity(typ << 5 | 25);
|
||||
arr.appendAssumeCapacity(@intCast((count >> 8) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast(count & 0xff));
|
||||
},
|
||||
65536...4294967295 => {
|
||||
try arr.ensureUnusedCapacity(arena, 5);
|
||||
arr.appendAssumeCapacity(typ << 5 | 26);
|
||||
arr.appendAssumeCapacity(@intCast((count >> 24) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 16) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 8) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast(count & 0xff));
|
||||
},
|
||||
else => {
|
||||
try arr.ensureUnusedCapacity(arena, 9);
|
||||
arr.appendAssumeCapacity(typ << 5 | 27);
|
||||
arr.appendAssumeCapacity(@intCast((count >> 56) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 48) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 40) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 32) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 24) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 16) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 8) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast(count & 0xff));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// I don't know why, but V8 expects any array or map (including the outer-most
|
||||
// object), to be encoded as embedded cbor data. This is CBOR that contains CBOR.
|
||||
// I feel that it's fine that it supports it, but why _require_ it? Seems like
|
||||
// a waste of 7 bytes.
|
||||
fn startEmbeddedMessage(arena: Allocator, arr: *std.ArrayListUnmanaged(u8)) !usize {
|
||||
try arr.appendSlice(arena, &.{ 0xd8, 0x18, 0x5a, 0, 0, 0, 0 });
|
||||
return arr.items.len;
|
||||
}
|
||||
|
||||
fn finalizeEmbeddedMessage(arr: *std.ArrayListUnmanaged(u8), pos: usize) !void {
|
||||
var items = arr.items;
|
||||
const length = items.len - pos;
|
||||
items[pos - 4] = @intCast((length >> 24) & 0xff);
|
||||
items[pos - 3] = @intCast((length >> 16) & 0xff);
|
||||
items[pos - 2] = @intCast((length >> 8) & 0xff);
|
||||
items[pos - 1] = @intCast(length & 0xff);
|
||||
}
|
||||
@@ -17,10 +17,12 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const cbor = @import("cbor/cbor.zig");
|
||||
const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
@@ -458,31 +460,21 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *const Self, msg: []const u8) void {
|
||||
self.inspector.send(msg);
|
||||
pub fn callInspector(self: *const Self, arena: Allocator, input: []const u8) !void {
|
||||
const encoded = try cbor.jsonToCbor(arena, input);
|
||||
try self.inspector.send(encoded);
|
||||
// force running micro tasks after send input to the inspector.
|
||||
self.cdp.browser.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, str: Env.Inspector.StringView) void {
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), str) catch |err| {
|
||||
log.err(.cdp, "send inspector response", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {
|
||||
if (log.enabled(.cdp, .debug)) {
|
||||
// msg should be {"method":<method>,...
|
||||
std.debug.assert(std.mem.startsWith(u8, msg, "{\"method\":"));
|
||||
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
|
||||
log.err(.cdp, "invalid inspector event", .{ .msg = msg });
|
||||
return;
|
||||
};
|
||||
const method = msg[10..method_end];
|
||||
log.debug(.cdp, "inspector event", .{ .method = method });
|
||||
}
|
||||
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
|
||||
pub fn onInspectorEvent(ctx: *anyopaque, str: Env.Inspector.StringView) void {
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), str) catch |err| {
|
||||
log.err(.cdp, "send inspector event", .{ .err = err });
|
||||
};
|
||||
}
|
||||
@@ -490,7 +482,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
// This is hacky x 2. First, we create the JSON payload by gluing our
|
||||
// session_id onto it. Second, we're much more client/websocket aware than
|
||||
// we should be.
|
||||
fn sendInspectorMessage(self: *Self, msg: []const u8) !void {
|
||||
fn sendInspectorMessage(self: *Self, str: Env.Inspector.StringView) !void {
|
||||
const session_id = self.session_id orelse {
|
||||
// We no longer have an active session. What should we do
|
||||
// in this case?
|
||||
@@ -501,27 +493,26 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
var arena = std.heap.ArenaAllocator.init(cdp.allocator);
|
||||
errdefer arena.deinit();
|
||||
|
||||
const field = ",\"sessionId\":\"";
|
||||
|
||||
// + 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;
|
||||
|
||||
const aa = arena.allocator();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
buf.ensureTotalCapacity(arena.allocator(), message_len) catch |err| {
|
||||
log.err(.cdp, "inspector buffer", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
// reserve 10 bytes for websocket header
|
||||
buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
|
||||
try buf.appendSlice(aa, &.{ 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 cbor.cborToJson(str.bytes(), buf.writer(aa));
|
||||
|
||||
std.debug.assert(buf.getLast() == '}');
|
||||
|
||||
// We need to inject the session_id
|
||||
// First, we strip out the closing '}'
|
||||
buf.items.len -= 1;
|
||||
|
||||
// Next we inject the session id field + value
|
||||
try buf.appendSlice(aa, ",\"sessionId\":\"");
|
||||
try buf.appendSlice(aa, session_id);
|
||||
|
||||
// Finally, we re-close the object. Smooth.
|
||||
try buf.appendSlice(aa, "\"}");
|
||||
|
||||
try cdp.client.sendJSONRaw(arena, buf);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ fn sendInspector(cmd: anytype, action: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// the result to return is handled directly by the inspector.
|
||||
bc.callInspector(cmd.input.json);
|
||||
return bc.callInspector(cmd.arena, cmd.input.json);
|
||||
}
|
||||
|
||||
fn logInspector(cmd: anytype, action: anytype) !void {
|
||||
|
||||
@@ -1547,7 +1547,15 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
// If necessary, turn a void context into something we can safely ptrCast
|
||||
const safe_context: *anyopaque = if (ContextT == void) @constCast(@ptrCast(&{})) else ctx;
|
||||
|
||||
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
|
||||
const channel = v8.InspectorChannel.init(safe_context, struct {
|
||||
fn onInspectorResponse(ctx2: *anyopaque, call_id: u32, msg: v8.StringView) void {
|
||||
InspectorContainer.onInspectorResponse(ctx2, call_id, StringView{ .inner = msg });
|
||||
}
|
||||
}.onInspectorResponse, struct {
|
||||
fn onInspectorEvent(ctx2: *anyopaque, msg: v8.StringView) void {
|
||||
InspectorContainer.onInspectorEvent(ctx2, StringView{ .inner = msg });
|
||||
}
|
||||
}.onInspectorEvent, isolate);
|
||||
|
||||
const client = v8.InspectorClient.init();
|
||||
|
||||
@@ -1561,7 +1569,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
self.inner.deinit();
|
||||
}
|
||||
|
||||
pub fn send(self: *const Inspector, msg: []const u8) void {
|
||||
pub fn send(self: *const Inspector, msg: []const u8) !void {
|
||||
self.session.dispatchProtocolMessage(self.isolate, msg);
|
||||
}
|
||||
|
||||
@@ -1621,6 +1629,23 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode;
|
||||
return toa.ptr;
|
||||
}
|
||||
|
||||
pub const StringView = struct {
|
||||
inner: v8.StringView,
|
||||
|
||||
pub fn length(self: StringView) usize {
|
||||
return self.inner.length();
|
||||
}
|
||||
|
||||
pub fn bytes(self: StringView) []const u8 {
|
||||
return self.inner.bytes()[0..self.length()];
|
||||
}
|
||||
};
|
||||
|
||||
const NoopInspector = struct {
|
||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: StringView) void {}
|
||||
pub fn onInspectorEvent(_: *anyopaque, _: StringView) void {}
|
||||
};
|
||||
};
|
||||
|
||||
pub const RemoteObject = v8.RemoteObject;
|
||||
@@ -3204,11 +3229,6 @@ fn stackForLogs(arena: Allocator, isolate: v8.Isolate) !?[]const u8 {
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
const NoopInspector = struct {
|
||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
||||
};
|
||||
|
||||
const ErrorModuleLoader = struct {
|
||||
pub fn fetchModuleSource(_: *anyopaque, _: []const u8) !?[]const u8 {
|
||||
return error.NoModuleLoadConfigured;
|
||||
|
||||
@@ -127,7 +127,6 @@ pub const Loop = struct {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// JS callbacks APIs
|
||||
// -----------------
|
||||
|
||||
@@ -255,7 +254,6 @@ pub const Loop = struct {
|
||||
}
|
||||
}.onConnect;
|
||||
|
||||
|
||||
const callback = try self.event_callback_pool.create();
|
||||
errdefer self.event_callback_pool.destroy(callback);
|
||||
callback.* = .{ .loop = self, .ctx = ctx };
|
||||
|
||||
Reference in New Issue
Block a user