master
  1gpa: Allocator,
  2graph: *const Build.Graph,
  3all_steps: []const *Build.Step,
  4listen_address: net.IpAddress,
  5ttyconf: Io.tty.Config,
  6root_prog_node: std.Progress.Node,
  7watch: bool,
  8
  9tcp_server: ?net.Server,
 10serve_thread: ?std.Thread,
 11
 12/// Uses `Io.Clock.awake`.
 13base_timestamp: Io.Timestamp,
 14/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
 15step_names_trailing: []u8,
 16
 17/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
 18/// Accessed atomically.
 19step_status_bits: []u8,
 20
 21fuzz: ?Fuzz,
 22time_report_mutex: Io.Mutex,
 23time_report_msgs: [][]u8,
 24time_report_update_times: []i64,
 25
 26build_status: std.atomic.Value(abi.BuildStatus),
 27/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
 28/// to increment this value. Each client thread waits for this increment with `std.Thread.Futex`, so
 29/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it
 30/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For
 31/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes,
 32/// because this value changes quickly so this would result in constantly spamming all clients with
 33/// an unreasonable number of packets.
 34update_id: std.atomic.Value(u32),
 35
 36runner_request_mutex: Io.Mutex,
 37runner_request_ready_cond: Io.Condition,
 38runner_request_empty_cond: Io.Condition,
 39runner_request: ?RunnerRequest,
 40
 41/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
 42/// on a fixed interval of this many milliseconds.
 43const default_update_interval_ms = 500;
 44
 45pub const base_clock: Io.Clock = .awake;
 46
 47/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
 48pub fn notifyUpdate(ws: *WebServer) void {
 49    _ = ws.update_id.rmw(.Add, 1, .release);
 50    std.Thread.Futex.wake(&ws.update_id, 16);
 51}
 52
 53pub const Options = struct {
 54    gpa: Allocator,
 55    ttyconf: Io.tty.Config,
 56    graph: *const std.Build.Graph,
 57    all_steps: []const *Build.Step,
 58    root_prog_node: std.Progress.Node,
 59    watch: bool,
 60    listen_address: net.IpAddress,
 61    base_timestamp: Io.Clock.Timestamp,
 62};
 63pub fn init(opts: Options) WebServer {
 64    // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent`
 65    // instead of threads, so that the web server can function in single-threaded builds.
 66    comptime assert(!builtin.single_threaded);
 67    assert(opts.base_timestamp.clock == base_clock);
 68
 69    const all_steps = opts.all_steps;
 70
 71    const step_names_trailing = opts.gpa.alloc(u8, len: {
 72        var name_bytes: usize = 0;
 73        for (all_steps) |step| name_bytes += step.name.len;
 74        break :len name_bytes + all_steps.len * 4;
 75    }) catch @panic("out of memory");
 76    {
 77        const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
 78        var idx: usize = all_steps.len * 4;
 79        for (all_steps, step_name_lens) |step, *name_len| {
 80            name_len.* = @intCast(step.name.len);
 81            @memcpy(step_names_trailing[idx..][0..step.name.len], step.name);
 82            idx += step.name.len;
 83        }
 84        assert(idx == step_names_trailing.len);
 85    }
 86
 87    const step_status_bits = opts.gpa.alloc(
 88        u8,
 89        std.math.divCeil(usize, all_steps.len, 4) catch unreachable,
 90    ) catch @panic("out of memory");
 91    @memset(step_status_bits, 0);
 92
 93    const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0;
 94    const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory");
 95    const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory");
 96    @memset(time_report_msgs, &.{});
 97    @memset(time_report_update_times, std.math.minInt(i64));
 98
 99    return .{
100        .gpa = opts.gpa,
101        .ttyconf = opts.ttyconf,
102        .graph = opts.graph,
103        .all_steps = all_steps,
104        .listen_address = opts.listen_address,
105        .root_prog_node = opts.root_prog_node,
106        .watch = opts.watch,
107
108        .tcp_server = null,
109        .serve_thread = null,
110
111        .base_timestamp = opts.base_timestamp.raw,
112        .step_names_trailing = step_names_trailing,
113
114        .step_status_bits = step_status_bits,
115
116        .fuzz = null,
117        .time_report_mutex = .init,
118        .time_report_msgs = time_report_msgs,
119        .time_report_update_times = time_report_update_times,
120
121        .build_status = .init(.idle),
122        .update_id = .init(0),
123
124        .runner_request_mutex = .init,
125        .runner_request_ready_cond = .{},
126        .runner_request_empty_cond = .{},
127        .runner_request = null,
128    };
129}
130pub fn deinit(ws: *WebServer) void {
131    const gpa = ws.gpa;
132
133    gpa.free(ws.step_names_trailing);
134    gpa.free(ws.step_status_bits);
135
136    if (ws.fuzz) |*f| f.deinit();
137    for (ws.time_report_msgs) |msg| gpa.free(msg);
138    gpa.free(ws.time_report_msgs);
139    gpa.free(ws.time_report_update_times);
140
141    if (ws.serve_thread) |t| {
142        if (ws.tcp_server) |*s| s.stream.close();
143        t.join();
144    }
145    if (ws.tcp_server) |*s| s.deinit();
146
147    gpa.free(ws.step_names_trailing);
148}
149pub fn start(ws: *WebServer) error{AlreadyReported}!void {
150    assert(ws.tcp_server == null);
151    assert(ws.serve_thread == null);
152    const io = ws.graph.io;
153
154    ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
155        log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.getPort(), @errorName(err) });
156        return error.AlreadyReported;
157    };
158    ws.serve_thread = std.Thread.spawn(.{}, serve, .{ws}) catch |err| {
159        log.err("unable to spawn web server thread: {s}", .{@errorName(err)});
160        ws.tcp_server.?.deinit(io);
161        ws.tcp_server = null;
162        return error.AlreadyReported;
163    };
164
165    log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address});
166    if (ws.listen_address.getPort() == 0) {
167        log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address});
168    }
169}
170fn serve(ws: *WebServer) void {
171    const io = ws.graph.io;
172    while (true) {
173        var stream = ws.tcp_server.?.accept(io) catch |err| {
174            log.err("failed to accept connection: {s}", .{@errorName(err)});
175            return;
176        };
177        _ = std.Thread.spawn(.{}, accept, .{ ws, stream }) catch |err| {
178            log.err("unable to spawn connection thread: {s}", .{@errorName(err)});
179            stream.close(io);
180            continue;
181        };
182    }
183}
184
185pub fn startBuild(ws: *WebServer) void {
186    if (ws.fuzz) |*fuzz| {
187        fuzz.deinit();
188        ws.fuzz = null;
189    }
190    for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
191    ws.build_status.store(.running, .monotonic);
192    ws.notifyUpdate();
193}
194
195pub fn updateStepStatus(ws: *WebServer, step: *Build.Step, new_status: abi.StepUpdate.Status) void {
196    const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
197        if (s == step) break @intCast(i);
198    } else unreachable;
199    const ptr = &ws.step_status_bits[step_idx / 4];
200    const bit_offset: u3 = @intCast((step_idx % 4) * 2);
201    const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
202    const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset;
203    _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
204    ws.notifyUpdate();
205}
206
207pub fn finishBuild(ws: *WebServer, opts: struct {
208    fuzz: bool,
209}) void {
210    if (opts.fuzz) {
211        switch (builtin.os.tag) {
212            // Current implementation depends on two things that need to be ported to Windows:
213            // * Memory-mapping to share data between the fuzzer and build runner.
214            // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
215            //   many addresses to source locations).
216            .windows => std.process.fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
217            else => {},
218        }
219        if (@bitSizeOf(usize) != 64) {
220            // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
221            // being compatible with `std.fs.getEndPos() u64`'s return value. This is not the case
222            // on 32-bit platforms.
223            // Affects or affected by issues #5185, #22523, and #22464.
224            std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
225        }
226
227        assert(ws.fuzz == null);
228
229        ws.build_status.store(.fuzz_init, .monotonic);
230        ws.notifyUpdate();
231
232        ws.fuzz = Fuzz.init(
233            ws.gpa,
234            ws.graph.io,
235            ws.ttyconf,
236            ws.all_steps,
237            ws.root_prog_node,
238            .{ .forever = .{ .ws = ws } },
239        ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
240        ws.fuzz.?.start();
241    }
242
243    ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic);
244    ws.notifyUpdate();
245}
246
247pub fn now(s: *const WebServer) i64 {
248    const io = s.graph.io;
249    const ts = base_clock.now(io) catch s.base_timestamp;
250    return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds());
251}
252
253fn accept(ws: *WebServer, stream: net.Stream) void {
254    const io = ws.graph.io;
255    defer {
256        // `net.Stream.close` wants to helpfully overwrite `stream` with
257        // `undefined`, but it cannot do so since it is an immutable parameter.
258        var copy = stream;
259        copy.close(io);
260    }
261    var send_buffer: [4096]u8 = undefined;
262    var recv_buffer: [4096]u8 = undefined;
263    var connection_reader = stream.reader(io, &recv_buffer);
264    var connection_writer = stream.writer(io, &send_buffer);
265    var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface);
266
267    while (true) {
268        var request = server.receiveHead() catch |err| switch (err) {
269            error.HttpConnectionClosing => return,
270            else => return log.err("failed to receive http request: {t}", .{err}),
271        };
272        switch (request.upgradeRequested()) {
273            .websocket => |opt_key| {
274                const key = opt_key orelse return log.err("missing websocket key", .{});
275                var web_socket = request.respondWebSocket(.{ .key = key }) catch {
276                    return log.err("failed to respond web socket: {t}", .{connection_writer.err.?});
277                };
278                ws.serveWebSocket(&web_socket) catch |err| {
279                    log.err("failed to serve websocket: {t}", .{err});
280                    return;
281                };
282                comptime unreachable;
283            },
284            .other => |name| return log.err("unknown upgrade request: {s}", .{name}),
285            .none => {
286                ws.serveRequest(&request) catch |err| switch (err) {
287                    error.AlreadyReported => return,
288                    else => {
289                        log.err("failed to serve '{s}': {t}", .{ request.head.target, err });
290                        return;
291                    },
292                };
293            },
294        }
295    }
296}
297
298fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
299    const io = ws.graph.io;
300
301    var prev_build_status = ws.build_status.load(.monotonic);
302
303    const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len);
304    defer ws.gpa.free(prev_step_status_bits);
305    for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| {
306        copy.* = @atomicLoad(u8, shared, .monotonic);
307    }
308
309    const recv_thread = try std.Thread.spawn(.{}, recvWebSocketMessages, .{ ws, sock });
310    defer recv_thread.join();
311
312    {
313        const hello_header: abi.Hello = .{
314            .status = prev_build_status,
315            .flags = .{
316                .time_report = ws.graph.time_report,
317            },
318            .timestamp = ws.now(),
319            .steps_len = @intCast(ws.all_steps.len),
320        };
321        var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits };
322        try sock.writeMessageVec(&bufs, .binary);
323    }
324
325    var prev_fuzz: Fuzz.Previous = .init;
326    var prev_time: i64 = std.math.minInt(i64);
327    while (true) {
328        const start_time = ws.now();
329        const start_update_id = ws.update_id.load(.acquire);
330
331        if (ws.fuzz) |*fuzz| {
332            try fuzz.sendUpdate(sock, &prev_fuzz);
333        }
334
335        {
336            try ws.time_report_mutex.lock(io);
337            defer ws.time_report_mutex.unlock(io);
338            for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| {
339                if (update_time <= prev_time) continue;
340                // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so
341                // that we don't hold up the build system on the client accepting this packet.
342                const owned_msg = try ws.gpa.dupe(u8, msg);
343                defer ws.gpa.free(owned_msg);
344                // Temporarily unlock, then re-lock after the message is sent.
345                ws.time_report_mutex.unlock(io);
346                defer ws.time_report_mutex.lockUncancelable(io);
347                try sock.writeMessage(owned_msg, .binary);
348            }
349        }
350
351        {
352            const build_status = ws.build_status.load(.monotonic);
353            if (build_status != prev_build_status) {
354                prev_build_status = build_status;
355                const msg: abi.StatusUpdate = .{ .new = build_status };
356                try sock.writeMessage(@ptrCast(&msg), .binary);
357            }
358        }
359
360        for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
361            const cur_byte = @atomicLoad(u8, shared, .monotonic);
362            if (prev_byte.* == cur_byte) continue;
363            const cur: [4]abi.StepUpdate.Status = .{
364                @enumFromInt(@as(u2, @truncate(cur_byte >> 0))),
365                @enumFromInt(@as(u2, @truncate(cur_byte >> 2))),
366                @enumFromInt(@as(u2, @truncate(cur_byte >> 4))),
367                @enumFromInt(@as(u2, @truncate(cur_byte >> 6))),
368            };
369            const prev: [4]abi.StepUpdate.Status = .{
370                @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))),
371                @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))),
372                @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))),
373                @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))),
374            };
375            for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| {
376                const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } };
377                if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary);
378            }
379            prev_byte.* = cur_byte;
380        }
381
382        prev_time = start_time;
383        std.Thread.Futex.timedWait(&ws.update_id, start_update_id, std.time.ns_per_ms * default_update_interval_ms) catch {};
384    }
385}
386fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
387    const io = ws.graph.io;
388
389    while (true) {
390        const msg = sock.readSmallMessage() catch return;
391        if (msg.opcode != .binary) continue;
392        if (msg.data.len == 0) continue;
393        const tag: abi.ToServerTag = @enumFromInt(msg.data[0]);
394        switch (tag) {
395            _ => continue,
396            .rebuild => while (true) {
397                ws.runner_request_mutex.lock(io) catch |err| switch (err) {
398                    error.Canceled => return,
399                };
400                defer ws.runner_request_mutex.unlock(io);
401                if (ws.runner_request == null) {
402                    ws.runner_request = .rebuild;
403                    ws.runner_request_ready_cond.signal(io);
404                    break;
405                }
406                ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return;
407            },
408        }
409    }
410}
411
412fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void {
413    // Strip an optional leading '/debug' component from the request.
414    const target: []const u8, const debug: bool = target: {
415        if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true };
416        if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true };
417        if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true };
418        break :target .{ req.head.target, false };
419    };
420
421    if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html");
422    if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript");
423    if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css");
424    if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css");
425    if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast);
426
427    if (ws.fuzz) |*fuzz| {
428        if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req);
429    }
430
431    try req.respond("not found", .{
432        .status = .not_found,
433        .extra_headers = &.{
434            .{ .name = "Content-Type", .value = "text/plain" },
435        },
436    });
437}
438
439fn serveLibFile(
440    ws: *WebServer,
441    request: *http.Server.Request,
442    sub_path: []const u8,
443    content_type: []const u8,
444) !void {
445    return serveFile(ws, request, .{
446        .root_dir = ws.graph.zig_lib_directory,
447        .sub_path = sub_path,
448    }, content_type);
449}
450fn serveClientWasm(
451    ws: *WebServer,
452    req: *http.Server.Request,
453    optimize_mode: std.builtin.OptimizeMode,
454) !void {
455    var arena_state: std.heap.ArenaAllocator = .init(ws.gpa);
456    defer arena_state.deinit();
457    const arena = arena_state.allocator();
458
459    // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page.
460    const bin_path = try buildClientWasm(ws, arena, optimize_mode);
461    return serveFile(ws, req, bin_path, "application/wasm");
462}
463
464pub fn serveFile(
465    ws: *WebServer,
466    request: *http.Server.Request,
467    path: Cache.Path,
468    content_type: []const u8,
469) !void {
470    const gpa = ws.gpa;
471    // The desired API is actually sendfile, which will require enhancing http.Server.
472    // We load the file with every request so that the user can make changes to the file
473    // and refresh the HTML page without restarting this server.
474    const file_contents = path.root_dir.handle.readFileAlloc(path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| {
475        log.err("failed to read '{f}': {s}", .{ path, @errorName(err) });
476        return error.AlreadyReported;
477    };
478    defer gpa.free(file_contents);
479    try request.respond(file_contents, .{
480        .extra_headers = &.{
481            .{ .name = "Content-Type", .value = content_type },
482            cache_control_header,
483        },
484    });
485}
486pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
487    const gpa = ws.gpa;
488    const io = ws.graph.io;
489
490    var send_buffer: [0x4000]u8 = undefined;
491    var response = try request.respondStreaming(&send_buffer, .{
492        .respond_options = .{
493            .extra_headers = &.{
494                .{ .name = "Content-Type", .value = "application/x-tar" },
495                cache_control_header,
496            },
497        },
498    });
499
500    var cached_cwd_path: ?[]const u8 = null;
501    defer if (cached_cwd_path) |p| gpa.free(p);
502
503    var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
504
505    for (paths) |path| {
506        var file = path.root_dir.handle.openFile(path.sub_path, .{}) catch |err| {
507            log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
508            continue;
509        };
510        defer file.close();
511        const stat = try file.stat();
512        var read_buffer: [1024]u8 = undefined;
513        var file_reader: Io.File.Reader = .initSize(file.adaptToNewApi(), io, &read_buffer, stat.size);
514
515        // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can
516        // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI:
517        // it turns out the WASM treats the first path component as the module name, typically
518        // resulting in modules named "" and "src". The compiler needs to tell the build system
519        // about the module graph so that the build system can correctly encode this information in
520        // the tar file.
521        archiver.prefix = path.root_dir.path orelse cwd: {
522            if (cached_cwd_path == null) cached_cwd_path = try std.process.getCwdAlloc(gpa);
523            break :cwd cached_cwd_path.?;
524        };
525        try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds()));
526    }
527
528    // intentionally not calling `archiver.finishPedantically`
529    try response.end();
530}
531
532fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path {
533    const io = ws.graph.io;
534    const root_name = "build-web";
535    const arch_os_abi = "wasm32-freestanding";
536    const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
537
538    const gpa = ws.gpa;
539    const graph = ws.graph;
540
541    const main_src_path: Cache.Path = .{
542        .root_dir = graph.zig_lib_directory,
543        .sub_path = "build-web/main.zig",
544    };
545    const walk_src_path: Cache.Path = .{
546        .root_dir = graph.zig_lib_directory,
547        .sub_path = "docs/wasm/Walk.zig",
548    };
549    const html_render_src_path: Cache.Path = .{
550        .root_dir = graph.zig_lib_directory,
551        .sub_path = "docs/wasm/html_render.zig",
552    };
553
554    var argv: std.ArrayList([]const u8) = .empty;
555
556    try argv.appendSlice(arena, &.{
557        graph.zig_exe, "build-exe", //
558        "-fno-entry", //
559        "-O", @tagName(optimize), //
560        "-target", arch_os_abi, //
561        "-mcpu", cpu_features, //
562        "--cache-dir", graph.global_cache_root.path orelse ".", //
563        "--global-cache-dir", graph.global_cache_root.path orelse ".", //
564        "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
565        "--name", root_name, //
566        "-rdynamic", //
567        "-fsingle-threaded", //
568        "--dep", "Walk", //
569        "--dep", "html_render", //
570        try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), //
571        try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), //
572        "--dep", "Walk", //
573        try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), //
574        "--listen=-",
575    });
576
577    var child: std.process.Child = .init(argv.items, gpa);
578    child.stdin_behavior = .Pipe;
579    child.stdout_behavior = .Pipe;
580    child.stderr_behavior = .Pipe;
581    try child.spawn();
582
583    var poller = Io.poll(gpa, enum { stdout, stderr }, .{
584        .stdout = child.stdout.?,
585        .stderr = child.stderr.?,
586    });
587    defer poller.deinit();
588
589    try child.stdin.?.writeAll(@ptrCast(@as([]const std.zig.Client.Message.Header, &.{
590        .{ .tag = .update, .bytes_len = 0 },
591        .{ .tag = .exit, .bytes_len = 0 },
592    })));
593
594    const Header = std.zig.Server.Message.Header;
595    var result: ?Cache.Path = null;
596    var result_error_bundle = std.zig.ErrorBundle.empty;
597
598    const stdout = poller.reader(.stdout);
599
600    poll: while (true) {
601        while (stdout.buffered().len < @sizeOf(Header)) if (!(try poller.poll())) break :poll;
602        const header = stdout.takeStruct(Header, .little) catch unreachable;
603        while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
604        const body = stdout.take(header.bytes_len) catch unreachable;
605
606        switch (header.tag) {
607            .zig_version => {
608                if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
609                    return error.ZigProtocolVersionMismatch;
610                }
611            },
612            .error_bundle => {
613                result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
614            },
615            .emit_digest => {
616                const EmitDigest = std.zig.Server.Message.EmitDigest;
617                const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
618                if (!ebp_hdr.flags.cache_hit) {
619                    log.info("source changes detected; rebuilt wasm component", .{});
620                }
621                const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
622                result = .{
623                    .root_dir = graph.global_cache_root,
624                    .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
625                };
626            },
627            else => {}, // ignore other messages
628        }
629    }
630
631    const stderr_contents = try poller.toOwnedSlice(.stderr);
632    if (stderr_contents.len > 0) {
633        std.debug.print("{s}", .{stderr_contents});
634    }
635
636    // Send EOF to stdin.
637    child.stdin.?.close();
638    child.stdin = null;
639
640    switch (try child.wait()) {
641        .Exited => |code| {
642            if (code != 0) {
643                log.err(
644                    "the following command exited with error code {d}:\n{s}",
645                    .{ code, try Build.Step.allocPrintCmd(arena, null, argv.items) },
646                );
647                return error.WasmCompilationFailed;
648            }
649        },
650        .Signal, .Stopped, .Unknown => {
651            log.err(
652                "the following command terminated unexpectedly:\n{s}",
653                .{try Build.Step.allocPrintCmd(arena, null, argv.items)},
654            );
655            return error.WasmCompilationFailed;
656        },
657    }
658
659    if (result_error_bundle.errorMessageCount() > 0) {
660        result_error_bundle.renderToStdErr(.{}, .auto);
661        log.err("the following command failed with {d} compilation errors:\n{s}", .{
662            result_error_bundle.errorMessageCount(),
663            try Build.Step.allocPrintCmd(arena, null, argv.items),
664        });
665        return error.WasmCompilationFailed;
666    }
667
668    const base_path = result orelse {
669        log.err("child process failed to report result\n{s}", .{
670            try Build.Step.allocPrintCmd(arena, null, argv.items),
671        });
672        return error.WasmCompilationFailed;
673    };
674    const bin_name = try std.zig.binNameAlloc(arena, .{
675        .root_name = root_name,
676        .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
677            .arch_os_abi = arch_os_abi,
678            .cpu_features = cpu_features,
679        }) catch unreachable) catch unreachable),
680        .output_mode = .Exe,
681    });
682    return base_path.join(arena, bin_name);
683}
684
685pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
686    compile: *Build.Step.Compile,
687
688    use_llvm: bool,
689    stats: abi.time_report.CompileResult.Stats,
690    ns_total: u64,
691
692    llvm_pass_timings_len: u32,
693    files_len: u32,
694    decls_len: u32,
695
696    /// The trailing data of `abi.time_report.CompileResult`, except the step name.
697    trailing: []const u8,
698}) void {
699    const gpa = ws.gpa;
700    const io = ws.graph.io;
701
702    const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
703        if (s == &opts.compile.step) break @intCast(i);
704    } else unreachable;
705
706    const old_buf = old: {
707        ws.time_report_mutex.lock(io) catch return;
708        defer ws.time_report_mutex.unlock(io);
709        const old = ws.time_report_msgs[step_idx];
710        ws.time_report_msgs[step_idx] = &.{};
711        break :old old;
712    };
713    const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
714
715    const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]);
716    out_header.* = .{
717        .step_idx = step_idx,
718        .flags = .{
719            .use_llvm = opts.use_llvm,
720        },
721        .stats = opts.stats,
722        .ns_total = opts.ns_total,
723        .llvm_pass_timings_len = opts.llvm_pass_timings_len,
724        .files_len = opts.files_len,
725        .decls_len = opts.decls_len,
726    };
727    @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
728
729    {
730        ws.time_report_mutex.lock(io) catch return;
731        defer ws.time_report_mutex.unlock(io);
732        assert(ws.time_report_msgs[step_idx].len == 0);
733        ws.time_report_msgs[step_idx] = buf;
734        ws.time_report_update_times[step_idx] = ws.now();
735    }
736    ws.notifyUpdate();
737}
738
739pub fn updateTimeReportGeneric(ws: *WebServer, step: *Build.Step, ns_total: u64) void {
740    const gpa = ws.gpa;
741    const io = ws.graph.io;
742
743    const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
744        if (s == step) break @intCast(i);
745    } else unreachable;
746
747    const old_buf = old: {
748        ws.time_report_mutex.lock(io) catch return;
749        defer ws.time_report_mutex.unlock(io);
750        const old = ws.time_report_msgs[step_idx];
751        ws.time_report_msgs[step_idx] = &.{};
752        break :old old;
753    };
754    const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
755    const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf);
756    out.* = .{
757        .step_idx = step_idx,
758        .ns_total = ns_total,
759    };
760    {
761        ws.time_report_mutex.lock(io) catch return;
762        defer ws.time_report_mutex.unlock(io);
763        assert(ws.time_report_msgs[step_idx].len == 0);
764        ws.time_report_msgs[step_idx] = buf;
765        ws.time_report_update_times[step_idx] = ws.now();
766    }
767    ws.notifyUpdate();
768}
769
770pub fn updateTimeReportRunTest(
771    ws: *WebServer,
772    run: *Build.Step.Run,
773    tests: *const Build.Step.Run.CachedTestMetadata,
774    ns_per_test: []const u64,
775) void {
776    const gpa = ws.gpa;
777    const io = ws.graph.io;
778
779    const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
780        if (s == &run.step) break @intCast(i);
781    } else unreachable;
782
783    assert(tests.names.len == ns_per_test.len);
784    const tests_len: u32 = @intCast(tests.names.len);
785
786    const new_len: u64 = len: {
787        var names_len: u64 = 0;
788        for (0..tests_len) |i| {
789            names_len += tests.testName(@intCast(i)).len + 1;
790        }
791        break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
792    };
793    const old_buf = old: {
794        ws.time_report_mutex.lock(io) catch return;
795        defer ws.time_report_mutex.unlock(io);
796        const old = ws.time_report_msgs[step_idx];
797        ws.time_report_msgs[step_idx] = &.{};
798        break :old old;
799    };
800    const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
801
802    const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]);
803    out_header.* = .{
804        .step_idx = step_idx,
805        .tests_len = tests_len,
806    };
807    var offset: usize = @sizeOf(abi.time_report.RunTestResult);
808    const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]);
809    @memcpy(ns_per_test_out, ns_per_test);
810    offset += tests_len * 8;
811    for (0..tests_len) |i| {
812        const name = tests.testName(@intCast(i));
813        @memcpy(buf[offset..][0..name.len], name);
814        buf[offset..][name.len] = 0;
815        offset += name.len + 1;
816    }
817    assert(offset == buf.len);
818
819    {
820        ws.time_report_mutex.lock(io) catch return;
821        defer ws.time_report_mutex.unlock(io);
822        assert(ws.time_report_msgs[step_idx].len == 0);
823        ws.time_report_msgs[step_idx] = buf;
824        ws.time_report_update_times[step_idx] = ws.now();
825    }
826    ws.notifyUpdate();
827}
828
829const RunnerRequest = union(enum) {
830    rebuild,
831};
832pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
833    const io = ws.graph.io;
834    ws.runner_request_mutex.lock(io) catch return;
835    defer ws.runner_request_mutex.unlock(io);
836    if (ws.runner_request) |req| {
837        ws.runner_request = null;
838        ws.runner_request_empty_cond.signal();
839        return req;
840    }
841    return null;
842}
843pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
844    const io = ws.graph.io;
845    try ws.runner_request_mutex.lock(io);
846    defer ws.runner_request_mutex.unlock(io);
847    while (true) {
848        if (ws.runner_request) |req| {
849            ws.runner_request = null;
850            ws.runner_request_empty_cond.signal(io);
851            return req;
852        }
853        try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex);
854    }
855}
856
857const cache_control_header: http.Header = .{
858    .name = "Cache-Control",
859    .value = "max-age=0, must-revalidate",
860};
861
862const builtin = @import("builtin");
863
864const std = @import("std");
865const Io = std.Io;
866const net = std.Io.net;
867const assert = std.debug.assert;
868const mem = std.mem;
869const log = std.log.scoped(.web_server);
870const Allocator = std.mem.Allocator;
871const Build = std.Build;
872const Cache = Build.Cache;
873const Fuzz = Build.Fuzz;
874const abi = Build.abi;
875const http = std.http;
876
877const WebServer = @This();