Skip to content

Commit a2932f0

Browse files
Merge pull request #435 from karlseguin/server_tests
Fix server hang on client disconnect
2 parents 5d4efb7 + 39a9efb commit a2932f0

File tree

4 files changed

+163
-27
lines changed

4 files changed

+163
-27
lines changed

src/browser/browser.zig

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,12 @@ pub const Page = struct {
242242

243243
// add global objects
244244
log.debug("setup global env", .{});
245-
try self.session.env.bindGlobal(&self.session.window);
245+
246+
if (comptime builtin.is_test == false) {
247+
// By not loading this during tests, we aren't required to load
248+
// all of the interfaces into zig-js-runtime.
249+
try self.session.env.bindGlobal(&self.session.window);
250+
}
246251

247252
// load polyfills
248253
try polyfill.load(self.arena.allocator(), self.session.env);

src/main_tests.zig

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,12 +337,6 @@ test {
337337

338338
std.testing.refAllDecls(@import("generate.zig"));
339339
std.testing.refAllDecls(@import("cdp/msg.zig"));
340-
341-
// Don't use refAllDecls, as this will pull in the entire project
342-
// and break the test build.
343-
// We should fix this. See this branch & the commit message for details:
344-
// https://github.com/karlseguin/browser/commit/193ab5ceab3d3758ea06db04f7690460d79eb79e
345-
_ = @import("server.zig");
346340
}
347341

348342
fn testJSRuntime(alloc: std.mem.Allocator) !void {

src/server.zig

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,13 @@ const Server = struct {
211211
self.queueClose(client.socket);
212212
return;
213213
};
214+
if (size == 0) {
215+
if (self.client != null) {
216+
self.client = null;
217+
}
218+
self.queueAccept();
219+
return;
220+
}
214221

215222
const more = client.processData(size) catch |err| {
216223
log.err("Client Processing Error: {}\n", .{err});
@@ -1053,14 +1060,6 @@ pub fn run(
10531060
timeout: u64,
10541061
loop: *jsruntime.Loop,
10551062
) !void {
1056-
if (comptime builtin.is_test) {
1057-
// There's bunch of code that won't compiler in a test build (because
1058-
// it relies on a global root.Types). So we fight the compiler and make
1059-
// sure it doesn't include any of that code. Hopefully one day we can
1060-
// remove all this.
1061-
return;
1062-
}
1063-
10641063
// create socket
10651064
const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK;
10661065
const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP);
@@ -1631,6 +1630,49 @@ test "server: mask" {
16311630
}
16321631
}
16331632

1633+
test "server: 404" {
1634+
var c = try createTestClient();
1635+
defer c.deinit();
1636+
1637+
const res = try c.httpRequest("GET /unknown HTTP/1.1\r\n\r\n");
1638+
try testing.expectEqualStrings("HTTP/1.1 404 \r\n" ++
1639+
"Connection: Close\r\n" ++
1640+
"Content-Length: 9\r\n\r\n" ++
1641+
"Not found", res);
1642+
}
1643+
1644+
test "server: get /json/version" {
1645+
const expected_response =
1646+
"HTTP/1.1 200 OK\r\n" ++
1647+
"Content-Length: 48\r\n" ++
1648+
"Content-Type: application/json; charset=UTF-8\r\n\r\n" ++
1649+
"{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9583/\"}";
1650+
1651+
{
1652+
// twice on the same connection
1653+
var c = try createTestClient();
1654+
defer c.deinit();
1655+
1656+
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
1657+
try testing.expectEqualStrings(expected_response, res1);
1658+
1659+
const res2 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
1660+
try testing.expectEqualStrings(expected_response, res2);
1661+
}
1662+
1663+
{
1664+
// again on a new connection
1665+
var c = try createTestClient();
1666+
defer c.deinit();
1667+
1668+
const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
1669+
try testing.expectEqualStrings(expected_response, res1);
1670+
1671+
const res2 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n");
1672+
try testing.expectEqualStrings(expected_response, res2);
1673+
}
1674+
}
1675+
16341676
fn assertHTTPError(
16351677
expected_error: HTTPError,
16361678
comptime expected_status: u16,
@@ -1762,3 +1804,63 @@ const MockServer = struct {
17621804
}
17631805
}
17641806
};
1807+
1808+
fn createTestClient() !TestClient {
1809+
const address = std.net.Address.initIp4([_]u8{ 127, 0, 0, 1 }, 9583);
1810+
const stream = try std.net.tcpConnectToAddress(address);
1811+
1812+
const timeout = std.mem.toBytes(posix.timeval{
1813+
.tv_sec = 2,
1814+
.tv_usec = 0,
1815+
});
1816+
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
1817+
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
1818+
return .{ .stream = stream };
1819+
}
1820+
1821+
const TestClient = struct {
1822+
stream: std.net.Stream,
1823+
buf: [1024]u8 = undefined,
1824+
1825+
fn deinit(self: *TestClient) void {
1826+
self.stream.close();
1827+
}
1828+
1829+
fn httpRequest(self: *TestClient, req: []const u8) ![]const u8 {
1830+
try self.stream.writeAll(req);
1831+
1832+
var pos: usize = 0;
1833+
var total_length: ?usize = null;
1834+
while (true) {
1835+
pos += try self.stream.read(self.buf[pos..]);
1836+
const response = self.buf[0..pos];
1837+
if (total_length == null) {
1838+
const header_end = std.mem.indexOf(u8, response, "\r\n\r\n") orelse continue;
1839+
const header = response[0 .. header_end + 4];
1840+
1841+
const cl_header = "Content-Length: ";
1842+
const start = (std.mem.indexOf(u8, header, cl_header) orelse {
1843+
return error.MissingContentLength;
1844+
}) + cl_header.len;
1845+
1846+
const end = std.mem.indexOfScalarPos(u8, header, start, '\r') orelse {
1847+
return error.InvalidContentLength;
1848+
};
1849+
const cl = std.fmt.parseInt(usize, header[start..end], 10) catch {
1850+
return error.InvalidContentLength;
1851+
};
1852+
1853+
total_length = cl + header.len;
1854+
}
1855+
1856+
if (total_length) |tl| {
1857+
if (pos == tl) {
1858+
return response;
1859+
}
1860+
if (pos > tl) {
1861+
return error.DataExceedsContentLength;
1862+
}
1863+
}
1864+
}
1865+
}
1866+
};

src/unit_tests.zig

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,17 @@
1818

1919
const std = @import("std");
2020
const builtin = @import("builtin");
21+
const parser = @import("netsurf");
2122

2223
const Allocator = std.mem.Allocator;
2324

25+
const jsruntime = @import("jsruntime");
26+
pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){});
27+
pub const UserContext = @import("user_context.zig").UserContext;
28+
// pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
29+
2430
pub const std_options = std.Options{
31+
.log_level = .err,
2532
.http_disable_tls = true,
2633
};
2734

