diff options
author | Yorhel <git@yorhel.nl> | 2021-06-10 09:18:12 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2021-06-10 09:18:12 +0200 |
commit | 269af7e83d52cecbe5c345e679538537be31151a (patch) | |
tree | 30c10c9453de3ec51c6804c26abbf0efd835bcb4 | |
parent | 746547338c33515c1d05518fefd85a59147de0e2 (diff) |
Committing some old changes. This project is pretty broken right nowzig
-rw-r--r-- | Config.zig | 38 | ||||
-rw-r--r-- | main.zig | 112 |
2 files changed, 108 insertions, 42 deletions
@@ -18,15 +18,16 @@ pub const Listener = struct { pub const Backend = struct { spawn: ?[]const u8 = null, + cwd: ?std.fs.Dir = null, max_processes: u32 = 0, }; -// Should be marked as noreturn, but https://github.com/ziglang/zig/issues/5728 -pub fn err(comptime fmt: []const u8, arg: anytype) void { - _ = std.io.getStdErr().writer().print(fmt, arg) catch {}; +pub fn err(comptime fmt: []const u8, arg: anytype) noreturn { + _ = nosuspend std.io.getStdErr().writer().print(fmt, arg) catch {}; std.process.exit(1); } +// TODO: This really should not have async calling convention. pub fn read() !Self { const eq = std.mem.eql; var args = std.process.args(); @@ -53,10 +54,8 @@ pub fn read() !Self { // Return the currently-being-configured listener, or exit with an error if we're not configuring a listener. fn listener(self: *Self) *Listener { - if (self.listeners.items.len == 0 or self.backend.spawn != null) { + if (self.listeners.items.len == 0 or self.backend.spawn != null) err("Option '{s}' can only be used when configuring a listener.\n", .{self._current_opt}); - unreachable; - } return &self.listeners.items[self.listeners.items.len - 1]; } @@ -65,7 +64,6 @@ fn optArg(self: *Self) [:0]const u8 { return arg; } err("Option '{s}' requires an argument.\n", .{self._current_opt}); - unreachable; } fn parseTypedAddr(arg: []const u8, ltype: *[]const u8) !?Address { @@ -99,30 +97,30 @@ fn readListen(self: *Self) Listener { const arg = self.optArg(); var ltype: []const u8 = undefined; if (parseTypedAddr(arg, <ype)) |a| { - if (!std.mem.eql(u8, ltype, "fastcgi")) { + if (!std.mem.eql(u8, ltype, "fastcgi")) err("Unknown listener type '{s}', only fastcgi is supported at the moment.\n", .{ltype}); - } return Listener{ .addr = a }; - } else |e| { - err("Error parsing address '{s}': {}\n", .{ arg, e }); - unreachable; - } + } else |e| err("Error parsing address '{s}': {}\n", .{ arg, e }); } fn readInt(self: *Self, comptime T: type) T { - return std.fmt.parseInt(T, self.optArg(), 10) catch |_| { - err("Option '{s}' expects an integer argument.\n", .{self._current_opt}); - unreachable; - }; + return std.fmt.parseInt(T, self.optArg(), 10) + catch |_| err("Option '{s}' expects an integer argument.\n", .{self._current_opt}); } fn readSpawn(self: *Self) void { if (self.backend.spawn != null) err("Only a single 'spawn' option is supported.\n", .{}); const arg = self.optArg(); - if (!std.mem.startsWith(u8, arg, "fastcgi:")) err("The 'spawn' option only supports 'fastcgi:' paths at the moment.\n", .{}); - const path = arg[8..]; + if (!std.mem.startsWith(u8, arg, "cgi:")) err("The 'spawn' option only supports 'cgi:' paths at the moment.\n", .{}); + const path = arg[4..]; if (path.len == 0) err("No spawn path given.\n", .{}); - self.backend.spawn = path; + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const rpath = std.os.realpath(path, buf[0..]) catch |e| + err("Unable to obtain full path of '{s}': {}.\n", .{path, e}); + self.backend.spawn = alloc.dupe(u8, rpath) catch unreachable; + // TODO: dirname can return null; may instead want to check if rpath points to an (executable) file to catch config errors early. + self.backend.cwd = std.fs.openDirAbsolute(std.fs.path.dirname(rpath).?, .{}) + catch |e| err("Unable to open parent directory of '{s}': {}.\n", .{rpath, e}); } test "parse typed address" { @@ -8,32 +8,50 @@ const fastcgi = @import("fastcgi.zig"); const Config = @import("Config.zig"); const Address = std.net.Address; -pub const io_mode = .evented; +// Initialize a custom event loop, because the default one spawns far too many threads. +var _event_loop: std.event.Loop = undefined; +pub const event_loop: *std.event.Loop = &_event_loop; +//pub const io_mode = .evented; +//const event_loop = std.event.Loop.instance.?; var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; pub const alloc = &general_purpose_allocator.allocator; -const mainloop = std.event.Loop.instance.?; + +var backend: Backend = undefined; pub fn main() !void { + event_loop.initSingleThreaded() catch unreachable; + event_loop.beginOneEvent(); // The event loop quits early for some reason without this, no clue why. I mean, there's always at least one event queued. + var m = async amain(); + event_loop.run(); + nosuspend try await m; +} + +fn amain() !void { const conf = try Config.read(); std.debug.print("Listener = {}, Front = {}\n", .{@sizeOf(Listener), @sizeOf(Front)}); if (conf.version) { - std.io.getStdOut().writeAll("fcgy 0.1\n") catch unreachable; + std.io.getStdOut().writeAll("fcgy 0.1\n") catch {}; std.process.exit(0); } if (conf.help) { - std.io.getStdOut().writeAll("fcgy <options> <listen-options> <spawn-options>\n") catch unreachable; + std.io.getStdOut().writeAll("fcgy <options> <listen-options> <spawn-options>\n") catch {}; std.process.exit(0); } - if (conf.listeners.items.len == 0) { + if (conf.listeners.items.len == 0) Config.err("No listeners defined\n", .{}); - } - if (conf.listeners.items.len == 1 and conf.listeners.items[0].addr == null) { + if (conf.listeners.items.len == 1 and conf.listeners.items[0].addr == null) Config.err("Listening on standard I/O is not supported yet.\n", .{}); - } + if (conf.backend.spawn == null) + Config.err("No backend configured.\n", .{}); + + std.debug.print("Backend CGI exec '{s}'.\n", .{ conf.backend.spawn.? }); + backend = .{ + .conf = &conf.backend, + }; for (conf.listeners.items) |l| { const listener = alloc.create(Listener) catch unreachable; @@ -44,13 +62,11 @@ pub fn main() !void { if (l.addr) |a| { try listener.server.listen(l.addr.?); std.debug.print("Listening on {}\n", .{l.addr}); - } else { + } else Config.err("Cannot mix standard I/O and server-type listeners.\n", .{}); - } listener.frame_next_tick = .{ .data = &listener.frame }; listener.frame = async listener.listen(); } - mainloop.run(); } @@ -79,7 +95,7 @@ const Listener = struct { const front = alloc.create(Front) catch unreachable; front.* = Front.init(self, con.address, con.stream); self.connections.append(&front.listener_node); - mainloop.runDetached(alloc, Front.run, .{front}) catch unreachable; + event_loop.runDetached(alloc, Front.run, .{front}) catch unreachable; } } @@ -90,7 +106,7 @@ const Listener = struct { if (self.listen_suspended) { self.listen_suspended = false; - mainloop.onNextTick(&self.frame_next_tick); + event_loop.onNextTick(&self.frame_next_tick); } } }; @@ -103,7 +119,6 @@ const Front = struct { requests: std.AutoHashMap(u16, *Request) = std.AutoHashMap(u16, *Request).init(alloc), has_shutdown: bool = false, rd: std.io.BufferedReader(4096, std.net.Stream.Reader), - wr: std.io.BufferedWriter(4096, std.net.Stream.Writer), // TODO: Put the frame of run() inside this struct? Problem: run() can't destroy() the Front struct in that case. const Self = @This(); @@ -114,22 +129,21 @@ const Front = struct { .stream = stream, .listener = listener, .rd = std.io.bufferedReader(stream.reader()), - .wr = std.io.bufferedWriter(stream.writer()), }; } fn run(self: *Self) void { std.debug.print("{*} Connection from {}\n", .{ self, self.addr }); - self.loop() catch |e| std.debug.print("{*} Connection closed: {}\n", .{ self, e }); + self.loop() catch |e| if(!self.has_shutdown) std.debug.print("{*} Connection closed: {}\n", .{ self, e }); if (self.has_shutdown) std.debug.print("{*} Connection has been shutdown\n", .{self}); - self.shutdown(); + self.stream.close(); self.listener.remove_connection(self); } /// Close the socket and stop the loop() fn shutdown(self: *Self) void { - if (!self.has_shutdown) self.stream.close(); + if (!self.has_shutdown) std.os.shutdown(self.stream.handle, .both) catch {}; self.has_shutdown = true; } @@ -165,12 +179,63 @@ const Front = struct { // We don't assign a back-end until we have all params, so this shouldn't happen. if (req.back != null) return error.invalidState; if (r.content_length > 0) return r.readArrayList(self.rd.reader(), &req.params); - // TODO: We received all params now, assign a backend and process the request try r.skip(self.rd.reader()); + try backend.init_request(req); } }; const Backend = struct { + conf: *const Config.Backend, + processes: std.TailQueue(void) = .{}, + + const Self = @This(); + const Process = struct { + node: std.TailQueue(void).Node = undefined, + connections: std.TailQueue(void) = .{}, + proc: *std.ChildProcess, + + const Self = @This(); + + fn deinit(self: *Self) void { + } + }; + + const Connection = struct { + node: std.TailQueue(void).Node = undefined, + process: *Process, + rd: std.io.BufferedReader(4096, std.fs.File.Reader), + // TODO: fd's (if not CGI) + }; + + // Find/spawn a backend connection for the request and pass along the parameters. + fn init_request(self: *Self, req: *Request) !void { + std.debug.assert(req.back == null); + var cmd = try std.ChildProcess.init(&[_]([]const u8){self.conf.spawn.?}, alloc); + errdefer cmd.deinit(); + cmd.stdin_behavior = .Pipe; + cmd.stdout_behavior = .Pipe; + cmd.stderr_behavior = .Ignore; // TODO: Capture stderr + + try cmd.spawn(); // https://github.com/ziglang/zig/issues/6682 + errdefer { _ = cmd.kill() catch {}; } // XXX: May block + + var proc = try alloc.create(Process); + errdefer alloc.destroy(proc); + proc.* = .{ .proc = cmd }; + + var con = try alloc.create(Connection); + errdefer alloc.destroy(con); + con.* = .{ + .process = proc, + .rd = std.io.bufferedReader(cmd.stdout.?.reader()), + }; + + // TODO: Spawn async reader. + + req.back = con; + proc.connections.append(&con.node); + self.processes.append(&proc.node); + } }; const Request = struct { @@ -182,17 +247,20 @@ const Request = struct { /// Whether we should keep the front-end connection alive after this request has been processed. front_keep_conn: bool, - // TODO: backend pointer - back: ?*void = null, + back: ?*Backend.Connection = null, /// List of parameters in FastCGI format. params: std.ArrayList(u8) = std.ArrayList(u8).init(alloc), const Self = @This(); + fn cgi_env_map(req: *Request) !std.BufMap { + ... + } + /// Free resources associated with this request. /// Does nothing when 'front' or 'back' are still set, as that means this request is still referenced from either side. - pub fn deinit(self: *Self) void { + fn deinit(self: *Self) void { if (self.front != null or self.back != null) { return; } |