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