| 1 | const std = @import("std"); |
| 2 | const assert = std.debug.assert; |
| 3 | const abi = std.Build.abi; |
| 4 | const gpa = std.heap.wasm_allocator; |
| 5 | const log = std.log; |
| 6 | const Allocator = std.mem.Allocator; |
| 7 | |
| 8 | const fuzz = @import("fuzz.zig"); |
| 9 | const time_report = @import("time_report.zig"); |
| 10 | |
| 11 | /// Nanoseconds. |
| 12 | var server_base_timestamp: i64 = 0; |
| 13 | /// Milliseconds. |
| 14 | var client_base_timestamp: i64 = 0; |
| 15 | |
| 16 | pub var step_list: []Step = &.{}; |
| 17 | /// Not accessed after initialization, but must be freed alongside `step_list`. |
| 18 | pub var step_list_data: []u8 = &.{}; |
| 19 | |
| 20 | const Step = struct { |
| 21 | name: []const u8, |
| 22 | status: abi.StepUpdate.Status, |
| 23 | }; |
| 24 | |
| 25 | const js = struct { |
| 26 | extern "core" fn log(ptr: [*]const u8, len: usize) void; |
| 27 | extern "core" fn panic(ptr: [*]const u8, len: usize) noreturn; |
| 28 | extern "core" fn timestamp() i64; |
| 29 | extern "core" fn hello( |
| 30 | steps_len: u32, |
| 31 | status: abi.BuildStatus, |
| 32 | time_report: bool, |
| 33 | ) void; |
| 34 | extern "core" fn updateBuildStatus(status: abi.BuildStatus) void; |
| 35 | extern "core" fn updateStepStatus(step_idx: u32) void; |
| 36 | extern "core" fn sendWsMessage(ptr: [*]const u8, len: usize) void; |
| 37 | }; |
| 38 | |
| 39 | pub const std_options: std.Options = .{ |
| 40 | .logFn = logFn, |
| 41 | }; |
| 42 | |
| 43 | pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn { |
| 44 | _ = st; |
| 45 | _ = addr; |
| 46 | log.err("panic: {s}", .{msg}); |
| 47 | @trap(); |
| 48 | } |
| 49 | |
| 50 | fn logFn( |
| 51 | comptime message_level: log.Level, |
| 52 | comptime scope: @EnumLiteral(), |
| 53 | comptime format: []const u8, |
| 54 | args: anytype, |
| 55 | ) void { |
| 56 | const level_txt = comptime message_level.asText(); |
| 57 | const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; |
| 58 | var buf: [500]u8 = undefined; |
| 59 | const line = std.mem.print(&buf, level_txt ++ prefix2 ++ format, args) catch l: { |
| 60 | buf[buf.len - 3 ..][0..3].* = "...".*; |
| 61 | break :l &buf; |
| 62 | }; |
| 63 | js.log(line.ptr, line.len); |
| 64 | } |
| 65 | |
| 66 | export fn alloc(n: usize) [*]u8 { |
| 67 | const slice = gpa.alloc(u8, n) catch @panic("OOM"); |
| 68 | return slice.ptr; |
| 69 | } |
| 70 | |
| 71 | var message_buffer: std.ArrayListAlignedUnmanaged(u8, .of(u64)) = .empty; |
| 72 | |
| 73 | /// Resizes the message buffer to be the correct length; returns the pointer to |
| 74 | /// the query string. |
| 75 | export fn message_begin(len: usize) [*]u8 { |
| 76 | message_buffer.resize(gpa, len) catch @panic("OOM"); |
| 77 | return message_buffer.items.ptr; |
| 78 | } |
| 79 | |
| 80 | export fn message_end() void { |
| 81 | const msg_bytes = message_buffer.items; |
| 82 | |
| 83 | const tag: abi.ToClientTag = @fromBackingInt(@intCast(msg_bytes[0])); |
| 84 | switch (tag) { |
| 85 | _ => @panic("malformed message"), |
| 86 | |
| 87 | .hello => return helloMessage(msg_bytes) catch @panic("OOM"), |
| 88 | .status_update => return statusUpdateMessage(msg_bytes) catch @panic("OOM"), |
| 89 | .step_update => return stepUpdateMessage(msg_bytes) catch @panic("OOM"), |
| 90 | |
| 91 | .fuzz_source_index => return fuzz.sourceIndexMessage(msg_bytes) catch @panic("OOM"), |
| 92 | .fuzz_coverage_update => return fuzz.coverageUpdateMessage(msg_bytes) catch @panic("OOM"), |
| 93 | .fuzz_entry_points => return fuzz.entryPointsMessage(msg_bytes) catch @panic("OOM"), |
| 94 | |
| 95 | .time_report_generic_result => return time_report.genericResultMessage(msg_bytes) catch @panic("OOM"), |
| 96 | .time_report_compile_result => return time_report.compileResultMessage(msg_bytes) catch @panic("OOM"), |
| 97 | .time_report_run_test_result => return time_report.runTestResultMessage(msg_bytes) catch @panic("OOM"), |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | const String = Slice(u8); |
| 102 | |
| 103 | pub fn Slice(T: type) type { |
| 104 | return packed struct(u64) { |
| 105 | ptr: u32, |
| 106 | len: u32, |
| 107 | |
| 108 | pub fn init(s: []const T) @This() { |
| 109 | return .{ |
| 110 | .ptr = @intFromPtr(s.ptr), |
| 111 | .len = s.len, |
| 112 | }; |
| 113 | } |
| 114 | }; |
| 115 | } |
| 116 | |
| 117 | pub fn fatal(comptime format: []const u8, args: anytype) noreturn { |
| 118 | var buf: [500]u8 = undefined; |
| 119 | const line = std.mem.print(&buf, format, args) catch l: { |
| 120 | buf[buf.len - 3 ..][0..3].* = "...".*; |
| 121 | break :l &buf; |
| 122 | }; |
| 123 | js.panic(line.ptr, line.len); |
| 124 | } |
| 125 | |
| 126 | fn helloMessage(msg_bytes: []align(4) u8) Allocator.Error!void { |
| 127 | if (msg_bytes.len < @sizeOf(abi.Hello)) @panic("malformed Hello message"); |
| 128 | const hdr: *const abi.Hello = @ptrCast(msg_bytes[0..@sizeOf(abi.Hello)]); |
| 129 | const trailing = msg_bytes[@sizeOf(abi.Hello)..]; |
| 130 | |
| 131 | client_base_timestamp = js.timestamp(); |
| 132 | server_base_timestamp = hdr.timestamp; |
| 133 | |
| 134 | const steps = try gpa.alloc(Step, hdr.steps_len); |
| 135 | errdefer gpa.free(steps); |
| 136 | |
| 137 | const step_name_lens: []align(1) const u32 = @ptrCast(trailing[0 .. steps.len * 4]); |
| 138 | |
| 139 | const step_name_data_len: usize = len: { |
| 140 | var sum: usize = 0; |
| 141 | for (step_name_lens) |n| sum += n; |
| 142 | break :len sum; |
| 143 | }; |
| 144 | const step_name_data: []const u8 = trailing[steps.len * 4 ..][0..step_name_data_len]; |
| 145 | const step_status_bits: []const u8 = trailing[steps.len * 4 + step_name_data_len ..]; |
| 146 | |
| 147 | const duped_step_name_data = try gpa.dupe(u8, step_name_data); |
| 148 | errdefer gpa.free(duped_step_name_data); |
| 149 | |
| 150 | var name_off: usize = 0; |
| 151 | for (steps, step_name_lens, 0..) |*step_out, name_len, step_idx| { |
| 152 | step_out.* = .{ |
| 153 | .name = duped_step_name_data[name_off..][0..name_len], |
| 154 | .status = @fromBackingInt(@intCast(@as(u2, @truncate(step_status_bits[step_idx / 4] >> @intCast((step_idx % 4) * 2))))), |
| 155 | }; |
| 156 | name_off += name_len; |
| 157 | } |
| 158 | |
| 159 | gpa.free(step_list); |
| 160 | gpa.free(step_list_data); |
| 161 | step_list = steps; |
| 162 | step_list_data = duped_step_name_data; |
| 163 | |
| 164 | js.hello(step_list.len, hdr.status, hdr.flags.time_report); |
| 165 | } |
| 166 | fn statusUpdateMessage(msg_bytes: []u8) Allocator.Error!void { |
| 167 | if (msg_bytes.len < @sizeOf(abi.StatusUpdate)) @panic("malformed StatusUpdate message"); |
| 168 | const msg: *const abi.StatusUpdate = @ptrCast(msg_bytes[0..@sizeOf(abi.StatusUpdate)]); |
| 169 | js.updateBuildStatus(msg.new); |
| 170 | } |
| 171 | fn stepUpdateMessage(msg_bytes: []u8) Allocator.Error!void { |
| 172 | if (msg_bytes.len < @sizeOf(abi.StepUpdate)) @panic("malformed StepUpdate message"); |
| 173 | const msg: *const abi.StepUpdate = @ptrCast(msg_bytes[0..@sizeOf(abi.StepUpdate)]); |
| 174 | if (msg.step_idx >= step_list.len) @panic("malformed StepUpdate message"); |
| 175 | step_list[msg.step_idx].status = msg.bits.status; |
| 176 | js.updateStepStatus(msg.step_idx); |
| 177 | } |
| 178 | |
| 179 | export fn stepName(idx: usize) String { |
| 180 | return .init(step_list[idx].name); |
| 181 | } |
| 182 | export fn stepStatus(idx: usize) u8 { |
| 183 | return @backingInt(step_list[idx].status); |
| 184 | } |
| 185 | |
| 186 | export fn rebuild() void { |
| 187 | const msg: abi.Rebuild = .{}; |
| 188 | const raw: []const u8 = @ptrCast(&msg); |
| 189 | js.sendWsMessage(raw.ptr, raw.len); |
| 190 | } |
| 191 | |
| 192 | /// Nanoseconds passed since a server timestamp. |
| 193 | pub fn nsSince(server_timestamp: i64) i64 { |
| 194 | const ms_passed = js.timestamp() - client_base_timestamp; |
| 195 | const ns_passed = server_base_timestamp - server_timestamp; |
| 196 | return ns_passed + ms_passed * std.time.ns_per_ms; |
| 197 | } |
| 198 | |
| 199 | pub fn fmtEscapeHtml(unescaped: []const u8) HtmlEscaper { |
| 200 | return .{ .unescaped = unescaped }; |
| 201 | } |
| 202 | const HtmlEscaper = struct { |
| 203 | unescaped: []const u8, |
| 204 | pub fn format(he: HtmlEscaper, w: *std.Io.Writer) !void { |
| 205 | for (he.unescaped) |c| switch (c) { |
| 206 | '&' => try w.writeAll("&amp;"), |
| 207 | '<' => try w.writeAll("&lt;"), |
| 208 | '>' => try w.writeAll("&gt;"), |
| 209 | '"' => try w.writeAll("&quot;"), |
| 210 | '\'' => try w.writeAll("&#39;"), |
| 211 | else => try w.writeByte(c), |
| 212 | }; |
| 213 | } |
| 214 | }; |