From b50102af01d07154b8bf59571596722684dc5183 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Sat, 2 Mar 2024 21:45:34 +0000 Subject: More work on self-hosted server --- src/main.zig | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/main.zig b/src/main.zig index 96dafc9..f9f204b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,29 +18,37 @@ const log = std.log.scoped(.server); // - and maybe even cgit (although I might scrap it in favour of stagit if I can figure out archive downloads // - do something about efficiency :) it's thread-per-request right now pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{ - .thread_safe = true, - }){}; - defer { - log.info("deinit gpa", .{}); - _ = gpa.deinit(); - } + var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + defer _ = gpa.deinit(); const a = gpa.allocator(); - var pool: std.Thread.Pool = undefined; try std.Thread.Pool.init(&pool, .{ .allocator = a, .n_jobs = 32 }); defer pool.deinit(); - var svr = std.http.Server.init(.{ .reuse_address = true, .reuse_port = true }); - const addr = try std.net.Address.parseIp("0.0.0.0", 8081); + defer svr.deinit(); + const port = 8081; + const addr = std.net.Address.initIp4(.{0,0,0,0}, port); try svr.listen(addr); log.info("listening on {}", .{addr}); - while (true) { + try acceptLoop(a, &svr, &pool, struct{ + fn cancelled() bool { + return false; + } + }); +} + +fn acceptLoop(a: std.mem.Allocator, svr: *std.http.Server, pool: *std.Thread.Pool, cancel: anytype) !void { + while (!cancel.cancelled()) { // Create the Response into the heap so that the ownership goes to the handling thread const res = try a.create(std.http.Server.Response); errdefer a.destroy(res); res.* = try svr.accept(.{ .allocator = a }); errdefer res.deinit(); + if (cancel.cancelled()) { + res.deinit(); + a.destroy(res); + break; + } try pool.spawn(handle, .{res}); } } @@ -201,6 +209,7 @@ fn serveStatic(res: *std.http.Server.Response, dirname: []const u8) !void { }; var requested_path = uri.path; requested_path = try std.fs.path.join(res.allocator, &.{ dirpath, requested_path }); + defer res.allocator.free(requested_path); const path = std.fs.realpathAlloc(res.allocator, requested_path) catch |e| { @@ -340,3 +349,83 @@ fn sendError(res: *std.http.Server.Response, e: anyerror) !void { }, } } + +const Fixture = struct { + a: std.mem.Allocator, + port: u16, + t: std.Thread, + cancel: std.atomic.Value(bool), + base_url: []const u8, + thread_pool: std.Thread.Pool, + server: std.http.Server, + client: std.http.Client, + + fn init(a: std.mem.Allocator) !*Fixture { + var res: *Fixture = try a.create(Fixture); + errdefer a.destroy(res); + res.a = a; + try std.Thread.Pool.init(&res.thread_pool, .{ .allocator = a, .n_jobs = 32 }); + errdefer res.thread_pool.deinit(); + res.server = std.http.Server.init(.{ .reuse_address = true, .reuse_port = true }); + errdefer res.server.deinit(); + const addr = std.net.Address.initIp4(.{127,0,0,1}, 0); + try res.server.listen(addr); + res.port = res.server.socket.listen_address.in.getPort(); + res.base_url = try std.fmt.allocPrint(a, "http://localhost:{}", .{res.port}); + errdefer a.free(res.base_url); + res.cancel = std.atomic.Value(bool).init(false); + res.t = try std.Thread.spawn(.{}, acceptLoop, .{a, &res.server, &res.thread_pool, res}); + res.client = .{.allocator = a}; + return res; + } + + fn deinit(self: *Fixture) void { + self.client.deinit(); + self.a.free(self.base_url); + self.cancel.store(true, .Unordered); + // Trigger the server's accept() method to wake it up + if (std.net.tcpConnectToAddress(std.net.Address.initIp4(.{127,0,0,1}, self.port))) |stream| { + stream.close(); + } else |_| {} + self.t.join(); + self.thread_pool.deinit(); + self.server.deinit(); + self.a.destroy(self); + } + + fn cancelled(self: *Fixture) bool { + return self.cancel.load(.Unordered); + } +}; + + +test "static pages" { + const a = std.testing.allocator; + var f = try Fixture.init(a); + defer f.deinit(); + const TestCase = struct { + path: []const u8, + file: []const u8, + }; + const test_cases = [_]TestCase{ + .{ .path = "/", .file = "public/index.html"}, + .{ .path = "/posts/2018-05-31-new-site/", .file = "public/posts/2018-05-31-new-site/index.html"}, + }; + + for (test_cases) |test_case| { + const url = try std.fmt.allocPrint(a, "{s}{s}", .{f.base_url, test_case.path}); + defer a.free(url); + var fr = try f.client.fetch(a, .{ + .location = .{.url = url} + }); + defer fr.deinit(); + try std.testing.expectEqual(std.http.Status.ok, fr.status); + const expected = try std.fs.cwd().readFileAlloc(a, test_case.file, 1_000_000); + defer a.free(expected); + try std.testing.expectEqualStrings(expected, fr.body.?); + } +} + +// test "404" { + +// } \ No newline at end of file -- cgit v1.2.3-ZIG