tiny.game.transport
Defined in tiny.game.
Two servers carry lines holding one command each from an outside tool to a host, over standard input or a Unix socket, and carry each reply back.
API (18)
Actions
Public operations.
CommandScratch.apply: Resets the arena and passes it,lineandouttocontrol.applyLine, returning that call's result.CommandScratch.deinit: Frees the arena's memory and leaves the scratch undefined.CommandScratch.init: Returns a scratch whose arena draws memory fromallocator.CommandWorkspace.apply: Clears the reply buffer, then hands the control object the reset arena, the line and a writer into the buffer.CommandWorkspace.deinit: Frees the reply buffer and the arena and leaves the workspace undefined.CommandWorkspace.init: Returns a workspace whose arena and reply buffer draw memory fromallocator.CommandWorkspace.written: Returns the reply that the lastapplywrote.LineBuffer.consume: Drops the first line and its newline, and moves the bytes after it to the front.LineBuffer.deinit: Frees the held bytes and leaves the buffer undefined.LineBuffer.feed: Appendsbytesto the held bytes.LineBuffer.init: Returns an empty buffer that grows withallocator.LineBuffer.next: Returns the first whole line, trimmed of spaces, tabs and carriage returns, ornullwhen the held bytes hold no newline.serveSocket: Serves lines tocontrolon a Unix socket atpath, one connection at a time, until the control object returns false.serveStdio: Reads lines from standard input and hands each one tocontroluntil the control object returns false or the input ends.
Types and contracts
Public types and contracts.
CommandScratch: An arena that a control object gets for each line, reset before the next line.CommandWorkspace: A scratch arena and a growing reply buffer for one connection, which keep a control object's reply in memory until the server sends it.LineBuffer: Bytes read from a connection and held until they are consumed, from which whole lines are taken in order.
Values and defaults
Public values and defaults.
line_capacity: The most bytes one line may hold, 1 MiB.
Source
Source: fun/game/src/root.zig:121
zig
pub const transport = @import("transport.zig");Source: fun/game/src/transport.zig
zig
//! Two servers carry lines holding one command each from an outside tool to a host, over standard//! input or a Unix socket, and carry each reply back. A tool or test drives a headless game by//! writing one command per line and reading the reply to each. A tool either starts the host and//! talks to it over the host's standard input, or connects to a host that is already running.//!//! A socket hands over bytes in pieces of any size, so a line can arrive split across reads, and//! one read can carry more than one line. A host answers many commands in one session, and it wants//! to reuse its memory from one command to the next.//!//! The caller supplies the object that answers each line (a *control object*): its `applyLine`//! takes an arena, the line and a writer, writes the reply to the writer, and returns false to stop//! serving. Both servers trim spaces, tabs and carriage returns from each line, skip a line left//! empty, and stop when the control object returns false. The arena is reset before each line and//! keeps its memory, so memory from one command serves the next.//!//! `serveStdio` reads lines from standard input and hands the control object a buffered writer to//! standard output. `serveStdio` never flushes that writer, so the control object flushes each//! reply itself. `serveSocket` listens on a Unix socket at a path the caller gives, prints a JSON//! `listening` event with the path on standard output, and serves one connection at a time. A line//! holds at most 1 MiB (`line_capacity`). When a line and its newline together pass 1 MiB, or a//! read fails, `serveStdio` ends as if the input had ended. `serveSocket` returns//! `error.LineCapacityExceeded` when the bytes it holds and one read together pass 1 MiB, so a line//! under 1 MiB can end it. That error, `error.OutOfMemory` from growing the held bytes, an error//! from the control object, or a failed accept returns from `serveSocket` and ends service for//! every later client.const std = @import("std");const pretty_json = @import("pretty").json;const sys = @import("sys");/// The most bytes one line may hold, 1 MiB. Both servers bound a line with it: `serveStdio` sizes/// its read buffer to it, and `LineBuffer.feed` refuses bytes past it. On standard input, a line/// and its newline together fit in it.pub const line_capacity = 1024 * 1024;/// An arena that a control object gets for each line, reset before the next line. `serveStdio`/// keeps one for the whole session, and `CommandWorkspace` holds one, so memory from one command/// serves the next. A reset keeps the memory the arena grew to.pub const CommandScratch = struct { arena: std.heap.ArenaAllocator, /// Returns a scratch whose arena draws memory from `allocator`. A server builds one before it /// reads its first line. pub fn init(allocator: std.mem.Allocator) CommandScratch { return .{ .arena = .init(allocator) }; } /// Frees the arena's memory and leaves the scratch undefined. A server calls it when it stops /// serving. pub fn deinit(self: *CommandScratch) void { self.arena.deinit(); self.* = undefined; } /// Resets the arena and passes it, `line` and `out` to `control.applyLine`, returning that /// call's result. A server calls it once per line, and a true result means the server keeps /// serving. Memory the control object took from the arena for an earlier line is invalid once /// the call resets it. The call returns any error that `applyLine` returns. pub fn apply(self: *CommandScratch, control: anytype, line: []const u8, out: *std.Io.Writer) !bool { _ = self.arena.reset(.retain_capacity); return control.applyLine(self.arena.allocator(), line, out); }};/// A scratch arena and a growing reply buffer for one connection, which keep a control object's/// reply in memory until the server sends it. `serveSocket` keeps one per connection and sends the/// reply after each line, and a test compares its reply with a direct call to the control object./// Once a first command has grown the arena and the buffer, repeating that command allocates/// nothing more.pub const CommandWorkspace = struct { scratch: CommandScratch, reply: std.Io.Writer.Allocating, /// Returns a workspace whose arena and reply buffer draw memory from `allocator`. `serveSocket` /// builds one for each connection it accepts. pub fn init(allocator: std.mem.Allocator) CommandWorkspace { return .{ .scratch = .init(allocator), .reply = .init(allocator), }; } /// Frees the reply buffer and the arena and leaves the workspace undefined. `serveSocket` calls /// it when a connection ends. pub fn deinit(self: *CommandWorkspace) void { self.reply.deinit(); self.scratch.deinit(); self.* = undefined; } /// Clears the reply buffer, then hands the control object the reset arena, the line and a /// writer into the buffer. `serveSocket` calls it once per line and then sends `written` to the /// client. The call returns the control object's result, true to keep serving, and any error /// the control object returns. A failed allocation for the reply reaches the caller as /// `error.WriteFailed`, and a later call can succeed. pub fn apply(self: *CommandWorkspace, control: anytype, line: []const u8) !bool { self.reply.clearRetainingCapacity(); return self.scratch.apply(control, line, &self.reply.writer); } /// Returns the reply that the last `apply` wrote. `serveSocket` sends these bytes to the client /// after each line. The slice stays valid until the next `apply` or `deinit`. pub fn written(self: *CommandWorkspace) []const u8 { return self.reply.written(); }};/// Bytes read from a connection and held until they are consumed, from which whole lines are taken/// in order. `serveSocket` feeds it each read, because a read can end inside a line or carry more/// than one line. The buffer holds at most `line_capacity` bytes.pub const LineBuffer = struct { allocator: std.mem.Allocator, pending: std.ArrayListUnmanaged(u8) = .empty, /// Returns an empty buffer that grows with `allocator`. `serveSocket` builds one for each /// connection. pub fn init(allocator: std.mem.Allocator) LineBuffer { return .{ .allocator = allocator }; } /// Frees the held bytes and leaves the buffer undefined. `serveSocket` calls it when a /// connection ends. The bytes of a line that never received its newline are dropped. pub fn deinit(self: *LineBuffer) void { self.pending.deinit(self.allocator); self.* = undefined; } /// Appends `bytes` to the held bytes. `serveSocket` feeds it each read from the connection. The /// call returns `error.LineCapacityExceeded` when the held bytes and `bytes` together pass /// `line_capacity`, and `error.OutOfMemory` when growing the buffer runs out of memory. The /// bound counts every held byte, so a read that finishes one line and starts later ones can /// pass it while each line is under 1 MiB. pub fn feed(self: *LineBuffer, bytes: []const u8) !void { std.debug.assert(self.pending.items.len <= line_capacity); if (bytes.len > line_capacity - self.pending.items.len) return error.LineCapacityExceeded; try self.pending.appendSlice(self.allocator, bytes); } /// Returns the first whole line, trimmed of spaces, tabs and carriage returns, or `null` when /// the held bytes hold no newline. `serveSocket` takes lines with it until it returns `null`. /// The call leaves the line held, and a caller calls `consume` to move past it. The line points /// into the buffer and stays valid until the next `feed` or `consume`. A blank line comes back /// empty. pub fn next(self: *LineBuffer) ?[]const u8 { const newline = std.mem.indexOfScalar(u8, self.pending.items, '\n') orelse return null; const line = self.pending.items[0..newline]; const trimmed = std.mem.trim(u8, line, " \t\r"); return trimmed; } /// Drops the first line and its newline, and moves the bytes after it to the front. /// `serveSocket` calls it after it handles each line from `next`. The call leaves the buffer as /// it was when no newline is held. The call copies every byte after the line. pub fn consume(self: *LineBuffer) void { const newline = std.mem.indexOfScalar(u8, self.pending.items, '\n') orelse return; const consumed = newline + 1; std.mem.copyForwards(u8, self.pending.items, self.pending.items[consumed..]); self.pending.shrinkRetainingCapacity(self.pending.items.len - consumed); }};/// Reads lines from standard input and hands each one to `control` until the control object returns/// false or the input ends. A host that a tool starts serves the tool's commands over its standard/// input and output. The control object gets a buffered writer to standard output for its reply./// The call never flushes that writer, so the control object flushes each reply, and bytes it/// leaves unflushed are lost when the call returns. Each line is trimmed of spaces, tabs and/// carriage returns, and a line left empty is skipped. A last line without a newline is still/// served. When a line and its newline together pass 1 MiB, or a read fails, the call ends as if/// the input had ended and returns normally. The call returns any error the control object returns./// The call keeps a 1 MiB read buffer and a 64 KiB write buffer on its stack, and it allocates from/// `allocator` for the arena alone.pub fn serveStdio(allocator: std.mem.Allocator, control: anytype) !void { var read_buffer: [line_capacity]u8 = undefined; var reader = sys.stdio.stdin().reader(sys.stdio.debugIo(), &read_buffer); var write_buffer: [64 * 1024]u8 = undefined; var writer = sys.stdio.stdout().writer(sys.stdio.debugIo(), &write_buffer); var scratch = CommandScratch.init(allocator); defer scratch.deinit(); while (true) { const line = reader.interface.takeDelimiter('\n') catch break orelse break; const trimmed = std.mem.trim(u8, line, " \t\r"); if (trimmed.len == 0) continue; const proceed = try scratch.apply(control, trimmed, &writer.interface); if (!proceed) break; }}/// Serves lines to `control` on a Unix socket at `path`, one connection at a time, until the/// control object returns false. A host that a tool connects to while it runs serves the tool's/// commands on a socket path. The call deletes any file at `path` before it listens, and deletes/// the socket file again when it returns. Once it listens, the call prints one JSON line with an/// `event` of `listening` and the path in `socket` on standard output. For each connection the call/// keeps a `LineBuffer` and a `CommandWorkspace`, and after each nonempty line it writes the/// control object's reply to the connection. A read error, the client closing, or a failed write/// ends that connection, and the call accepts the next one. A partial line held when the client/// closes is dropped. `error.LineCapacityExceeded` or `error.OutOfMemory` from the held bytes, an/// error from the control object, or a failed accept returns from the call and ends service for/// every later client.pub fn serveSocket(allocator: std.mem.Allocator, control: anytype, path: []const u8) !void { sys.fs.deleteFile(path) catch {}; const address = try sys.net.Address.initUnix(path); var server = try sys.net.Address.listen(address, .{}); defer server.deinit(); defer sys.fs.deleteFile(path) catch {}; try announceSocket(path); accepting: while (true) { var accepted = try server.accept(); defer accepted.stream.close(); var lines = LineBuffer.init(allocator); defer lines.deinit(); var workspace = CommandWorkspace.init(allocator); defer workspace.deinit(); var chunk: [16 * 1024]u8 = undefined; reading: while (true) { const read = accepted.stream.read(&chunk) catch break :reading; if (read == 0) break :reading; try lines.feed(chunk[0..read]); while (lines.next()) |line| { var proceed = true; if (line.len > 0) { proceed = try workspace.apply(control, line); accepted.stream.writeAll(workspace.written()) catch { lines.consume(); break :reading; }; } lines.consume(); if (!proceed) break :accepting; } } }}fn announceSocket(path: []const u8) !void { var stdout_buffer: [4096]u8 = undefined; var writer = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer); var json = pretty_json.Writer.init(&writer.interface, .minified); const root = try json.object(); try root.field("event", "listening"); try root.field("socket", path); try root.endLine(); try writer.interface.flush();}test "line buffer frames chunked input into trimmed lines" { var lines = LineBuffer.init(std.testing.allocator); defer lines.deinit(); try lines.feed("{\"cmd\":\"sta"); try std.testing.expect(lines.next() == null); try lines.feed("tus\"}\r\n{\"cmd\":\"quit\"}\n"); const first = lines.next().?; try std.testing.expectEqualStrings("{\"cmd\":\"status\"}", first); lines.consume(); const second = lines.next().?; try std.testing.expectEqualStrings("{\"cmd\":\"quit\"}", second); lines.consume(); try std.testing.expect(lines.next() == null);}Complete call list for transport.serveSocket
10 direct calls.
tiny.game.transport.CommandWorkspace.apply[method] atfun/game/src/transport.zig:95tiny.game.transport.CommandWorkspace.deinit[method] atfun/game/src/transport.zig:84tiny.game.transport.CommandWorkspace.init[function] atfun/game/src/transport.zig:75tiny.game.transport.CommandWorkspace.written[method] atfun/game/src/transport.zig:102tiny.game.transport.LineBuffer.consume[method] atfun/game/src/transport.zig:153tiny.game.transport.LineBuffer.deinit[method] atfun/game/src/transport.zig:122tiny.game.transport.LineBuffer.feed[method] atfun/game/src/transport.zig:132tiny.game.transport.LineBuffer.init[function] atfun/game/src/transport.zig:116tiny.game.transport.LineBuffer.next[method] atfun/game/src/transport.zig:143fun.game.src.transport.announceSocket[function] — private; no exact target atfun/game/src/transport.zig:240in nearest public ownertiny.game.transport
Audit
| Definitions | 19 |
|---|---|
| Public names | 19 |
| Members | 5 |
| Version | 26.7.0 |
| Revision | daab053ee433 |