@@ -31,11 +38,16 @@ const BORDER = "=" ** 80;
3138
var current_test: ?[]const u8 = null;
3239

3340
pub fn main() !void {
41+
try parser.init();
42+
defer parser.deinit();
43+
3444
var mem: [8192]u8 = undefined;
3545
var fba = std.heap.FixedBufferAllocator.init(&mem);
36-
3746
const allocator = fba.allocator();
3847

48+
var loop = try jsruntime.Loop.init(allocator);
49+
defer loop.deinit();
50+
3951
const env = Env.init(allocator);
4052
defer env.deinit(allocator);
4153

@@ -47,12 +59,20 @@ pub fn main() !void {
4759
var skip: usize = 0;
4860
var leak: usize = 0;
4961

50-
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
51-
var listener = try address.listen(.{ .reuse_address = true });
52-
defer listener.deinit();
53-
const http_thread = try std.Thread.spawn(.{}, serverHTTP, .{&listener});
62+
const http_thread = blk: {
63+
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
64+
const thread = try std.Thread.spawn(.{}, serveHTTP, .{address});
65+
break :blk thread;
66+
};
5467
defer http_thread.join();
5568

69+
const cdp_thread = blk: {
70+
const address = try std.net.Address.parseIp("127.0.0.1", 9583);
71+
const thread = try std.Thread.spawn(.{}, serveCDP, .{ allocator, address, &loop });
72+
break :blk thread;
73+
};
74+
defer cdp_thread.join();
75+
5676
const printer = Printer.init();
5777
printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
5878

@@ -98,7 +118,9 @@ pub fn main() !void {
98118
}
99119

100120
if (result) |_| {
101-
pass += 1;
121+
if (is_unnamed_test == false) {
122+
pass += 1;
123+
}
102124
} else |err| switch (err) {
103125
error.SkipZigTest => {
104126
skip += 1;
@@ -117,11 +139,13 @@ pub fn main() !void {
117139
},
118140
}
119141

120-
if (env.verbose) {
121-
const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
122-
printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
123-
} else {
124-
printer.status(status, ".", .{});
142+
if (is_unnamed_test == false) {
143+
if (env.verbose) {
144+
const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
145+
printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
146+
} else {
147+
printer.status(status, ".", .{});
148+
}
125149
}
126150
}
127151

@@ -294,7 +318,10 @@ fn isUnnamed(t: std.builtin.TestFn) bool {
294318
return true;
295319
}
296320

297-
fn serverHTTP(listener: *std.net.Server) !void {
321+
fn serveHTTP(address: std.net.Address) !void {
322+
var listener = try address.listen(.{ .reuse_address = true });
323+
defer listener.deinit();
324+
298325
var read_buffer: [1024]u8 = undefined;
299326
ACCEPT: while (true) {
300327
var conn = try listener.accept();
@@ -320,6 +347,14 @@ fn serverHTTP(listener: *std.net.Server) !void {
320347
}
321348
}
322349

350+
fn serveCDP(allocator: Allocator, address: std.net.Address, loop: *jsruntime.Loop) !void {
351+
const server = @import("server.zig");
352+
server.run(allocator, address, std.time.ns_per_s * 2, loop) catch |err| {
353+
std.debug.print("CDP server error: {}", .{err});
354+
return err;
355+
};
356+
}
357+
323358
const Response = struct {
324359
body: []const u8 = "",
325360
status: std.http.Status = .ok,

0 commit comments

Comments
 (0)