const std = @import("std"); const builtin = @import("builtin"); const mime = @import("mime.zig"); const net = std.net; const mem = std.mem; const fs = std.fs; const build = std.build; const www_dir_path = sdkPath("/www"); const buffer_size = 2048; const esc = struct { pub const reset = "\x1b[0m"; pub const bold = "\x1b[1m"; pub const underline = "\x1b[4m"; pub const red = "\x1b[31m"; pub const yellow = "\x1b[33m"; pub const cyan = "\x1b[36m"; pub const gray = "\x1b[90m"; }; pub const Options = struct { install_dir: ?build.InstallDir = null, watch_paths: ?[]const []const u8 = null, listen_address: ?net.Address = null, }; pub const Error = error{CannotOpenDirectory} || mem.Allocator.Error; pub fn serve(step: *build.CompileStep, options: Options) Error!*Wasmserve { const self = try step.step.owner.allocator.create(Wasmserve); const install_dir = options.install_dir orelse build.InstallDir{ .lib = {} }; const install_dir_iter = fs.cwd().makeOpenPathIterable(step.step.owner.getInstallPath(install_dir, ""), .{}) catch return error.CannotOpenDirectory; self.* = Wasmserve{ .step = build.Step.init(.run, "wasmserve", step.step.owner.allocator, Wasmserve.make), .b = step.step.owner, .exe_step = step, .install_dir = install_dir, .install_dir_iter = install_dir_iter, .address = options.listen_address orelse net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 8080), .subscriber = null, .watch_paths = options.watch_paths orelse &.{step.root_src.?.path}, .mtimes = std.AutoHashMap(fs.File.INode, i128).init(step.step.owner.allocator), .notify_msg = null, }; return self; } const Wasmserve = struct { step: build.Step, b: *build.Builder, exe_step: *build.CompileStep, install_dir: build.InstallDir, install_dir_iter: fs.IterableDir, address: net.Address, subscriber: ?*net.StreamServer.Connection, watch_paths: []const []const u8, mtimes: std.AutoHashMap(fs.File.INode, i128), notify_msg: ?NotifyMessage, const NotifyMessage = struct { const Event = enum { built, build_error, stopped, }; event: Event, data: []const u8, }; pub fn make(step: *build.Step) !void { const self = @fieldParentPtr(Wasmserve, "step", step); self.compile(); std.debug.assert(mem.eql(u8, fs.path.extension(self.exe_step.out_filename), ".wasm")); var www_dir = try fs.cwd().openIterableDir(www_dir_path, .{}); defer www_dir.close(); var www_dir_iter = www_dir.iterate(); while (try www_dir_iter.next()) |file| { const path = try fs.path.join(self.b.allocator, &.{ www_dir_path, file.name }); defer self.b.allocator.free(path); const install_www = self.b.addInstallFileWithDir( .{ .path = path }, self.install_dir, file.name, ); try install_www.step.make(); } const watch_thread = try std.Thread.spawn(.{}, watch, .{self}); defer watch_thread.detach(); try self.runServer(); } fn runServer(self: *Wasmserve) !void { var server = net.StreamServer.init(.{ .reuse_address = true }); defer server.deinit(); try server.listen(self.address); var addr_buf = @as([45]u8, undefined); var fbs = std.io.fixedBufferStream(&addr_buf); if (self.address.format("", .{}, fbs.writer())) { std.log.info("Started listening at " ++ esc.cyan ++ esc.underline ++ "http://{s}" ++ esc.reset ++ "...", .{fbs.getWritten()}); } else |err| logErr(err, @src()); while (server.accept()) |conn| { self.respond(conn) catch |err| { logErr(err, @src()); continue; }; } else |err| logErr(err, @src()); } fn respond(self: *Wasmserve, conn: net.StreamServer.Connection) !void { errdefer respondError(conn.stream, 500, "Internal Server Error") catch |err| logErr(err, @src()); var recv_buf: [buffer_size]u8 = undefined; const first_line = conn.stream.reader().readUntilDelimiter(&recv_buf, '\n') catch |err| { switch (err) { error.StreamTooLong => try respondError(conn.stream, 414, "Too Long Request"), else => try respondError(conn.stream, 400, "Bad Request"), } return; }; var first_line_iter = mem.split(u8, first_line, " "); _ = first_line_iter.next(); // skip method if (first_line_iter.next()) |uri| { if (uri[0] != '/') { try respondError(conn.stream, 400, "Bad Request"); return; } const url = dropFragment(uri)[1..]; if (mem.eql(u8, url, "notify")) { _ = try conn.stream.write("HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Type: text/event-stream\r\nCache-Control: No-Cache\r\n\r\n"); self.subscriber = try self.b.allocator.create(net.StreamServer.Connection); self.subscriber.?.* = conn; if (self.notify_msg) |msg| if (msg.event != .built) self.notify(); return; } if (self.researchPath(url)) |file_path| { try self.respondFile(conn.stream, file_path); return; } else |_| {} try respondError(conn.stream, 404, "Not Found"); } else { try respondError(conn.stream, 400, "Bad Request"); } } fn respondFile(self: Wasmserve, stream: net.Stream, path: []const u8) !void { const ext = fs.path.extension(path); var file_mime: []const u8 = "text/plain"; inline for (mime.mime_list) |entry| { for (entry.ext) |ext_entry| { if (std.mem.eql(u8, ext, ext_entry)) file_mime = entry.mime; } } const file = try self.install_dir_iter.dir.openFile(path, .{}); defer file.close(); const file_size = try file.getEndPos(); try stream.writer().print( "HTTP/1.1 200 OK\r\n" ++ "Connection: close\r\n" ++ "Content-Length: {d}\r\n" ++ "Content-Type: {s}\r\n" ++ "\r\n", .{ file_size, file_mime }, ); try fs.File.writeFileAll(.{ .handle = stream.handle }, file, .{}); } fn respondError(stream: net.Stream, code: u32, desc: []const u8) !void { try stream.writer().print( "HTTP/1.1 {d} {s}\r\n" ++ "Connection: close\r\n" ++ "Content-Length: {d}\r\n" ++ "Content-Type: text/html\r\n" ++ "\r\n