Something went wrong. Try again.
A Wayland client for Zig 0.16 with no libwayland: protocol scanner, sans-I/O wire protocol, and socket client.
Something went wrong. Try again.
7.2 kB · 171 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Serves the generated API documentation over HTTP, the way `zig std` serves//! the standard library's.//!//! A server is needed rather than just opening `index.html`, because the//! viewer fetches `sources.tar` and `main.wasm` at runtime and a browser//! refuses those requests from a `file://` page.//!//! Run through the build system: `zig build docs-serve`. It is deliberately//! minimal, serving one directory to one person on the loopback interface —//! it is a convenience for reading `zig build docs`, not a web server, and//! nothing about it should be pointed at a network.//!//! It is worth noting what this is *not*: this repository implements a file//! transfer protocol, and none of that is used here. HTTP is what a browser//! speaks, so HTTP is what this speaks.//!//! Every connection gets its own thread, which is not a throughput concern but//! a correctness one: a browser opens several connections at once and holds//! some of them open without sending anything, so a server that reads them one//! at a time blocks on a speculative connection and never answers the real//! requests.//!//! A thread each, rather than a pool: a connection handler blocks until its//! client goes away, which can be minutes, and a pool sized for short tasks//! wedges once every worker is parked on an idle socket. A browser opens a//! handful of connections, so the thread count stays small in practice.
const std = @import("std");const Io = std.Io;
/// Nothing in a documentation bundle comes close to this; it exists so that a/// stray huge file cannot exhaust memory.const max_file_size = 64 * 1024 * 1024;
pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const io = init.io; const args = try init.minimal.args.toSlice(init.arena.allocator());
var stderr_buffer: [512]u8 = undefined; var stderr_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buffer); const stderr = &stderr_writer.interface;
if (args.len != 3) { try stderr.writeAll("usage: docs-server <directory> <port>\n"); try stderr.flush(); std.process.exit(2); } const docs_path = args[1]; const port = try std.fmt.parseInt(u16, args[2], 10);
var docs_dir = Io.Dir.cwd().openDir(io, docs_path, .{}) catch |err| { try stderr.print("cannot open {s}: {s}\n", .{ docs_path, @errorName(err) }); try stderr.flush(); std.process.exit(1); }; defer docs_dir.close(io);
const address: Io.net.IpAddress = .{ .ip4 = .{ .bytes = .{ 127, 0, 0, 1 }, .port = port } }; var server = address.listen(io, .{ .reuse_address = true }) catch |err| { try stderr.print("cannot listen on 127.0.0.1:{d}: {s}\n", .{ port, @errorName(err) }); if (err == error.AddressInUse) { try stderr.writeAll("another port can be chosen with -Ddocs-port=N\n"); } try stderr.flush(); std.process.exit(1); }; defer server.deinit(io);
try stderr.print("serving {s} at http://127.0.0.1:{d}/\npress ctrl-c to stop\n", .{ docs_path, port }); try stderr.flush();
while (true) { const stream = server.accept(io) catch |err| switch (err) { // One client giving up is not a reason to stop serving. error.ConnectionAborted, error.WouldBlock, error.ProtocolFailure => continue, else => return err, };
const thread = std.Thread.spawn(.{}, handleConnection, .{ io, gpa, docs_dir, stream }) catch { // Out of threads. Serving it here blocks the ones behind it, but // dropping it silently would look like the same hang from the // browser's side with none of the progress. handleConnection(io, gpa, docs_dir, stream); continue; }; // Nothing joins these: each ends when its client disconnects, and the // process runs until interrupted. thread.detach(); }}
/// Serves one connection until the client goes away, then closes it.fn handleConnection(io: Io, gpa: std.mem.Allocator, docs_dir: Io.Dir, stream: Io.net.Stream) void { defer stream.close(io);
var recv_buffer: [16 * 1024]u8 = undefined; var send_buffer: [64 * 1024]u8 = undefined; var stream_reader = stream.reader(io, &recv_buffer); var stream_writer = stream.writer(io, &send_buffer); var http_server: std.http.Server = .init(&stream_reader.interface, &stream_writer.interface);
// Keep answering on the same connection, so that one page load does not // need a connection per file. while (true) { var request = http_server.receiveHead() catch return; serve(&request, io, gpa, docs_dir) catch return; // A client that asked to close is waiting for exactly that; waiting // for another request it will never send would hang it until it times // out. if (!request.head.keep_alive) return; }}
fn serve( request: *std.http.Server.Request, io: Io, gpa: std.mem.Allocator, docs_dir: Io.Dir,) !void { const target = request.head.target; const path_end = std.mem.indexOfAny(u8, target, "?#") orelse target.len; var path = target[0..path_end]; if (std.mem.startsWith(u8, path, "/")) path = path[1..]; if (path.len == 0) path = "index.html";
// The documentation directory is the whole world this server knows about. if (std.mem.indexOf(u8, path, "..") != null or std.fs.path.isAbsolute(path)) { return request.respond("bad request\n", .{ .status = .bad_request }); }
const content = docs_dir.readFileAlloc(io, path, gpa, .limited(max_file_size)) catch { return request.respond("not found\n", .{ .status = .not_found }); }; defer gpa.free(content);
try request.respond(content, .{ .extra_headers = &.{.{ .name = "content-type", .value = mimeType(path) }}, });}
/// Enough of a MIME table for what `zig build docs` emits.fn mimeType(path: []const u8) []const u8 { const extension = std.fs.path.extension(path); const table = [_]struct { []const u8, []const u8 }{ .{ ".html", "text/html; charset=utf-8" }, .{ ".js", "text/javascript; charset=utf-8" }, .{ ".css", "text/css; charset=utf-8" }, .{ ".wasm", "application/wasm" }, .{ ".tar", "application/x-tar" }, .{ ".json", "application/json" }, .{ ".svg", "image/svg+xml" }, }; for (table) |entry| { if (std.mem.eql(u8, extension, entry[0])) return entry[1]; } return "application/octet-stream";}
const testing = std.testing;
test mimeType { try testing.expectEqualStrings("text/html; charset=utf-8", mimeType("index.html")); try testing.expectEqualStrings("application/wasm", mimeType("main.wasm")); try testing.expectEqualStrings("application/x-tar", mimeType("sources.tar")); try testing.expectEqualStrings("text/javascript; charset=utf-8", mimeType("main.js")); try testing.expectEqualStrings("application/octet-stream", mimeType("noextension"));}