Merge branch 'main' into cdp_struct

This commit is contained in:
Karl Seguin
2025-02-20 22:08:37 +08:00
10 changed files with 360 additions and 163 deletions

View File

@@ -267,7 +267,12 @@ pub const Page = struct {
// add global objects
log.debug("setup global env", .{});
try self.session.env.bindGlobal(&self.session.window);
if (comptime builtin.is_test == false) {
// By not loading this during tests, we aren't required to load
// all of the interfaces into zig-js-runtime.
try self.session.env.bindGlobal(&self.session.window);
}
// load polyfills
try polyfill.load(self.arena.allocator(), self.session.env);

View File

@@ -202,23 +202,13 @@ pub fn main() !void {
// allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the page allocator
var alloc: std.mem.Allocator = undefined;
var _gpa: ?std.heap.GeneralPurposeAllocator(.{}) = null;
if (builtin.mode == .Debug) {
_gpa = std.heap.GeneralPurposeAllocator(.{}){};
alloc = _gpa.?.allocator();
} else {
alloc = std.heap.page_allocator;
}
defer {
if (_gpa) |*gpa| {
switch (gpa.deinit()) {
.ok => std.debug.print("No memory leaks\n", .{}),
.leak => @panic("Memory leak"),
}
}
}
// - in Release mode we use the c allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) {
_ = gpa.detectLeaks();
};
// args
var args: std.process.ArgIterator = undefined;

View File

@@ -336,12 +336,6 @@ test {
std.testing.refAllDecls(queryTest);
std.testing.refAllDecls(@import("generate.zig"));
// Don't use refAllDecls, as this will pull in the entire project
// and break the test build.
// We should fix this. See this branch & the commit message for details:
// https://github.com/karlseguin/browser/commit/193ab5ceab3d3758ea06db04f7690460d79eb79e
_ = @import("server.zig");
}
fn testJSRuntime(alloc: std.mem.Allocator) !void {

View File

@@ -184,6 +184,13 @@ const Server = struct {
client.close(null);
return;
};
if (size == 0) {
if (self.client != null) {
self.client = null;
}
self.queueAccept();
return;
}
const more = client.processData(size) catch |err| {
log.err("Client Processing Error: {any}\n", .{err});
@@ -970,14 +977,6 @@ pub fn run(
timeout: u64,
loop: *jsruntime.Loop,
) !void {
if (comptime builtin.is_test) {
// There's bunch of code that won't compiler in a test build (because
// it relies on a global root.Types). So we fight the compiler and make
// sure it doesn't include any of that code. Hopefully one day we can
// remove all this.
return;
}
// create socket
const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK;
const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP);
@@ -1555,6 +1554,49 @@ test "server: mask" {
}
}
test "server: 404" {
var c = try createTestClient();
defer c.deinit();
const res = try c.httpRequest("GET /unknown HTTP/1.1\r\n\r\n");
try testing.expectEqualStrings("HTTP/1.1 404 \r\n" ++
"Connection: Close\r\n" ++
"Content-Length: 9\r\n\r\n" ++
"Not found", res);
}
test "server: get /json/version" {
const expected_response =
"HTTP/1.1 200 OK\r\n" ++
"Content-Length: 48\r\n" ++
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9583/\"}";
{
// twice on the same connection
var c = try createTestClient();
defer c.deinit();
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
try testing.expectEqualStrings(expected_response, res1);
const res2 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
try testing.expectEqualStrings(expected_response, res2);
}
{
// again on a new connection
var c = try createTestClient();
defer c.deinit();
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
try testing.expectEqualStrings(expected_response, res1);
const res2 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
try testing.expectEqualStrings(expected_response, res2);
}
}
fn assertHTTPError(
expected_error: anyerror,
comptime expected_status: u16,
@@ -1680,6 +1722,7 @@ const MockServer = struct {
}
};
const MockCDP = struct {
messages: std.ArrayListUnmanaged([]const u8) = .{},
@@ -1705,3 +1748,63 @@ const MockCDP = struct {
return true;
}
};
fn createTestClient() !TestClient {
const address = std.net.Address.initIp4([_]u8{ 127, 0, 0, 1 }, 9583);
const stream = try std.net.tcpConnectToAddress(address);
const timeout = std.mem.toBytes(posix.timeval{
.tv_sec = 2,
.tv_usec = 0,
});
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
return .{ .stream = stream };
}
const TestClient = struct {
stream: std.net.Stream,
buf: [1024]u8 = undefined,
fn deinit(self: *TestClient) void {
self.stream.close();
}
fn httpRequest(self: *TestClient, req: []const u8) ![]const u8 {
try self.stream.writeAll(req);
var pos: usize = 0;
var total_length: ?usize = null;
while (true) {
pos += try self.stream.read(self.buf[pos..]);
const response = self.buf[0..pos];
if (total_length == null) {
const header_end = std.mem.indexOf(u8, response, "\r\n\r\n") orelse continue;
const header = response[0 .. header_end + 4];
const cl_header = "Content-Length: ";
const start = (std.mem.indexOf(u8, header, cl_header) orelse {
return error.MissingContentLength;
}) + cl_header.len;
const end = std.mem.indexOfScalarPos(u8, header, start, '\r') orelse {
return error.InvalidContentLength;
};
const cl = std.fmt.parseInt(usize, header[start..end], 10) catch {
return error.InvalidContentLength;
};
total_length = cl + header.len;
}
if (total_length) |tl| {
if (pos == tl) {
return response;
}
if (pos > tl) {
return error.DataExceedsContentLength;
}
}
}
}
};

View File

@@ -18,10 +18,17 @@
const std = @import("std");
const builtin = @import("builtin");
const parser = @import("netsurf");
const Allocator = std.mem.Allocator;
const jsruntime = @import("jsruntime");
pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){});
pub const UserContext = @import("user_context.zig").UserContext;
// pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
pub const std_options = std.Options{
.log_level = .err,
.http_disable_tls = true,
};
@@ -31,10 +38,14 @@ const BORDER = "=" ** 80;
var current_test: ?[]const u8 = null;
pub fn main() !void {
var mem: [8192]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&mem);
try parser.init();
defer parser.deinit();
const allocator = fba.allocator();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var loop = try jsruntime.Loop.init(allocator);
defer loop.deinit();
const env = Env.init(allocator);
defer env.deinit(allocator);
@@ -47,12 +58,20 @@ pub fn main() !void {
var skip: usize = 0;
var leak: usize = 0;
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
var listener = try address.listen(.{ .reuse_address = true });
defer listener.deinit();
const http_thread = try std.Thread.spawn(.{}, serverHTTP, .{&listener});
const http_thread = blk: {
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
const thread = try std.Thread.spawn(.{}, serveHTTP, .{address});
break :blk thread;
};
defer http_thread.join();
const cdp_thread = blk: {
const address = try std.net.Address.parseIp("127.0.0.1", 9583);
const thread = try std.Thread.spawn(.{}, serveCDP, .{ allocator, address, &loop });
break :blk thread;
};
defer cdp_thread.join();
const printer = Printer.init();
printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
@@ -98,7 +117,9 @@ pub fn main() !void {
}
if (result) |_| {
pass += 1;
if (is_unnamed_test == false) {
pass += 1;
}
} else |err| switch (err) {
error.SkipZigTest => {
skip += 1;
@@ -117,11 +138,13 @@ pub fn main() !void {
},
}
if (env.verbose) {
const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
} else {
printer.status(status, ".", .{});
if (is_unnamed_test == false) {
if (env.verbose) {
const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
} else {
printer.status(status, ".", .{});
}
}
}
@@ -294,7 +317,10 @@ fn isUnnamed(t: std.builtin.TestFn) bool {
return true;
}
fn serverHTTP(listener: *std.net.Server) !void {
fn serveHTTP(address: std.net.Address) !void {
var listener = try address.listen(.{ .reuse_address = true });
defer listener.deinit();
var read_buffer: [1024]u8 = undefined;
ACCEPT: while (true) {
var conn = try listener.accept();
@@ -320,6 +346,14 @@ fn serverHTTP(listener: *std.net.Server) !void {
}
}
fn serveCDP(allocator: Allocator, address: std.net.Address, loop: *jsruntime.Loop) !void {
const server = @import("server.zig");
server.run(allocator, address, std.time.ns_per_s * 2, loop) catch |err| {
std.debug.print("CDP server error: {}", .{err});
return err;
};
}
const Response = struct {
body: []const u8 = "",
status: std.http.Status = .ok,

View File

@@ -103,7 +103,7 @@ pub const XMLHttpRequest = struct {
ctx: ?Client.Ctx = null,
method: std.http.Method,
state: u16,
state: State,
url: ?[]const u8,
uri: std.Uri,
// request headers
@@ -150,11 +150,13 @@ pub const XMLHttpRequest = struct {
pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
pub const UNSENT: u16 = 0;
pub const OPENED: u16 = 1;
pub const HEADERS_RECEIVED: u16 = 2;
pub const LOADING: u16 = 3;
pub const DONE: u16 = 4;
const State = enum(u16) {
unsent = 0,
opened = 1,
headers_received = 2,
loading = 3,
done = 4,
};
// https://xhr.spec.whatwg.org/#response-type
const ResponseType = enum {
@@ -297,7 +299,7 @@ pub const XMLHttpRequest = struct {
.method = undefined,
.url = null,
.uri = undefined,
.state = UNSENT,
.state = .unsent,
.cli = userctx.httpClient,
};
}
@@ -347,7 +349,7 @@ pub const XMLHttpRequest = struct {
}
pub fn get_readyState(self: *XMLHttpRequest) u16 {
return self.state;
return @intFromEnum(self.state);
}
pub fn get_timeout(_: *XMLHttpRequest) u32 {
@@ -367,7 +369,7 @@ pub const XMLHttpRequest = struct {
}
pub fn set_withCredentials(self: *XMLHttpRequest, withCredentials: bool) !void {
if (self.state != OPENED and self.state != UNSENT) return DOMError.InvalidState;
if (self.state != .opened and self.state != .unsent) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
self.withCredentials = withCredentials;
@@ -401,7 +403,7 @@ pub const XMLHttpRequest = struct {
log.debug("open url ({s})", .{self.url.?});
self.sync = if (asyn) |b| !b else false;
self.state = OPENED;
self.state = .opened;
self.dispatchEvt("readystatechange");
}
@@ -477,14 +479,14 @@ pub const XMLHttpRequest = struct {
}
pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void {
if (self.state != OPENED) return DOMError.InvalidState;
if (self.state != .opened) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
return try self.headers.append(name, value);
}
// TODO body can be either a XMLHttpRequestBodyInit or a document
pub fn _send(self: *XMLHttpRequest, alloc: std.mem.Allocator, body: ?[]const u8) !void {
if (self.state != OPENED) return DOMError.InvalidState;
if (self.state != .opened) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
// The body argument provides the request body, if any, and is ignored
@@ -554,7 +556,7 @@ pub const XMLHttpRequest = struct {
// TODO handle override mime type
self.state = HEADERS_RECEIVED;
self.state = .headers_received;
self.dispatchEvt("readystatechange");
self.response_status = @intFromEnum(self.req.?.response.status);
@@ -592,7 +594,7 @@ pub const XMLHttpRequest = struct {
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = LOADING;
self.state = .loading;
self.dispatchEvt("readystatechange");
// dispatch a progress event progress.
@@ -604,7 +606,7 @@ pub const XMLHttpRequest = struct {
self.response_bytes = buf.items;
self.send_flag = false;
self.state = DONE;
self.state = .done;
self.dispatchEvt("readystatechange");
// dispatch a progress event load.
@@ -666,7 +668,7 @@ pub const XMLHttpRequest = struct {
self.priv_state = .done;
self.err = err;
self.state = DONE;
self.state = .done;
self.send_flag = false;
self.dispatchEvt("readystatechange");
self.dispatchProgressEvent("error", .{});
@@ -697,7 +699,7 @@ pub const XMLHttpRequest = struct {
}
pub fn set_responseType(self: *XMLHttpRequest, rtype: []const u8) !void {
if (self.state == LOADING or self.state == DONE) return DOMError.InvalidState;
if (self.state == .loading or self.state == .done) return DOMError.InvalidState;
if (std.mem.eql(u8, rtype, "")) {
self.response_type = .Empty;
@@ -735,7 +737,7 @@ pub const XMLHttpRequest = struct {
return DOMError.InvalidState;
}
if (self.state != DONE) return null;
if (self.state != .done) return null;
// fastpath if response is previously parsed.
if (self.response_obj) |obj| {
@@ -761,7 +763,7 @@ pub const XMLHttpRequest = struct {
// https://xhr.spec.whatwg.org/#the-response-attribute
pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
if (self.response_type == .Empty or self.response_type == .Text) {
if (self.state == LOADING or self.state == DONE) {
if (self.state == .loading or self.state == .done) {
return .{ .Text = try self.get_responseText() };
}
return .{ .Text = "" };