authorgravatar for techatrix@mailbox.orgTechatrix <techatrix@mailbox.org> 2026-07-13 01:17:03+02:00
committergravatar for techatrix@mailbox.orgTechatrix <techatrix@mailbox.org> 2026-07-21 14:34:40+02:00
log0b681ec6c39bf49a590ce2a8fa46e91c08426c43
tree9e572431f48a4ecf0086b665137d90e27db5cb3e
parent7faf6be3535195b96b69fb9b8d12f16015d7ec36
signaturebadge-check Signed by SSH key SHA256:HYC3SjXQcAt6uwv9pu/6OoVQ2rUH8rb5zKiUHSe9uxk

std.zig.Client: introduction


6 files changed, 202 insertions(+), 191 deletions(-)

lib/compiler/Maker/Step.zig+10-8
......@@ -561,24 +561,26 @@ fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *Zi
561561 var result: ?Path = null;
562562 var eos_err: error{EndOfStream}!void = {};
563563
564 const stdout = zp.multi_reader.fileReader(0);
564 var client: std.zig.Client = .{
565 .in = zp.multi_reader.reader(0),
566 .out = undefined,
567 };
565568
566569 while (true) {
567 const Header = std.zig.Server.Message.Header;
568 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
569 error.EndOfStream => break,
570 error.ReadFailed => return stdout.err.?,
571 };
572 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
570 const header = client.receiveMessageWithMultiReader(&zp.multi_reader, .none) catch |err| switch (err) {
571 error.Timeout => unreachable,
573572 error.EndOfStream => |e| {
573 if (client.in.bufferedLen() == 0) break;
574574 // Better to report the crash with stderr below, but we set
575575 // this in case the child exits successfully while violating
576576 // this protocol.
577577 eos_err = e;
578578 break;
579579 },
580 error.ReadFailed => return stdout.err.?,
580 else => |e| return e,
581581 };
582 const body = client.in.take(header.bytes_len) catch unreachable;
583
582584 switch (header.tag) {
583585 .zig_version => {
584586 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
lib/compiler/Maker/Step/Run.zig+37-121
......@@ -384,13 +384,23 @@ fn waitZigTest(
384384 var sub_prog_node: ?std.Progress.Node = null;
385385 defer if (sub_prog_node) |n| n.end();
386386
387 const stdout = multi_reader.reader(0);
388 const stderr = multi_reader.reader(1);
389
390 var stdin_writer = child.stdin.?.writerStreaming(io, &.{});
391
392 var client: std.zig.Client = .{
393 .in = stdout,
394 .out = &stdin_writer.interface,
395 };
396
387397 if (opt_metadata.*) |*md| {
388398 // Previous unit test process died or was killed; we're continuing where it left off
389 requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
399 requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
390400 } else {
391401 // Running unit tests normally
392402 run.fuzz_tests.clearRetainingCapacity();
393 sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err };
403 client.serveBodylessMessage(.query_test_metadata) catch |err| return .{ .write_failed = err };
394404 }
395405
396406 var active_test_index: ?u32 = null;
......@@ -410,10 +420,6 @@ fn waitZigTest(
410420 .raw = .fromNanoseconds(ns),
411421 } else null;
412422
413 const stdout = multi_reader.reader(0);
414 const stderr = multi_reader.reader(1);
415 const Header = std.zig.Server.Message.Header;
416
417423 while (true) {
418424 const timeout: Io.Timeout = t: {
419425 const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
......@@ -421,46 +427,20 @@ fn waitZigTest(
421427 break :t .{ .deadline = last_update.addDuration(duration) };
422428 };
423429
424 // This block is exited when `stdout` contains enough bytes for a `Header`.
425 header_ready: {
426 if (stdout.buffered().len >= @sizeOf(Header)) {
427 // We already have one, no need to poll!
428 break :header_ready;
429 }
430
431 multi_reader.fill(64, timeout) catch |err| switch (err) {
432 error.Timeout => return .{ .timeout = .{
433 .active_test_index = active_test_index,
434 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
435 } },
436 error.EndOfStream => return .{ .no_poll = .{
437 .active_test_index = active_test_index,
438 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
439 } },
440 else => |e| return e,
441 };
442
443 continue;
444 }
445 // There is definitely a header available now -- read it.
446 const header = stdout.takeStruct(Header, .little) catch unreachable;
447
448 while (stdout.buffered().len < header.bytes_len) {
449 multi_reader.fill(64, timeout) catch |err| switch (err) {
450 error.Timeout => return .{ .timeout = .{
451 .active_test_index = active_test_index,
452 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
453 } },
454 error.EndOfStream => return .{ .no_poll = .{
455 .active_test_index = active_test_index,
456 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
457 } },
458 else => |e| return e,
459 };
460 }
461
462 const body = stdout.take(header.bytes_len) catch unreachable;
430 const header = client.receiveMessageWithMultiReader(multi_reader, timeout) catch |err| switch (err) {
431 error.Timeout => return .{ .timeout = .{
432 .active_test_index = active_test_index,
433 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
434 } },
435 error.EndOfStream => return .{ .no_poll = .{
436 .active_test_index = active_test_index,
437 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
438 } },
439 else => |e| return e,
440 };
441 const body = client.in.take(header.bytes_len) catch unreachable;
463442 var body_r: std.Io.Reader = .fixed(body);
443
464444 switch (header.tag) {
465445 .zig_version => {
466446 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail(
......@@ -500,7 +480,7 @@ fn waitZigTest(
500480 active_test_index = null;
501481 last_update = .now(io, .awake);
502482
503 requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
483 requestNextTest(&client, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
504484 },
505485 .test_started => {
506486 active_test_index = opt_metadata.*.?.next_index - 1;
......@@ -551,7 +531,7 @@ fn waitZigTest(
551531 md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
552532 last_update = now;
553533
554 requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
534 requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
555535 },
556536 else => {}, // ignore other messages
557537 }
......@@ -697,17 +677,18 @@ const FuzzTestRunner = struct {
697677
698678 for (0.., f.instances) |id, *instance| {
699679 const id32: u32 = @intCast(id);
680 var writer = instance.child.stdin.?.writerStreaming(io, &.{});
681 const client: std.zig.Client = .{
682 .in = undefined,
683 .out = &writer.interface,
684 };
700685 (switch (f.ctx.fuzz.mode) {
701 .forever => sendRunFuzzTestMessage(
702 io,
703 instance.child.stdin.?,
686 .forever => client.serveRunFuzzTestMessage(
704687 run.fuzz_tests.items,
705688 .forever,
706689 id32,
707690 ),
708 .limit => |limit| sendRunFuzzTestMessage(
709 io,
710 instance.child.stdin.?,
691 .limit => |limit| client.serveRunFuzzTestMessage(
711692 run.fuzz_tests.items,
712693 .iterations,
713694 limit.amount,
......@@ -1315,7 +1296,7 @@ pub const CachedTestMetadata = struct {
13151296 }
13161297};
13171298
1318fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
1299fn requestNextTest(client: *std.zig.Client, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
13191300 while (metadata.next_index < metadata.names.len) {
13201301 const i = metadata.next_index;
13211302 metadata.next_index += 1;
......@@ -1326,76 +1307,11 @@ fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node:
13261307 if (sub_prog_node.*) |n| n.end();
13271308 sub_prog_node.* = metadata.prog_node.start(name, 0);
13281309
1329 try sendRunTestMessage(io, in, .run_test, i);
1310 try client.serveRunTest(i);
13301311 return;
13311312 } else {
13321313 metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done
1333 try sendMessage(io, in, .exit);
1334 }
1335}
1336
1337fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
1338 const header: std.zig.Client.Message.Header = .{
1339 .tag = tag,
1340 .bytes_len = 0,
1341 };
1342 var w = file.writerStreaming(io, &.{});
1343 w.interface.writeStruct(header, .little) catch |err| switch (err) {
1344 error.WriteFailed => return w.err.?,
1345 };
1346}
1347
1348fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void {
1349 const header: std.zig.Client.Message.Header = .{
1350 .tag = tag,
1351 .bytes_len = 4,
1352 };
1353 var w = file.writerStreaming(io, &.{});
1354 w.interface.writeStruct(header, .little) catch |err| switch (err) {
1355 error.WriteFailed => return w.err.?,
1356 };
1357 w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
1358 error.WriteFailed => return w.err.?,
1359 };
1360}
1361
1362fn sendRunFuzzTestMessage(
1363 io: Io,
1364 file: Io.File,
1365 test_names: []const []const u8,
1366 kind: std.Build.abi.fuzz.LimitKind,
1367 amount_or_instance: u64,
1368) !void {
1369 const header: std.zig.Client.Message.Header = .{
1370 .tag = .start_fuzzing,
1371 .bytes_len = 1 + 8 + 4 + count: {
1372 var c: u32 = @intCast(test_names.len * 4);
1373 for (test_names) |name| {
1374 c += @intCast(name.len);
1375 }
1376 break :count c;
1377 },
1378 };
1379 var w = file.writerStreaming(io, &.{});
1380 w.interface.writeStruct(header, .little) catch |err| switch (err) {
1381 error.WriteFailed => return w.err.?,
1382 };
1383 w.interface.writeByte(@backingInt(kind)) catch |err| switch (err) {
1384 error.WriteFailed => return w.err.?,
1385 };
1386 w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) {
1387 error.WriteFailed => return w.err.?,
1388 };
1389 w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) {
1390 error.WriteFailed => return w.err.?,
1391 };
1392 for (test_names) |test_name| {
1393 w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) {
1394 error.WriteFailed => return w.err.?,
1395 };
1396 w.interface.writeAll(test_name) catch |err| switch (err) {
1397 error.WriteFailed => return w.err.?,
1398 };
1314 try client.serveBodylessMessage(.exit);
13991315 }
14001316}
14011317
lib/compiler/std-docs.zig+21-22
......@@ -346,29 +346,39 @@ fn buildWasmBinary(
346346 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
347347 defer multi_reader.deinit();
348348
349 try sendMessage(io, child.stdin.?, .update);
350 try sendMessage(io, child.stdin.?, .exit);
349 const stdout = multi_reader.reader(0);
350
351 var stdin_buffer: [256]u8 = undefined;
352 var stdin_writer = child.stdin.?.writerStreaming(io, &stdin_buffer);
353
354 var client: std.zig.Client = .{
355 .in = stdout,
356 .out = &stdin_writer.interface,
357 };
358
359 try client.serveMessageHeader(.{ .tag = .update, .bytes_len = 0 });
360 try client.serveMessageHeader(.{ .tag = .exit, .bytes_len = 0 });
361 try client.out.flush();
351362
352363 var result: ?Cache.Path = null;
353364 var result_error_bundle = std.zig.ErrorBundle.empty;
354365
355 const stdout = multi_reader.fileReader(0);
356 const MessageHeader = std.zig.Server.Message.Header;
357
358366 var eos_err: error{EndOfStream}!void = {};
359367
360368 while (true) {
361 const header = stdout.interface.takeStruct(MessageHeader, .little) catch |err| switch (err) {
362 error.EndOfStream => break,
363 error.ReadFailed => return stdout.err.?,
364 };
365 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
369 const header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) {
370 error.Timeout => unreachable,
366371 error.EndOfStream => |e| {
372 if (client.in.bufferedLen() == 0) break;
373 // Better to report the crash with stderr below, but we set
374 // this in case the child exits successfully while violating
375 // this protocol.
367376 eos_err = e;
368377 break;
369378 },
370 error.ReadFailed => return stdout.err.?,
379 else => |e| return e,
371380 };
381 const body = client.in.take(header.bytes_len) catch unreachable;
372382
373383 switch (header.tag) {
374384 .zig_version => {
......@@ -435,17 +445,6 @@ fn buildWasmBinary(
435445 };
436446}
437447
438fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
439 const header: std.zig.Client.Message.Header = .{
440 .tag = tag,
441 .bytes_len = 0,
442 };
443 var w = file.writer(io, &.{});
444 w.interface.writeStruct(header, .little) catch |err| switch (err) {
445 error.WriteFailed => return w.err.?,
446 };
447}
448
449448fn openBrowserTab(io: Io, url: []const u8) !void {
450449 // Until https://github.com/ziglang/zig/issues/19205 is implemented, we
451450 // spawn and then leak a concurrent task for this child process.
lib/std/zig/Client.zig+88-2
......@@ -1,3 +1,17 @@
1const Client = @This();
2
3const std = @import("std");
4const Io = std.Io;
5const Allocator = std.mem.Allocator;
6const assert = std.debug.assert;
7const OutMessage = std.zig.Client.Message;
8const InMessage = std.zig.Server.Message;
9const Reader = Io.Reader;
10const Writer = Io.Writer;
11
12in: *Reader,
13out: *Writer,
14
115pub const Message = struct {
216 pub const Header = extern struct {
317 tag: Tag,
......@@ -50,7 +64,79 @@ pub const Message = struct {
5064 };
5165
5266 comptime {
53 const std = @import("std");
54 std.debug.assert(@sizeOf(std.Build.abi.fuzz.LimitKind) == 1);
67 assert(@sizeOf(std.Build.abi.fuzz.LimitKind) == 1);
5568 }
5669};
70
71pub fn receiveMessage(c: *const Client) Reader.Error!InMessage.Header {
72 return c.in.takeStruct(InMessage.Header, .little);
73}
74
75/// Assumes that `c.in` is a reader in `multi_reader`.
76/// Guarantees that the response body will be buffered in `c.in` on success.
77pub fn receiveMessageWithMultiReader(
78 c: *Client,
79 multi_reader: *Io.File.MultiReader,
80 timeout: Io.Timeout,
81) (Io.File.MultiReader.Error || Io.Timeout.Error)!InMessage.Header {
82 while (c.in.bufferedLen() < @sizeOf(InMessage.Header)) {
83 multi_reader.fill(64, timeout) catch |err| switch (err) {
84 error.Canceled,
85 error.Timeout,
86 error.ConcurrencyUnavailable,
87 error.EndOfStream,
88 => |e| return e,
89 };
90 }
91 const header = c.in.takeStruct(InMessage.Header, .little) catch unreachable;
92 while (c.in.bufferedLen() < header.bytes_len) {
93 try multi_reader.fill(header.bytes_len - c.in.bufferedLen(), timeout);
94 }
95 try multi_reader.checkAnyError();
96 return header;
97}
98
99/// Don't forget to flush!
100pub fn serveMessageHeader(c: *const Client, header: OutMessage.Header) Writer.Error!void {
101 try c.out.writeStruct(header, .little);
102}
103
104pub fn serveBodylessMessage(c: *const Client, tag: OutMessage.Tag) Writer.Error!void {
105 try c.serveMessageHeader(.{ .tag = tag, .bytes_len = 0 });
106 try c.out.flush();
107}
108
109pub fn serveRunTest(c: *const Client, index: u32) !void {
110 try c.serveMessageHeader(.{
111 .tag = .run_test,
112 .bytes_len = @sizeOf(u32),
113 });
114 try c.out.writeInt(u32, index, .little);
115 try c.out.flush();
116}
117
118pub fn serveRunFuzzTestMessage(
119 c: *const Client,
120 test_names: []const []const u8,
121 kind: std.Build.abi.fuzz.LimitKind,
122 amount_or_instance: u64,
123) !void {
124 try c.serveMessageHeader(.{
125 .tag = .start_fuzzing,
126 .bytes_len = 1 + 8 + 4 + count: {
127 var bytes_len: u32 = @intCast(test_names.len * 4);
128 for (test_names) |name| {
129 bytes_len += @intCast(name.len);
130 }
131 break :count bytes_len;
132 },
133 });
134 try c.out.writeByte(@backingInt(kind));
135 try c.out.writeInt(u64, amount_or_instance, .little);
136 try c.out.writeInt(u32, @intCast(test_names.len), .little);
137 for (test_names) |test_name| {
138 try c.out.writeInt(u32, @intCast(test_name.len), .little);
139 try c.out.writeAll(test_name);
140 }
141 try c.out.flush();
142}
src/Compilation.zig+12-8
......@@ -6006,26 +6006,30 @@ fn spawnZigRc(
60066006 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
60076007 defer multi_reader.deinit();
60086008
6009 const stdout = multi_reader.fileReader(0);
6010 const MessageHeader = std.zig.Server.Message.Header;
6009 const stdout = multi_reader.reader(0);
60116010
60126011 var eos_err: error{EndOfStream}!void = {};
60136012
6013 var client: std.zig.Client = .{
6014 .in = stdout,
6015 .out = undefined,
6016 };
6017
60146018 while (true) {
6015 const header = stdout.interface.takeStruct(MessageHeader, .little) catch |err| switch (err) {
6016 error.EndOfStream => break,
6017 error.ReadFailed => return stdout.err.?,
6018 };
6019 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
6019 const header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) {
6020 error.Timeout => unreachable,
60206021 error.EndOfStream => |e| {
6022 if (client.in.bufferedLen() == 0) break;
60216023 // Better to report the crash with stderr below, but we set
60226024 // this in case the child exits successfully while violating
60236025 // this protocol.
60246026 eos_err = e;
60256027 break;
60266028 },
6027 error.ReadFailed => return stdout.err.?,
6029 else => |e| return e,
60286030 };
6031 const body = client.in.take(header.bytes_len) catch unreachable;
6032
60296033 switch (header.tag) {
60306034 // We expect exactly one ErrorBundle, and if any error_bundle header is
60316035 // sent then it's a fatal error.
tools/incr-check.zig+34-30
......@@ -305,21 +305,23 @@ const Eval = struct {
305305
306306 fn check(eval: *Eval, mr: *Io.File.MultiReader, update: Case.Update, prog_node: std.Progress.Node) !void {
307307 const arena = eval.arena;
308 const stdout = mr.fileReader(0);
309 const stderr = &mr.fileReader(1).interface;
310 const Header = std.zig.Server.Message.Header;
308 const stdout = mr.reader(0);
309 const stderr = mr.reader(1);
310
311 var client: std.zig.Client = .{
312 .in = stdout,
313 .out = undefined,
314 };
311315
312316 while (true) {
313 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
314 error.EndOfStream => break,
315 error.ReadFailed => return stdout.err.?,
316 };
317 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
317 const header = client.receiveMessageWithMultiReader(mr, .none) catch |err| switch (err) {
318 error.Timeout => unreachable,
318319 // If this panic triggers it might be helpful to rework this
319320 // code to print the stderr from the abnormally terminated child.
320321 error.EndOfStream => @panic("unexpected mid-message end of stream"),
321 error.ReadFailed => return stdout.err.?,
322 else => |e| return e,
322323 };
324 const body = client.in.take(header.bytes_len) catch unreachable;
323325
324326 switch (header.tag) {
325327 .error_bundle => {
......@@ -605,12 +607,13 @@ const Eval = struct {
605607
606608 fn requestUpdate(eval: *Eval) !void {
607609 const io = eval.io;
608 const header: std.zig.Client.Message.Header = .{
609 .tag = .update,
610 .bytes_len = 0,
610
611 var w = eval.child.stdin.?.writerStreaming(io, &.{});
612 var client: std.zig.Client = .{
613 .in = undefined,
614 .out = &w.interface,
611615 };
612 var w = eval.child.stdin.?.writer(io, &.{});
613 w.interface.writeStruct(header, .little) catch |err| switch (err) {
616 client.serveBodylessMessage(.update) catch |err| switch (err) {
614617 error.WriteFailed => return w.err.?,
615618 };
616619 }
......@@ -618,22 +621,23 @@ const Eval = struct {
618621 fn end(eval: *Eval, mr: *Io.File.MultiReader) !void {
619622 requestExit(eval.child, eval);
620623
621 const stdout = mr.fileReader(0);
622 const Header = std.zig.Server.Message.Header;
624 var client: std.zig.Client = .{
625 .in = mr.reader(0),
626 .out = undefined,
627 };
623628
624629 while (true) {
625 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
626 error.EndOfStream => break,
627 error.ReadFailed => return stdout.err.?,
628 };
629 stdout.interface.discardAll(header.bytes_len) catch |err| switch (err) {
630 error.ReadFailed => return stdout.err.?,
631 error.EndOfStream => |e| return e,
630 const header = client.receiveMessageWithMultiReader(mr, .none) catch |err| switch (err) {
631 error.Timeout => unreachable,
632 error.EndOfStream => |e| {
633 if (client.in.bufferedLen() == 0) break;
634 return e;
635 },
636 else => |e| return e,
632637 };
638 try client.in.discardAll(header.bytes_len);
633639 }
634640
635 try mr.fillRemaining(.none);
636
637641 const stderr = mr.reader(1).buffered();
638642 if (stderr.len > 0) eval.fatal("unexpected stderr:\n{s}", .{stderr});
639643 }
......@@ -899,12 +903,12 @@ fn requestExit(child: *std.process.Child, eval: *Eval) void {
899903 if (child.stdin == null) return;
900904 const io = eval.io;
901905
902 const header: std.zig.Client.Message.Header = .{
903 .tag = .exit,
904 .bytes_len = 0,
906 var w = eval.child.stdin.?.writerStreaming(io, &.{});
907 var client: std.zig.Client = .{
908 .in = undefined,
909 .out = &w.interface,
905910 };
906 var w = eval.child.stdin.?.writer(io, &.{});
907 w.interface.writeStruct(header, .little) catch |err| switch (err) {
911 client.serveBodylessMessage(.exit) catch |err| switch (err) {
908912 error.WriteFailed => switch (w.err.?) {
909913 error.BrokenPipe => {},
910914 else => |e| eval.fatal("failed to send exit: {t}", .{e}),