A maildir and Maildir++ library for Zig 0.16: delivery, flags, folders and quota.
0

Configure Feed

Select the types of activity you want to include in your feed.

zig-maildir / tools / docs_server.zig
7.2 kB 171 lines
1// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us> 2// SPDX-License-Identifier: MIT 3 4//! Serves the generated API documentation over HTTP, the way `zig std` serves 5//! the standard library's. 6//! 7//! A server is needed rather than just opening `index.html`, because the 8//! viewer fetches `sources.tar` and `main.wasm` at runtime and a browser 9//! refuses those requests from a `file://` page. 10//! 11//! Run through the build system: `zig build docs-serve`. It is deliberately 12//! minimal, serving one directory to one person on the loopback interface — 13//! it is a convenience for reading `zig build docs`, not a web server, and 14//! nothing about it should be pointed at a network. 15//! 16//! It is worth noting what this is *not*: this repository implements a file 17//! transfer protocol, and none of that is used here. HTTP is what a browser 18//! speaks, so HTTP is what this speaks. 19//! 20//! Every connection gets its own thread, which is not a throughput concern but 21//! a correctness one: a browser opens several connections at once and holds 22//! some of them open without sending anything, so a server that reads them one 23//! at a time blocks on a speculative connection and never answers the real 24//! requests. 25//! 26//! A thread each, rather than a pool: a connection handler blocks until its 27//! client goes away, which can be minutes, and a pool sized for short tasks 28//! wedges once every worker is parked on an idle socket. A browser opens a 29//! handful of connections, so the thread count stays small in practice. 30 31const std = @import("std"); 32const Io = std.Io; 33 34/// Nothing in a documentation bundle comes close to this; it exists so that a 35/// stray huge file cannot exhaust memory. 36const max_file_size = 64 * 1024 * 1024; 37 38pub fn main(init: std.process.Init) !void { 39 const gpa = init.gpa; 40 const io = init.io; 41 const args = try init.minimal.args.toSlice(init.arena.allocator()); 42 43 var stderr_buffer: [512]u8 = undefined; 44 var stderr_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buffer); 45 const stderr = &stderr_writer.interface; 46 47 if (args.len != 3) { 48 try stderr.writeAll("usage: docs-server <directory> <port>\n"); 49 try stderr.flush(); 50 std.process.exit(2); 51 } 52 const docs_path = args[1]; 53 const port = try std.fmt.parseInt(u16, args[2], 10); 54 55 var docs_dir = Io.Dir.cwd().openDir(io, docs_path, .{}) catch |err| { 56 try stderr.print("cannot open {s}: {s}\n", .{ docs_path, @errorName(err) }); 57 try stderr.flush(); 58 std.process.exit(1); 59 }; 60 defer docs_dir.close(io); 61 62 const address: Io.net.IpAddress = .{ .ip4 = .{ .bytes = .{ 127, 0, 0, 1 }, .port = port } }; 63 var server = address.listen(io, .{ .reuse_address = true }) catch |err| { 64 try stderr.print("cannot listen on 127.0.0.1:{d}: {s}\n", .{ port, @errorName(err) }); 65 if (err == error.AddressInUse) { 66 try stderr.writeAll("another port can be chosen with -Ddocs-port=N\n"); 67 } 68 try stderr.flush(); 69 std.process.exit(1); 70 }; 71 defer server.deinit(io); 72 73 try stderr.print("serving {s} at http://127.0.0.1:{d}/\npress ctrl-c to stop\n", .{ docs_path, port }); 74 try stderr.flush(); 75 76 while (true) { 77 const stream = server.accept(io) catch |err| switch (err) { 78 // One client giving up is not a reason to stop serving. 79 error.ConnectionAborted, error.WouldBlock, error.ProtocolFailure => continue, 80 else => return err, 81 }; 82 83 const thread = std.Thread.spawn(.{}, handleConnection, .{ io, gpa, docs_dir, stream }) catch { 84 // Out of threads. Serving it here blocks the ones behind it, but 85 // dropping it silently would look like the same hang from the 86 // browser's side with none of the progress. 87 handleConnection(io, gpa, docs_dir, stream); 88 continue; 89 }; 90 // Nothing joins these: each ends when its client disconnects, and the 91 // process runs until interrupted. 92 thread.detach(); 93 } 94} 95 96/// Serves one connection until the client goes away, then closes it. 97fn handleConnection(io: Io, gpa: std.mem.Allocator, docs_dir: Io.Dir, stream: Io.net.Stream) void { 98 defer stream.close(io); 99 100 var recv_buffer: [16 * 1024]u8 = undefined; 101 var send_buffer: [64 * 1024]u8 = undefined; 102 var stream_reader = stream.reader(io, &recv_buffer); 103 var stream_writer = stream.writer(io, &send_buffer); 104 var http_server: std.http.Server = .init(&stream_reader.interface, &stream_writer.interface); 105 106 // Keep answering on the same connection, so that one page load does not 107 // need a connection per file. 108 while (true) { 109 var request = http_server.receiveHead() catch return; 110 serve(&request, io, gpa, docs_dir) catch return; 111 // A client that asked to close is waiting for exactly that; waiting 112 // for another request it will never send would hang it until it times 113 // out. 114 if (!request.head.keep_alive) return; 115 } 116} 117 118fn serve( 119 request: *std.http.Server.Request, 120 io: Io, 121 gpa: std.mem.Allocator, 122 docs_dir: Io.Dir, 123) !void { 124 const target = request.head.target; 125 const path_end = std.mem.indexOfAny(u8, target, "?#") orelse target.len; 126 var path = target[0..path_end]; 127 if (std.mem.startsWith(u8, path, "/")) path = path[1..]; 128 if (path.len == 0) path = "index.html"; 129 130 // The documentation directory is the whole world this server knows about. 131 if (std.mem.indexOf(u8, path, "..") != null or std.fs.path.isAbsolute(path)) { 132 return request.respond("bad request\n", .{ .status = .bad_request }); 133 } 134 135 const content = docs_dir.readFileAlloc(io, path, gpa, .limited(max_file_size)) catch { 136 return request.respond("not found\n", .{ .status = .not_found }); 137 }; 138 defer gpa.free(content); 139 140 try request.respond(content, .{ 141 .extra_headers = &.{.{ .name = "content-type", .value = mimeType(path) }}, 142 }); 143} 144 145/// Enough of a MIME table for what `zig build docs` emits. 146fn mimeType(path: []const u8) []const u8 { 147 const extension = std.fs.path.extension(path); 148 const table = [_]struct { []const u8, []const u8 }{ 149 .{ ".html", "text/html; charset=utf-8" }, 150 .{ ".js", "text/javascript; charset=utf-8" }, 151 .{ ".css", "text/css; charset=utf-8" }, 152 .{ ".wasm", "application/wasm" }, 153 .{ ".tar", "application/x-tar" }, 154 .{ ".json", "application/json" }, 155 .{ ".svg", "image/svg+xml" }, 156 }; 157 for (table) |entry| { 158 if (std.mem.eql(u8, extension, entry[0])) return entry[1]; 159 } 160 return "application/octet-stream"; 161} 162 163const testing = std.testing; 164 165test mimeType { 166 try testing.expectEqualStrings("text/html; charset=utf-8", mimeType("index.html")); 167 try testing.expectEqualStrings("application/wasm", mimeType("main.wasm")); 168 try testing.expectEqualStrings("application/x-tar", mimeType("sources.tar")); 169 try testing.expectEqualStrings("text/javascript; charset=utf-8", mimeType("main.js")); 170 try testing.expectEqualStrings("application/octet-stream", mimeType("noextension")); 171}