carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1//! todo-zig: TodoMVC for the CARLOS bakeoff.
2//!
3//! app --socket <path> --db <path> (on Carloku)
4//! app --listen 127.0.0.1:<port> --db <path> (locally)
5//!
6//! Each connection runs as its own task on `std.Io`'s threaded backend.
7//! SIGTERM shuts the listening socket down, which ends the accept loop;
8//! open connections are cancelled and the database is closed before exit.
9
10const std = @import("std");
11const Io = std.Io;
12const net = Io.net;
13const db = @import("db.zig");
14const app_mod = @import("app.zig");
15
16const usage = "usage: app (--socket <path> | --listen 127.0.0.1:<port>) --db <path>\n";
17
18pub fn main(init: std.process.Init) !void {
19 const io = init.io;
20 const gpa = init.gpa;
21 const arena = init.arena.allocator();
22
23 var socket_path: ?[]const u8 = null;
24 var listen_addr: ?[]const u8 = null;
25 var db_path: ?[:0]const u8 = null;
26 var args = init.minimal.args.iterate();
27 _ = args.skip();
28 while (args.next()) |arg| {
29 if (std.mem.eql(u8, arg, "--socket")) {
30 socket_path = args.next() orelse return fail(usage);
31 } else if (std.mem.eql(u8, arg, "--listen")) {
32 listen_addr = args.next() orelse return fail(usage);
33 } else if (std.mem.eql(u8, arg, "--db")) {
34 db_path = args.next() orelse return fail(usage);
35 } else return fail(usage);
36 }
37 const db_file = db_path orelse return fail(usage);
38 if ((socket_path == null) == (listen_addr == null)) return fail(usage);
39
40 var store = db.Store.open(io, db_file) catch return fail("cannot open the database\n");
41 defer store.close();
42
43 // A POST's Origin must be this app's own: https://$CARLOS_HOST on the
44 // platform, http://<listen address> locally.
45 var origins: std.ArrayList([]const u8) = .empty;
46 if (init.environ_map.get("CARLOS_HOST")) |host| {
47 if (host.len > 0) try origins.append(arena, try std.fmt.allocPrint(arena, "https://{s}", .{host}));
48 }
49 if (listen_addr) |addr| try origins.append(arena, try std.fmt.allocPrint(arena, "http://{s}", .{addr}));
50
51 const app: app_mod.App = .{
52 .store = &store,
53 .io = io,
54 .origins = origins.items,
55 .https = socket_path != null,
56 };
57
58 var server = if (socket_path) |path| unix: {
59 Io.Dir.cwd().deleteFile(io, path) catch |err| switch (err) {
60 error.FileNotFound => {},
61 else => return err,
62 };
63 const ua = try net.UnixAddress.init(path);
64 break :unix try ua.listen(io, .{});
65 } else tcp: {
66 const ip = net.IpAddress.parseLiteral(listen_addr.?) catch return fail("--listen wants 127.0.0.1:<port>\n");
67 if (ip != .ip4 or !std.mem.eql(u8, &ip.ip4.bytes, &.{ 127, 0, 0, 1 })) {
68 return fail("--listen binds 127.0.0.1 only\n");
69 }
70 break :tcp try ip.listen(io, .{ .reuse_address = true });
71 };
72 defer {
73 server.deinit(io);
74 if (socket_path) |path| Io.Dir.cwd().deleteFile(io, path) catch {};
75 }
76
77 listen_fd.store(server.socket.handle, .release);
78 const on_stop: std.posix.Sigaction = .{
79 .handler = .{ .handler = stop },
80 .mask = std.posix.sigemptyset(),
81 .flags = 0,
82 };
83 std.posix.sigaction(.TERM, &on_stop, null);
84 std.posix.sigaction(.INT, &on_stop, null);
85
86 std.log.info("todo-zig {s} listening on {s}", .{
87 @import("build_options").version,
88 socket_path orelse listen_addr.?,
89 });
90
91 var connections: Io.Group = .init;
92 defer connections.cancel(io);
93 while (true) {
94 const stream = server.accept(io) catch |err| switch (err) {
95 error.SocketNotListening => break,
96 error.ConnectionAborted => continue,
97 else => {
98 std.log.err("accept: {t}", .{err});
99 continue;
100 },
101 };
102 connections.concurrent(io, serve, .{ &app, gpa, stream }) catch {
103 // No thread to spare: serve it here rather than drop it.
104 serve(&app, gpa, stream);
105 };
106 }
107 std.log.info("shutting down", .{});
108}
109
110/// Answers requests on one connection until the client or server closes it.
111fn serve(app: *const app_mod.App, gpa: std.mem.Allocator, stream: net.Stream) void {
112 const io = app.io;
113 defer stream.close(io);
114 var read_buf: [16 * 1024]u8 = undefined;
115 var write_buf: [16 * 1024]u8 = undefined;
116 var reader = stream.reader(io, &read_buf);
117 var writer = stream.writer(io, &write_buf);
118 var http: std.http.Server = .init(&reader.interface, &writer.interface);
119 var arena: std.heap.ArenaAllocator = .init(gpa);
120 defer arena.deinit();
121
122 while (true) {
123 var req = http.receiveHead() catch return;
124 const keep_alive = req.head.keep_alive;
125 app_mod.handle(app, &req, arena.allocator()) catch |err| {
126 std.log.err("{s}: {t}", .{ @tagName(req.head.method), err });
127 req.respond("internal error\n", .{ .status = .internal_server_error, .keep_alive = false }) catch {};
128 return;
129 };
130 _ = arena.reset(.retain_capacity);
131 if (!keep_alive or http.reader.state != .ready) return;
132 }
133}
134
135var listen_fd: std.atomic.Value(std.posix.fd_t) = .init(-1);
136
137/// Signal handler: shutting the listening socket down wakes `accept` with
138/// `error.SocketNotListening`. shutdown(2) is async-signal-safe.
139fn stop(_: std.posix.SIG) callconv(.c) void {
140 const fd = listen_fd.load(.acquire);
141 if (fd >= 0) _ = std.c.shutdown(fd, std.posix.SHUT.RDWR);
142}
143
144fn fail(msg: []const u8) error{InvalidArguments} {
145 std.debug.print("{s}", .{msg});
146 return error.InvalidArguments;
147}
148
149test {
150 _ = @import("app.zig");
151 _ = @import("view.zig");
152 _ = @import("db.zig");
153 _ = @import("db_test.zig");
154}
155