authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-09 22:10:12-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-23 22:15:09-08:00
logffcbd48a1220ce6d652ee762001d88baa385de49
treee1ea279b4cd0b8991df04d8a799589f72dbffd86
parent78d262d96ee6200c7a6bc0a41fe536d263c24d92

std: rework TTY detection and printing

This commit sketches an idea for how to deal with detection of file streams as being terminals. When a File stream is a terminal, writes through the stream should have their escapes stripped unless the programmer explicitly enables terminal escapes. Furthermore, the programmer needs a convenient API for intentionally outputting escapes into the stream. In particular it should be possible to set colors that are silently discarded when the stream is not a terminal. This commit makes `Io.File.Writer` track the terminal mode in the already-existing `mode` field, making it the appropriate place to implement escape stripping. `Io.lockStderrWriter` returns a `*Io.File.Writer` with terminal detection already done by default. This is a higher-level application layer stream for writing to stderr. Meanwhile, `std.debug.lockStderrWriter` also returns a `*Io.File.Writer` but a lower-level one that is hard-coded to use a static single-threaded `std.Io.Threaded` instance. This is the same instance that is used for collecting debug information and iterating the unwind info.

10 files changed, 448 insertions(+), 400 deletions(-)

lib/std/Io.zig+11-11
......@@ -82,8 +82,6 @@ pub const Limit = enum(usize) {
8282pub const Reader = @import("Io/Reader.zig");
8383pub const Writer = @import("Io/Writer.zig");
8484
85pub const tty = @import("Io/tty.zig");
86
8785pub fn poll(
8886 gpa: Allocator,
8987 comptime StreamEnum: type,
......@@ -535,7 +533,6 @@ test {
535533 _ = net;
536534 _ = Reader;
537535 _ = Writer;
538 _ = tty;
539536 _ = Evented;
540537 _ = Threaded;
541538 _ = @import("Io/test.zig");
......@@ -720,6 +717,9 @@ pub const VTable = struct {
720717
721718 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
722719 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
720 lockStderrWriter: *const fn (?*anyopaque, buffer: []u8) Cancelable!*File.Writer,
721 tryLockStderrWriter: *const fn (?*anyopaque, buffer: []u8) ?*File.Writer,
722 unlockStderrWriter: *const fn (?*anyopaque) void,
723723
724724 now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp,
725725 sleep: *const fn (?*anyopaque, Timeout) SleepError!void,
......@@ -740,10 +740,6 @@ pub const VTable = struct {
740740 netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface,
741741 netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name,
742742 netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) net.HostName.LookupError!void,
743
744 lockStderrWriter: *const fn (?*anyopaque, buffer: []u8) Cancelable!*Writer,
745 tryLockStderrWriter: *const fn (?*anyopaque, buffer: []u8) ?*Writer,
746 unlockStderrWriter: *const fn (?*anyopaque) void,
747743};
748744
749745pub const Cancelable = error{
......@@ -2186,13 +2182,17 @@ pub fn select(io: Io, s: anytype) Cancelable!SelectUnion(@TypeOf(s)) {
21862182///
21872183/// See also:
21882184/// * `tryLockStderrWriter`
2189pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*Writer {
2190 return io.vtable.lockStderrWriter(io.userdata, buffer);
2185pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*File.Writer {
2186 const result = try io.vtable.lockStderrWriter(io.userdata, buffer);
2187 result.io = io;
2188 return result;
21912189}
21922190
21932191/// Same as `lockStderrWriter` but uncancelable and non-blocking.
2194pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*Writer {
2195 return io.vtable.tryLockStderrWriter(io.userdata, buffer);
2192pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*File.Writer {
2193 const result = io.vtable.tryLockStderrWriter(io.userdata, buffer) orelse return null;
2194 result.io = io;
2195 return result;
21962196}
21972197
21982198pub fn unlockStderrWriter(io: Io) void {
lib/std/Io/File/Writer.zig+234-9
......@@ -1,4 +1,6 @@
11const Writer = @This();
2const builtin = @import("builtin");
3const is_windows = builtin.os.tag == .windows;
24
35const std = @import("../../std.zig");
46const Io = std.Io;
......@@ -16,7 +18,144 @@ write_file_err: ?WriteFileError = null,
1618seek_err: ?SeekError = null,
1719interface: Io.Writer,
1820
19pub const Mode = File.Reader.Mode;
21pub const Mode = union(enum) {
22 /// Uses `Io.VTable.fileWriteFileStreaming` if possible. Not a terminal.
23 /// `setColor` does nothing.
24 streaming,
25 /// Uses `Io.VTable.fileWriteFilePositional` if possible. Not a terminal.
26 /// `setColor` does nothing.
27 positional,
28 /// Avoids `Io.VTable.fileWriteFileStreaming`. Not a terminal. `setColor`
29 /// does nothing.
30 streaming_simple,
31 /// Avoids `Io.VTable.fileWriteFilePositional`. Not a terminal. `setColor`
32 /// does nothing.
33 positional_simple,
34 /// It's a terminal. Writes are escaped so as to strip escape sequences.
35 /// Color is enabled.
36 terminal_escaped,
37 /// It's a terminal. Colors are enabled via calling
38 /// SetConsoleTextAttribute. Writes are not escaped.
39 terminal_winapi: TerminalWinapi,
40 /// Indicates writing cannot continue because of a seek failure.
41 failure,
42
43 pub fn toStreaming(m: @This()) @This() {
44 return switch (m) {
45 .positional, .streaming => .streaming,
46 .positional_simple, .streaming_simple => .streaming_simple,
47 inline else => |_, x| x,
48 };
49 }
50
51 pub fn toSimple(m: @This()) @This() {
52 return switch (m) {
53 .positional, .positional_simple => .positional_simple,
54 .streaming, .streaming_simple => .streaming_simple,
55 inline else => |x| x,
56 };
57 }
58
59 pub fn toUnescaped(m: @This()) @This() {
60 return switch (m) {
61 .terminal_escaped => .streaming_simple,
62 inline else => |x| x,
63 };
64 }
65
66 pub const TerminalWinapi = if (!is_windows) noreturn else struct {
67 handle: File.Handle,
68 reset_attributes: u16,
69 };
70
71 /// Detect suitable TTY configuration options for the given file (commonly
72 /// stdout/stderr).
73 ///
74 /// Will attempt to enable ANSI escape code support if necessary/possible.
75 pub fn detect(io: Io, file: File, want_color: bool, fallback: Mode) Io.Cancelable!Mode {
76 if (!want_color) return if (try file.isTty(io)) .terminal_escaped else fallback;
77
78 if (file.enableAnsiEscapeCodes(io)) |_| {
79 return .terminal_escaped;
80 } else |err| switch (err) {
81 error.Canceled => return error.Canceled,
82 error.NotTerminalDevice, error.Unexpected => {},
83 }
84
85 if (is_windows and file.isTty(io)) {
86 const windows = std.os.windows;
87 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
88 if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.FALSE) {
89 return .{ .terminal_winapi = .{
90 .handle = file.handle,
91 .reset_attributes = info.wAttributes,
92 } };
93 }
94 return .terminal_escaped;
95 }
96
97 return fallback;
98 }
99
100 pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error;
101
102 pub fn setColor(mode: Mode, io_w: *Io.Writer, color: Color) Mode.SetColorError!void {
103 switch (mode) {
104 .streaming, .positional, .streaming_simple, .positional_simple, .failure => return,
105 .terminal_escaped => {
106 const color_string = switch (color) {
107 .black => "\x1b[30m",
108 .red => "\x1b[31m",
109 .green => "\x1b[32m",
110 .yellow => "\x1b[33m",
111 .blue => "\x1b[34m",
112 .magenta => "\x1b[35m",
113 .cyan => "\x1b[36m",
114 .white => "\x1b[37m",
115 .bright_black => "\x1b[90m",
116 .bright_red => "\x1b[91m",
117 .bright_green => "\x1b[92m",
118 .bright_yellow => "\x1b[93m",
119 .bright_blue => "\x1b[94m",
120 .bright_magenta => "\x1b[95m",
121 .bright_cyan => "\x1b[96m",
122 .bright_white => "\x1b[97m",
123 .bold => "\x1b[1m",
124 .dim => "\x1b[2m",
125 .reset => "\x1b[0m",
126 };
127 try io_w.writeAll(color_string);
128 },
129 .terminal_winapi => |ctx| {
130 const windows = std.os.windows;
131 const attributes: windows.WORD = switch (color) {
132 .black => 0,
133 .red => windows.FOREGROUND_RED,
134 .green => windows.FOREGROUND_GREEN,
135 .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN,
136 .blue => windows.FOREGROUND_BLUE,
137 .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE,
138 .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE,
139 .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE,
140 .bright_black => windows.FOREGROUND_INTENSITY,
141 .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY,
142 .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY,
143 .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY,
144 .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
145 .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
146 .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
147 .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
148 // "dim" is not supported using basic character attributes, but let's still make it do *something*.
149 // This matches the old behavior of TTY.Color before the bright variants were added.
150 .dim => windows.FOREGROUND_INTENSITY,
151 .reset => ctx.reset_attributes,
152 };
153 try io_w.flush();
154 try windows.SetConsoleTextAttribute(ctx.handle, attributes);
155 },
156 }
157 }
158};
20159
21160pub const Error = error{
22161 DiskQuota,
......@@ -74,6 +213,16 @@ pub fn initStreaming(file: File, io: Io, buffer: []u8) Writer {
74213 };
75214}
76215
216/// Detects if `file` is terminal and sets the mode accordingly.
217pub fn initDetect(file: File, io: Io, buffer: []u8) Io.Cancelable!Writer {
218 return .{
219 .io = io,
220 .file = file,
221 .interface = initInterface(buffer),
222 .mode = try .detect(io, file, true, .positional),
223 };
224}
225
77226pub fn initInterface(buffer: []u8) Io.Writer {
78227 return .{
79228 .vtable = &.{
......@@ -99,8 +248,9 @@ pub fn moveToReader(w: *Writer) File.Reader {
99248pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
100249 const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
101250 switch (w.mode) {
102 .positional, .positional_reading => return drainPositional(w, data, splat),
103 .streaming, .streaming_reading => return drainStreaming(w, data, splat),
251 .positional, .positional_simple => return drainPositional(w, data, splat),
252 .streaming, .streaming_simple, .terminal_winapi => return drainStreaming(w, data, splat),
253 .terminal_escaped => return drainEscaping(w, data, splat),
104254 .failure => return error.WriteFailed,
105255 }
106256}
......@@ -141,13 +291,38 @@ fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.
141291 return w.interface.consume(n);
142292}
143293
294fn findTerminalEscape(buffer: []const u8) ?usize {
295 return std.mem.findScalar(u8, buffer, 0x1b);
296}
297
298fn drainEscaping(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
299 const io = w.io;
300 const header = w.interface.buffered();
301 if (findTerminalEscape(header)) |i| {
302 _ = i;
303 @panic("TODO strip terminal escape sequence");
304 }
305 for (data) |d| {
306 if (findTerminalEscape(d)) |i| {
307 _ = i;
308 @panic("TODO strip terminal escape sequence");
309 }
310 }
311 const n = io.vtable.fileWriteStreaming(io.userdata, w.file, header, data, splat) catch |err| {
312 w.err = err;
313 return error.WriteFailed;
314 };
315 w.pos += n;
316 return w.interface.consume(n);
317}
318
144319pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
145320 const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
146321 switch (w.mode) {
147322 .positional => return sendFilePositional(w, file_reader, limit),
148 .positional_reading => return error.Unimplemented,
323 .positional_simple => return error.Unimplemented,
149324 .streaming => return sendFileStreaming(w, file_reader, limit),
150 .streaming_reading => return error.Unimplemented,
325 .streaming_simple, .terminal_escaped, .terminal_winapi => return error.Unimplemented,
151326 .failure => return error.WriteFailed,
152327 }
153328}
......@@ -214,10 +389,10 @@ pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void {
214389 assert(w.interface.buffered().len == 0);
215390 const io = w.io;
216391 switch (w.mode) {
217 .positional, .positional_reading => {
392 .positional, .positional_simple => {
218393 w.pos = offset;
219394 },
220 .streaming, .streaming_reading => {
395 .streaming, .streaming_simple, .terminal_escaped, .terminal_winapi => {
221396 if (w.seek_err) |err| return err;
222397 io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| {
223398 w.seek_err = err;
......@@ -243,15 +418,65 @@ pub fn end(w: *Writer) EndError!void {
243418 try w.interface.flush();
244419 switch (w.mode) {
245420 .positional,
246 .positional_reading,
421 .positional_simple,
247422 => w.file.setLength(io, w.pos) catch |err| switch (err) {
248423 error.NonResizable => return,
249424 else => |e| return e,
250425 },
251426
252427 .streaming,
253 .streaming_reading,
428 .streaming_simple,
254429 .failure,
255430 => {},
256431 }
257432}
433
434pub const Color = enum {
435 black,
436 red,
437 green,
438 yellow,
439 blue,
440 magenta,
441 cyan,
442 white,
443 bright_black,
444 bright_red,
445 bright_green,
446 bright_yellow,
447 bright_blue,
448 bright_magenta,
449 bright_cyan,
450 bright_white,
451 dim,
452 bold,
453 reset,
454};
455
456pub const SetColorError = Mode.SetColorError;
457
458pub fn setColor(w: *Writer, color: Color) SetColorError!void {
459 return w.mode.setColor(&w.interface, color);
460}
461
462pub fn disableEscape(w: *Writer) Mode {
463 const prev = w.mode;
464 w.mode = w.mode.toUnescaped();
465 return prev;
466}
467
468pub fn restoreEscape(w: *Writer, mode: Mode) void {
469 w.mode = mode;
470}
471
472pub fn writeAllUnescaped(w: *Writer, bytes: []const u8) Io.Error!void {
473 const prev_mode = w.disableEscape();
474 defer w.restoreEscape(prev_mode);
475 return w.interface.writeAll(bytes);
476}
477
478pub fn printUnescaped(w: *Writer, comptime fmt: []const u8, args: anytype) Io.Error!void {
479 const prev_mode = w.disableEscape();
480 defer w.restoreEscape(prev_mode);
481 return w.interface.print(fmt, args);
482}
lib/std/Io/Threaded.zig+34-13
......@@ -77,7 +77,13 @@ use_sendfile: UseSendfile = .default,
7777use_copy_file_range: UseCopyFileRange = .default,
7878use_fcopyfile: UseFcopyfile = .default,
7979
80stderr_writer: Io.Writer,
80stderr_writer: File.Writer = .{
81 .io = undefined,
82 .interface = Io.File.Writer.initInterface(&.{}),
83 .file = if (is_windows) undefined else .stderr(),
84 .mode = undefined,
85},
86stderr_writer_initialized: bool = false,
8187
8288pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum {
8389 enabled,
......@@ -737,6 +743,9 @@ pub fn io(t: *Threaded) Io {
737743
738744 .processExecutableOpen = processExecutableOpen,
739745 .processExecutablePath = processExecutablePath,
746 .lockStderrWriter = lockStderrWriter,
747 .tryLockStderrWriter = tryLockStderrWriter,
748 .unlockStderrWriter = unlockStderrWriter,
740749
741750 .now = now,
742751 .sleep = sleep,
......@@ -864,6 +873,9 @@ pub fn ioBasic(t: *Threaded) Io {
864873
865874 .processExecutableOpen = processExecutableOpen,
866875 .processExecutablePath = processExecutablePath,
876 .lockStderrWriter = lockStderrWriter,
877 .tryLockStderrWriter = tryLockStderrWriter,
878 .unlockStderrWriter = unlockStderrWriter,
867879
868880 .now = now,
869881 .sleep = sleep,
......@@ -9516,33 +9528,42 @@ fn netLookupFallible(
95169528 return error.OptionUnsupported;
95179529}
95189530
9519fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*Io.Writer {
9531fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*File.Writer {
95209532 const t: *Threaded = @ptrCast(@alignCast(userdata));
95219533 // Only global mutex since this is Threaded.
95229534 Io.stderr_thread_mutex.lock();
9523 if (is_windows) t.stderr_writer.file = .stderr();
9535 if (!t.stderr_writer_initialized) {
9536 if (is_windows) t.stderr_writer.file = .stderr();
9537 t.stderr_writer.mode = try .detect(ioBasic(t), t.stderr_writer.file, true, .streaming_simple);
9538 t.stderr_writer_initialized = true;
9539 }
95249540 std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {};
9525 t.stderr_writer.flush() catch {};
9526 t.stderr_writer.buffer = buffer;
9541 t.stderr_writer.interface.flush() catch {};
9542 t.stderr_writer.interface.buffer = buffer;
95279543 return &t.stderr_writer;
95289544}
95299545
9530fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*Io.Writer {
9546fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*File.Writer {
95319547 const t: *Threaded = @ptrCast(@alignCast(userdata));
95329548 // Only global mutex since this is Threaded.
95339549 if (!Io.stderr_thread_mutex.tryLock()) return null;
9534 std.Progress.clearWrittenWithEscapeCodes(t.io()) catch {};
9535 if (is_windows) t.stderr_writer.file = .stderr();
9536 t.stderr_writer.flush() catch {};
9537 t.stderr_writer.buffer = buffer;
9550 if (!t.stderr_writer_initialized) {
9551 if (is_windows) t.stderr_writer.file = .stderr();
9552 t.stderr_writer.mode = File.Writer.Mode.detect(ioBasic(t), t.stderr_writer.file, true, .streaming_simple) catch
9553 return null;
9554 t.stderr_writer_initialized = true;
9555 }
9556 std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {};
9557 t.stderr_writer.interface.flush() catch {};
9558 t.stderr_writer.interface.buffer = buffer;
95389559 return &t.stderr_writer;
95399560}
95409561
95419562fn unlockStderrWriter(userdata: ?*anyopaque) void {
95429563 const t: *Threaded = @ptrCast(@alignCast(userdata));
9543 t.stderr_writer.flush() catch {};
9544 t.stderr_writer.end = 0;
9545 t.stderr_writer.buffer = &.{};
9564 t.stderr_writer.interface.flush() catch {};
9565 t.stderr_writer.interface.end = 0;
9566 t.stderr_writer.interface.buffer = &.{};
95469567 Io.stderr_thread_mutex.unlock();
95479568}
95489569
lib/std/Io/tty.zig deleted-135
......@@ -1,135 +0,0 @@
1const builtin = @import("builtin");
2const native_os = builtin.os.tag;
3
4const std = @import("std");
5const Io = std.Io;
6const File = std.Io.File;
7const process = std.process;
8const windows = std.os.windows;
9
10pub const Color = enum {
11 black,
12 red,
13 green,
14 yellow,
15 blue,
16 magenta,
17 cyan,
18 white,
19 bright_black,
20 bright_red,
21 bright_green,
22 bright_yellow,
23 bright_blue,
24 bright_magenta,
25 bright_cyan,
26 bright_white,
27 dim,
28 bold,
29 reset,
30};
31
32/// Provides simple functionality for manipulating the terminal in some way,
33/// such as coloring text, etc.
34pub const Config = union(enum) {
35 no_color,
36 escape_codes,
37 windows_api: if (native_os == .windows) WindowsContext else noreturn,
38
39 /// Detect suitable TTY configuration options for the given file (commonly stdout/stderr).
40 /// This includes feature checks for ANSI escape codes and the Windows console API, as well as
41 /// respecting the `NO_COLOR` and `CLICOLOR_FORCE` environment variables to override the default.
42 /// Will attempt to enable ANSI escape code support if necessary/possible.
43 pub fn detect(io: Io, file: File) Config {
44 const force_color: ?bool = if (builtin.os.tag == .wasi)
45 null // wasi does not support environment variables
46 else if (process.hasNonEmptyEnvVarConstant("NO_COLOR"))
47 false
48 else if (process.hasNonEmptyEnvVarConstant("CLICOLOR_FORCE"))
49 true
50 else
51 null;
52
53 if (force_color == false) return .no_color;
54
55 if (file.enableAnsiEscapeCodes(io)) |_| {
56 return .escape_codes;
57 } else |_| {}
58
59 if (native_os == .windows and file.isTty()) {
60 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
61 if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) == windows.FALSE) {
62 return if (force_color == true) .escape_codes else .no_color;
63 }
64 return .{ .windows_api = .{
65 .handle = file.handle,
66 .reset_attributes = info.wAttributes,
67 } };
68 }
69
70 return if (force_color == true) .escape_codes else .no_color;
71 }
72
73 pub const WindowsContext = struct {
74 handle: File.Handle,
75 reset_attributes: u16,
76 };
77
78 pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error;
79
80 pub fn setColor(conf: Config, w: *Io.Writer, color: Color) SetColorError!void {
81 nosuspend switch (conf) {
82 .no_color => return,
83 .escape_codes => {
84 const color_string = switch (color) {
85 .black => "\x1b[30m",
86 .red => "\x1b[31m",
87 .green => "\x1b[32m",
88 .yellow => "\x1b[33m",
89 .blue => "\x1b[34m",
90 .magenta => "\x1b[35m",
91 .cyan => "\x1b[36m",
92 .white => "\x1b[37m",
93 .bright_black => "\x1b[90m",
94 .bright_red => "\x1b[91m",
95 .bright_green => "\x1b[92m",
96 .bright_yellow => "\x1b[93m",
97 .bright_blue => "\x1b[94m",
98 .bright_magenta => "\x1b[95m",
99 .bright_cyan => "\x1b[96m",
100 .bright_white => "\x1b[97m",
101 .bold => "\x1b[1m",
102 .dim => "\x1b[2m",
103 .reset => "\x1b[0m",
104 };
105 try w.writeAll(color_string);
106 },
107 .windows_api => |ctx| {
108 const attributes = switch (color) {
109 .black => 0,
110 .red => windows.FOREGROUND_RED,
111 .green => windows.FOREGROUND_GREEN,
112 .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN,
113 .blue => windows.FOREGROUND_BLUE,
114 .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE,
115 .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE,
116 .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE,
117 .bright_black => windows.FOREGROUND_INTENSITY,
118 .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY,
119 .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY,
120 .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY,
121 .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
122 .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
123 .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
124 .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
125 // "dim" is not supported using basic character attributes, but let's still make it do *something*.
126 // This matches the old behavior of TTY.Color before the bright variants were added.
127 .dim => windows.FOREGROUND_INTENSITY,
128 .reset => ctx.reset_attributes,
129 };
130 try w.flush();
131 try windows.SetConsoleTextAttribute(ctx.handle, attributes);
132 },
133 };
134 }
135};
lib/std/Progress.zig+2-3
......@@ -755,10 +755,9 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
755755 }
756756}
757757
758fn clearWrittenWithEscapeCodes(w: *Io.Writer) anyerror!void {
758pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) anyerror!void {
759759 if (noop_impl or !global_progress.need_clear) return;
760
761 try w.writeAll(clear ++ progress_remove);
760 try file_writer.interface.writeAllUnescaped(clear ++ progress_remove);
762761 global_progress.need_clear = false;
763762}
764763
lib/std/debug.zig+101-125
......@@ -1,7 +1,6 @@
11const std = @import("std.zig");
22const Io = std.Io;
33const Writer = std.Io.Writer;
4const tty = std.Io.tty;
54const math = std.math;
65const mem = std.mem;
76const posix = std.posix;
......@@ -262,6 +261,10 @@ pub const sys_can_stack_trace = switch (builtin.cpu.arch) {
262261 else => true,
263262};
264263
264/// This is used for debug information and debug printing. It is intentionally
265/// separate from the application's `Io` instance.
266var static_single_threaded_io: Io.Threaded = .init_single_threaded;
267
265268/// Allows the caller to freely write to stderr until `unlockStderrWriter` is called.
266269///
267270/// During the lock, any `std.Progress` information is cleared from the terminal.
......@@ -279,18 +282,12 @@ pub const sys_can_stack_trace = switch (builtin.cpu.arch) {
279282///
280283/// Alternatively, use the higher-level `Io.lockStderrWriter` to integrate with
281284/// the application's chosen `Io` implementation.
282pub fn lockStderrWriter(buffer: []u8) struct { *Writer, tty.Config } {
283 Io.stderr_thread_mutex.lock();
284 const w = std.Progress.lockStderrWriter(buffer);
285 // The stderr lock also locks access to `global.conf`.
286 if (StderrWriter.singleton.tty_config == null) {
287 StderrWriter.singleton.tty_config = .detect(io, .stderr());
288 }
289 return .{ w, global.conf.? };
285pub fn lockStderrWriter(buffer: []u8) *File.Writer {
286 return static_single_threaded_io.ioBasic().lockStderrWriter(buffer) catch unreachable;
290287}
291288
292289pub fn unlockStderrWriter() void {
293 std.Progress.unlockStderrWriter();
290 static_single_threaded_io.ioBasic().unlockStderrWriter();
294291}
295292
296293/// Writes to stderr, ignoring errors.
......@@ -305,39 +302,13 @@ pub fn unlockStderrWriter() void {
305302/// Alternatively, use the higher-level `std.log` or `Io.lockStderrWriter` to
306303/// integrate with the application's chosen `Io` implementation.
307304pub fn print(comptime fmt: []const u8, args: anytype) void {
308 var buffer: [64]u8 = undefined;
309 const bw, _ = lockStderrWriter(&buffer);
310 defer unlockStderrWriter();
311 nosuspend bw.print(fmt, args) catch return;
312}
313
314const StderrWriter = struct {
315 interface: Writer,
316 tty_config: ?tty.Config,
317
318 var singleton: StderrWriter = .{
319 .interface = .{
320 .buffer = &.{},
321 .vtable = &.{ .drain = drain },
322 },
323 .tty_config = null,
324 };
325
326 fn drain(io_w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
327 const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
328 var n: usize = 0;
329 const header = w.interface.buffered();
330 if (header.len != 0) n += try std.Io.Threaded.debugWrite(header);
331 for (data[0 .. data.len - 1]) |d| {
332 if (d.len != 0) n += try std.Io.Threaded.debugWrite(d);
333 }
334 const pattern = data[data.len - 1];
335 if (pattern.len != 0) {
336 for (0..splat) |_| n += try std.Io.Threaded.debugWrite(pattern);
337 }
338 return io_w.consume(n);
305 nosuspend {
306 var buffer: [64]u8 = undefined;
307 const stderr = lockStderrWriter(&buffer);
308 defer unlockStderrWriter();
309 stderr.interface.print(fmt, args) catch return;
339310 }
340};
311}
341312
342313/// Marked `inline` to propagate a comptime-known error to callers.
343314pub inline fn getSelfDebugInfo() !*SelfInfo {
......@@ -357,16 +328,16 @@ pub fn dumpHex(bytes: []const u8) void {
357328}
358329
359330/// Prints a hexadecimal view of the bytes, returning any error that occurs.
360pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) !void {
331pub fn dumpHexFallible(bw: *Writer, fwm: File.Writer.Mode, bytes: []const u8) !void {
361332 var chunks = mem.window(u8, bytes, 16, 16);
362333 while (chunks.next()) |window| {
363334 // 1. Print the address.
364335 const address = (@intFromPtr(bytes.ptr) + 0x10 * (std.math.divCeil(usize, chunks.index orelse bytes.len, 16) catch unreachable)) - 0x10;
365 try tty_config.setColor(bw, .dim);
336 try fwm.setColor(bw, .dim);
366337 // We print the address in lowercase and the bytes in uppercase hexadecimal to distinguish them more.
367338 // Also, make sure all lines are aligned by padding the address.
368339 try bw.print("{x:0>[1]} ", .{ address, @sizeOf(usize) * 2 });
369 try tty_config.setColor(bw, .reset);
340 try fwm.setColor(bw, .reset);
370341
371342 // 2. Print the bytes.
372343 for (window, 0..) |byte, index| {
......@@ -386,7 +357,7 @@ pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) !
386357 try bw.writeByte(byte);
387358 } else {
388359 // Related: https://github.com/ziglang/zig/issues/7600
389 if (tty_config == .windows_api) {
360 if (fwm == .terminal_winapi) {
390361 try bw.writeByte('.');
391362 continue;
392363 }
......@@ -408,11 +379,11 @@ pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) !
408379
409380test dumpHexFallible {
410381 const bytes: []const u8 = &.{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x12, 0x13 };
411 var aw: Writer.Allocating = .init(std.testing.allocator);
382 var aw: Writer.Allocating = .init(testing.allocator);
412383 defer aw.deinit();
413384
414385 try dumpHexFallible(&aw.writer, .no_color, bytes);
415 const expected = try std.fmt.allocPrint(std.testing.allocator,
386 const expected = try std.fmt.allocPrint(testing.allocator,
416387 \\{x:0>[2]} 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........
417388 \\{x:0>[2]} 01 12 13 ...
418389 \\
......@@ -421,8 +392,8 @@ test dumpHexFallible {
421392 @intFromPtr(bytes.ptr) + 16,
422393 @sizeOf(usize) * 2,
423394 });
424 defer std.testing.allocator.free(expected);
425 try std.testing.expectEqualStrings(expected, aw.written());
395 defer testing.allocator.free(expected);
396 try testing.expectEqualStrings(expected, aw.written());
426397}
427398
428399/// The pointer through which a `cpu_context.Native` is received from callers of stack tracing logic.
......@@ -437,7 +408,7 @@ pub const CpuContextPtr = if (cpu_context.Native == noreturn) noreturn else *con
437408/// away, and in fact the optimizer is able to use the assertion in its
438409/// heuristics.
439410///
440/// Inside a test block, it is best to use the `std.testing` module rather than
411/// Inside a test block, it is best to use the `testing` module rather than
441412/// this function, because this function may not detect a test failure in
442413/// ReleaseFast and ReleaseSmall mode. Outside of a test block, this assert
443414/// function is the correct function to use.
......@@ -574,26 +545,26 @@ pub fn defaultPanic(
574545 _ = panicking.fetchAdd(1, .seq_cst);
575546
576547 trace: {
577 const stderr, const tty_config = lockStderrWriter(&.{});
548 const stderr = lockStderrWriter(&.{});
578549 defer unlockStderrWriter();
579550
580551 if (builtin.single_threaded) {
581 stderr.print("panic: ", .{}) catch break :trace;
552 stderr.interface.print("panic: ", .{}) catch break :trace;
582553 } else {
583554 const current_thread_id = std.Thread.getCurrentId();
584 stderr.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
555 stderr.interface.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
585556 }
586 stderr.print("{s}\n", .{msg}) catch break :trace;
557 stderr.interface.print("{s}\n", .{msg}) catch break :trace;
587558
588559 if (@errorReturnTrace()) |t| if (t.index > 0) {
589 stderr.writeAll("error return context:\n") catch break :trace;
590 writeStackTrace(t, stderr, tty_config) catch break :trace;
591 stderr.writeAll("\nstack trace:\n") catch break :trace;
560 stderr.interface.writeAll("error return context:\n") catch break :trace;
561 writeStackTrace(t, &stderr.interface, stderr.mode) catch break :trace;
562 stderr.interface.writeAll("\nstack trace:\n") catch break :trace;
592563 };
593564 writeCurrentStackTrace(.{
594565 .first_address = first_trace_addr orelse @returnAddress(),
595566 .allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
596 }, stderr, tty_config) catch break :trace;
567 }, &stderr.interface, stderr.mode) catch break :trace;
597568 }
598569
599570 waitForOtherThreadToFinishPanicking();
......@@ -603,8 +574,8 @@ pub fn defaultPanic(
603574 // A panic happened while trying to print a previous panic message.
604575 // We're still holding the mutex but that's fine as we're going to
605576 // call abort().
606 const stderr, _ = lockStderrWriter(&.{});
607 stderr.writeAll("aborting due to recursive panic\n") catch {};
577 const stderr = lockStderrWriter(&.{});
578 stderr.interface.writeAll("aborting due to recursive panic\n") catch {};
608579 },
609580 else => {}, // Panicked while printing the recursive panic message.
610581 }
......@@ -651,8 +622,7 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:
651622 defer it.deinit();
652623 if (!it.stratOk(options.allow_unsafe_unwind)) return empty_trace;
653624
654 var threaded: Io.Threaded = .init_single_threaded;
655 const io = threaded.ioBasic();
625 const io = static_single_threaded_io.ioBasic();
656626
657627 var total_frames: usize = 0;
658628 var index: usize = 0;
......@@ -686,36 +656,34 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:
686656/// Write the current stack trace to `writer`, annotated with source locations.
687657///
688658/// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing.
689pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_config: tty.Config) Writer.Error!void {
690 var threaded: Io.Threaded = .init_single_threaded;
691 const io = threaded.ioBasic();
692
659pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void {
693660 if (!std.options.allow_stack_tracing) {
694 tty_config.setColor(writer, .dim) catch {};
661 fwm.setColor(writer, .dim) catch {};
695662 try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{});
696 tty_config.setColor(writer, .reset) catch {};
663 fwm.setColor(writer, .reset) catch {};
697664 return;
698665 }
699666 const di_gpa = getDebugInfoAllocator();
700667 const di = getSelfDebugInfo() catch |err| switch (err) {
701668 error.UnsupportedTarget => {
702 tty_config.setColor(writer, .dim) catch {};
669 fwm.setColor(writer, .dim) catch {};
703670 try writer.print("Cannot print stack trace: debug info unavailable for target\n", .{});
704 tty_config.setColor(writer, .reset) catch {};
671 fwm.setColor(writer, .reset) catch {};
705672 return;
706673 },
707674 };
708675 var it: StackIterator = .init(options.context);
709676 defer it.deinit();
710677 if (!it.stratOk(options.allow_unsafe_unwind)) {
711 tty_config.setColor(writer, .dim) catch {};
678 fwm.setColor(writer, .dim) catch {};
712679 try writer.print("Cannot print stack trace: safe unwind unavailable for target\n", .{});
713 tty_config.setColor(writer, .reset) catch {};
680 fwm.setColor(writer, .reset) catch {};
714681 return;
715682 }
716683 var total_frames: usize = 0;
717684 var wait_for = options.first_address;
718685 var printed_any_frame = false;
686 const io = static_single_threaded_io.ioBasic();
719687 while (true) switch (it.next(io)) {
720688 .switch_to_fp => |unwind_error| {
721689 switch (StackIterator.fp_usability) {
......@@ -733,31 +701,31 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri
733701 error.Unexpected => "unexpected error",
734702 };
735703 if (it.stratOk(options.allow_unsafe_unwind)) {
736 tty_config.setColor(writer, .dim) catch {};
704 fwm.setColor(writer, .dim) catch {};
737705 try writer.print(
738706 "Unwind error at address `{s}:0x{x}` ({s}), remaining frames may be incorrect\n",
739707 .{ module_name, unwind_error.address, caption },
740708 );
741 tty_config.setColor(writer, .reset) catch {};
709 fwm.setColor(writer, .reset) catch {};
742710 } else {
743 tty_config.setColor(writer, .dim) catch {};
711 fwm.setColor(writer, .dim) catch {};
744712 try writer.print(
745713 "Unwind error at address `{s}:0x{x}` ({s}), stopping trace early\n",
746714 .{ module_name, unwind_error.address, caption },
747715 );
748 tty_config.setColor(writer, .reset) catch {};
716 fwm.setColor(writer, .reset) catch {};
749717 return;
750718 }
751719 },
752720 .end => break,
753721 .frame => |ret_addr| {
754722 if (total_frames > 10_000) {
755 tty_config.setColor(writer, .dim) catch {};
723 fwm.setColor(writer, .dim) catch {};
756724 try writer.print(
757725 "Stopping trace after {d} frames (large frame count may indicate broken debug info)\n",
758726 .{total_frames},
759727 );
760 tty_config.setColor(writer, .reset) catch {};
728 fwm.setColor(writer, .reset) catch {};
761729 return;
762730 }
763731 total_frames += 1;
......@@ -767,7 +735,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri
767735 }
768736 // `ret_addr` is the return address, which is *after* the function call.
769737 // Subtract 1 to get an address *in* the function call for a better source location.
770 try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config);
738 try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm);
771739 printed_any_frame = true;
772740 },
773741 };
......@@ -775,7 +743,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri
775743}
776744/// A thin wrapper around `writeCurrentStackTrace` which writes to stderr and ignores write errors.
777745pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void {
778 const stderr, const tty_config = lockStderrWriter(&.{});
746 const stderr = lockStderrWriter(&.{});
779747 defer unlockStderrWriter();
780748 writeCurrentStackTrace(.{
781749 .first_address = a: {
......@@ -785,33 +753,40 @@ pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void {
785753 },
786754 .context = options.context,
787755 .allow_unsafe_unwind = options.allow_unsafe_unwind,
788 }, stderr, tty_config) catch |err| switch (err) {
756 }, &stderr.interface, stderr.mode) catch |err| switch (err) {
789757 error.WriteFailed => {},
790758 };
791759}
792760
793761pub const FormatStackTrace = struct {
794762 stack_trace: StackTrace,
795 tty_config: tty.Config,
796763
797 pub fn format(context: @This(), writer: *Writer) Writer.Error!void {
798 try writer.writeAll("\n");
799 try writeStackTrace(&context.stack_trace, writer, context.tty_config);
764 pub const Decorated = struct {
765 stack_trace: StackTrace,
766 file_writer_mode: File.Writer.Mode,
767
768 pub fn format(decorated: Decorated, writer: *Writer) Writer.Error!void {
769 try writer.writeByte('\n');
770 try writeStackTrace(&decorated.stack_trace, writer, decorated.file_writer_mode);
771 }
772 };
773
774 pub fn format(context: FormatStackTrace, writer: *Writer) Writer.Error!void {
775 return Decorated.format(.{
776 .stack_trace = context.stack_trace,
777 .file_writer_mode = .streaming,
778 }, writer);
800779 }
801780};
802781
803782/// Write a previously captured stack trace to `writer`, annotated with source locations.
804pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void {
783pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void {
805784 if (!std.options.allow_stack_tracing) {
806 tty_config.setColor(writer, .dim) catch {};
785 fwm.setColor(writer, .dim) catch {};
807786 try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{});
808 tty_config.setColor(writer, .reset) catch {};
787 fwm.setColor(writer, .reset) catch {};
809788 return;
810789 }
811 // We use an independent Io implementation here in case there was a problem
812 // with the application's Io implementation itself.
813 var threaded: Io.Threaded = .init_single_threaded;
814 const io = threaded.ioBasic();
815790
816791 // Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if
817792 // `st` is `@errorReturnTrace()` and errors are encountered while writing the stack trace.
......@@ -820,22 +795,23 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.C
820795 const di_gpa = getDebugInfoAllocator();
821796 const di = getSelfDebugInfo() catch |err| switch (err) {
822797 error.UnsupportedTarget => {
823 tty_config.setColor(writer, .dim) catch {};
798 fwm.setColor(writer, .dim) catch {};
824799 try writer.print("Cannot print stack trace: debug info unavailable for target\n\n", .{});
825 tty_config.setColor(writer, .reset) catch {};
800 fwm.setColor(writer, .reset) catch {};
826801 return;
827802 },
828803 };
804 const io = static_single_threaded_io.ioBasic();
829805 const captured_frames = @min(n_frames, st.instruction_addresses.len);
830806 for (st.instruction_addresses[0..captured_frames]) |ret_addr| {
831807 // `ret_addr` is the return address, which is *after* the function call.
832808 // Subtract 1 to get an address *in* the function call for a better source location.
833 try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config);
809 try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm);
834810 }
835811 if (n_frames > captured_frames) {
836 tty_config.setColor(writer, .bold) catch {};
812 fwm.setColor(writer, .bold) catch {};
837813 try writer.print("({d} additional stack frames skipped...)\n", .{n_frames - captured_frames});
838 tty_config.setColor(writer, .reset) catch {};
814 fwm.setColor(writer, .reset) catch {};
839815 }
840816}
841817/// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors.
......@@ -1143,7 +1119,7 @@ fn printSourceAtAddress(
11431119 debug_info: *SelfInfo,
11441120 writer: *Writer,
11451121 address: usize,
1146 tty_config: tty.Config,
1122 fwm: File.Writer.Mode,
11471123) Writer.Error!void {
11481124 const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) {
11491125 error.MissingDebugInfo,
......@@ -1151,15 +1127,15 @@ fn printSourceAtAddress(
11511127 error.InvalidDebugInfo,
11521128 => .unknown,
11531129 error.ReadFailed, error.Unexpected, error.Canceled => s: {
1154 tty_config.setColor(writer, .dim) catch {};
1130 fwm.setColor(writer, .dim) catch {};
11551131 try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{});
1156 tty_config.setColor(writer, .reset) catch {};
1132 fwm.setColor(writer, .reset) catch {};
11571133 break :s .unknown;
11581134 },
11591135 error.OutOfMemory => s: {
1160 tty_config.setColor(writer, .dim) catch {};
1136 fwm.setColor(writer, .dim) catch {};
11611137 try writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{});
1162 tty_config.setColor(writer, .reset) catch {};
1138 fwm.setColor(writer, .reset) catch {};
11631139 break :s .unknown;
11641140 },
11651141 };
......@@ -1171,7 +1147,7 @@ fn printSourceAtAddress(
11711147 address,
11721148 symbol.name orelse "???",
11731149 symbol.compile_unit_name orelse debug_info.getModuleName(gpa, address) catch "???",
1174 tty_config,
1150 fwm,
11751151 );
11761152}
11771153fn printLineInfo(
......@@ -1181,10 +1157,10 @@ fn printLineInfo(
11811157 address: usize,
11821158 symbol_name: []const u8,
11831159 compile_unit_name: []const u8,
1184 tty_config: tty.Config,
1160 fwm: File.Writer.Mode,
11851161) Writer.Error!void {
11861162 nosuspend {
1187 tty_config.setColor(writer, .bold) catch {};
1163 fwm.setColor(writer, .bold) catch {};
11881164
11891165 if (source_location) |*sl| {
11901166 try writer.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column });
......@@ -1192,11 +1168,11 @@ fn printLineInfo(
11921168 try writer.writeAll("???:?:?");
11931169 }
11941170
1195 tty_config.setColor(writer, .reset) catch {};
1171 fwm.setColor(writer, .reset) catch {};
11961172 try writer.writeAll(": ");
1197 tty_config.setColor(writer, .dim) catch {};
1173 fwm.setColor(writer, .dim) catch {};
11981174 try writer.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name });
1199 tty_config.setColor(writer, .reset) catch {};
1175 fwm.setColor(writer, .reset) catch {};
12001176 try writer.writeAll("\n");
12011177
12021178 // Show the matching source code line if possible
......@@ -1207,9 +1183,9 @@ fn printLineInfo(
12071183 const space_needed = @as(usize, @intCast(sl.column - 1));
12081184
12091185 try writer.splatByteAll(' ', space_needed);
1210 tty_config.setColor(writer, .green) catch {};
1186 fwm.setColor(writer, .green) catch {};
12111187 try writer.writeAll("^");
1212 tty_config.setColor(writer, .reset) catch {};
1188 fwm.setColor(writer, .reset) catch {};
12131189 }
12141190 try writer.writeAll("\n");
12151191 } else |_| {
......@@ -1250,18 +1226,18 @@ fn printLineFromFile(io: Io, writer: *Writer, source_location: SourceLocation) !
12501226}
12511227
12521228test printLineFromFile {
1253 const io = std.testing.io;
1254 const gpa = std.testing.allocator;
1229 const io = testing.io;
1230 const gpa = testing.allocator;
12551231
12561232 var aw: Writer.Allocating = .init(gpa);
12571233 defer aw.deinit();
12581234 const output_stream = &aw.writer;
12591235
12601236 const join = std.fs.path.join;
1261 const expectError = std.testing.expectError;
1262 const expectEqualStrings = std.testing.expectEqualStrings;
1237 const expectError = testing.expectError;
1238 const expectEqualStrings = testing.expectEqualStrings;
12631239
1264 var test_dir = std.testing.tmpDir(.{});
1240 var test_dir = testing.tmpDir(.{});
12651241 defer test_dir.cleanup();
12661242 // Relies on testing.tmpDir internals which is not ideal, but SourceLocation requires paths.
12671243 const test_dir_path = try join(gpa, &.{ ".zig-cache", "tmp", test_dir.sub_path[0..] });
......@@ -1578,19 +1554,19 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex
15781554 _ = panicking.fetchAdd(1, .seq_cst);
15791555
15801556 trace: {
1581 const stderr, const tty_config = lockStderrWriter(&.{});
1557 const stderr = lockStderrWriter(&.{});
15821558 defer unlockStderrWriter();
15831559
15841560 if (addr) |a| {
1585 stderr.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
1561 stderr.interface.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
15861562 } else {
1587 stderr.print("{s} (no address available)\n", .{name}) catch break :trace;
1563 stderr.interface.print("{s} (no address available)\n", .{name}) catch break :trace;
15881564 }
15891565 if (opt_ctx) |context| {
15901566 writeCurrentStackTrace(.{
15911567 .context = context,
15921568 .allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
1593 }, stderr, tty_config) catch break :trace;
1569 }, &stderr.interface, stderr.mode) catch break :trace;
15941570 }
15951571 }
15961572 },
......@@ -1599,8 +1575,8 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex
15991575 // A segfault happened while trying to print a previous panic message.
16001576 // We're still holding the mutex but that's fine as we're going to
16011577 // call abort().
1602 const stderr, _ = lockStderrWriter(&.{});
1603 stderr.writeAll("aborting due to recursive panic\n") catch {};
1578 const stderr = lockStderrWriter(&.{});
1579 stderr.interface.writeAll("aborting due to recursive panic\n") catch {};
16041580 },
16051581 else => {}, // Panicked while printing the recursive panic message.
16061582 }
......@@ -1632,9 +1608,9 @@ test "manage resources correctly" {
16321608 return @returnAddress();
16331609 }
16341610 };
1635 const gpa = std.testing.allocator;
1636 var threaded: Io.Threaded = .init_single_threaded;
1637 const io = threaded.ioBasic();
1611 const gpa = testing.allocator;
1612 const io = testing.io;
1613
16381614 var discarding: Writer.Discarding = .init(&.{});
16391615 var di: SelfInfo = .init;
16401616 defer di.deinit(gpa);
lib/std/heap/debug_allocator.zig+17-79
......@@ -179,8 +179,6 @@ pub fn DebugAllocator(comptime config: Config) type {
179179 total_requested_bytes: @TypeOf(total_requested_bytes_init) = total_requested_bytes_init,
180180 requested_memory_limit: @TypeOf(requested_memory_limit_init) = requested_memory_limit_init,
181181 mutex: @TypeOf(mutex_init) = mutex_init,
182 /// Set this value differently to affect how errors and leaks are logged.
183 tty_config: std.Io.tty.Config = .no_color,
184182
185183 const Self = @This();
186184
......@@ -427,7 +425,6 @@ pub fn DebugAllocator(comptime config: Config) type {
427425 bucket: *BucketHeader,
428426 size_class_index: usize,
429427 used_bits_count: usize,
430 tty_config: std.Io.tty.Config,
431428 ) usize {
432429 const size_class = @as(usize, 1) << @as(Log2USize, @intCast(size_class_index));
433430 const slot_count = slot_counts[size_class_index];
......@@ -444,11 +441,7 @@ pub fn DebugAllocator(comptime config: Config) type {
444441 const page_addr = @intFromPtr(bucket) & ~(page_size - 1);
445442 const addr = page_addr + slot_index * size_class;
446443 log.err("memory address 0x{x} leaked: {f}", .{
447 addr,
448 std.debug.FormatStackTrace{
449 .stack_trace = stack_trace,
450 .tty_config = tty_config,
451 },
444 addr, std.debug.FormatStackTrace{ .stack_trace = stack_trace },
452445 });
453446 leaks += 1;
454447 }
......@@ -460,8 +453,6 @@ pub fn DebugAllocator(comptime config: Config) type {
460453
461454 /// Emits log messages for leaks and then returns the number of detected leaks (0 if no leaks were detected).
462455 pub fn detectLeaks(self: *Self) usize {
463 const tty_config = self.tty_config;
464
465456 var leaks: usize = 0;
466457
467458 for (self.buckets, 0..) |init_optional_bucket, size_class_index| {
......@@ -469,7 +460,7 @@ pub fn DebugAllocator(comptime config: Config) type {
469460 const slot_count = slot_counts[size_class_index];
470461 const used_bits_count = usedBitsCount(slot_count);
471462 while (optional_bucket) |bucket| {
472 leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count, tty_config);
463 leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count);
473464 optional_bucket = bucket.prev;
474465 }
475466 }
......@@ -480,10 +471,7 @@ pub fn DebugAllocator(comptime config: Config) type {
480471 const stack_trace = large_alloc.getStackTrace(.alloc);
481472 log.err("memory address 0x{x} leaked: {f}", .{
482473 @intFromPtr(large_alloc.bytes.ptr),
483 std.debug.FormatStackTrace{
484 .stack_trace = stack_trace,
485 .tty_config = tty_config,
486 },
474 std.debug.FormatStackTrace{ .stack_trace = stack_trace },
487475 });
488476 leaks += 1;
489477 }
......@@ -535,28 +523,14 @@ pub fn DebugAllocator(comptime config: Config) type {
535523 @memset(addr_buf[@min(st.index, addr_buf.len)..], 0);
536524 }
537525
538 fn reportDoubleFree(
539 tty_config: std.Io.tty.Config,
540 ret_addr: usize,
541 alloc_stack_trace: StackTrace,
542 free_stack_trace: StackTrace,
543 ) void {
526 fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void {
544527 @branchHint(.cold);
545528 var addr_buf: [stack_n]usize = undefined;
546529 const second_free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf);
547530 log.err("Double free detected. Allocation: {f} First free: {f} Second free: {f}", .{
548 std.debug.FormatStackTrace{
549 .stack_trace = alloc_stack_trace,
550 .tty_config = tty_config,
551 },
552 std.debug.FormatStackTrace{
553 .stack_trace = free_stack_trace,
554 .tty_config = tty_config,
555 },
556 std.debug.FormatStackTrace{
557 .stack_trace = second_free_stack_trace,
558 .tty_config = tty_config,
559 },
531 std.debug.FormatStackTrace{ .stack_trace = alloc_stack_trace },
532 std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
533 std.debug.FormatStackTrace{ .stack_trace = second_free_stack_trace },
560534 });
561535 }
562536
......@@ -587,7 +561,7 @@ pub fn DebugAllocator(comptime config: Config) type {
587561
588562 if (config.retain_metadata and entry.value_ptr.freed) {
589563 if (config.safety) {
590 reportDoubleFree(self.tty_config, ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
564 reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
591565 @panic("Unrecoverable double free");
592566 } else {
593567 unreachable;
......@@ -598,18 +572,11 @@ pub fn DebugAllocator(comptime config: Config) type {
598572 @branchHint(.cold);
599573 var addr_buf: [stack_n]usize = undefined;
600574 const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf);
601 const tty_config = self.tty_config;
602575 log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
603576 entry.value_ptr.bytes.len,
604577 old_mem.len,
605 std.debug.FormatStackTrace{
606 .stack_trace = entry.value_ptr.getStackTrace(.alloc),
607 .tty_config = tty_config,
608 },
609 std.debug.FormatStackTrace{
610 .stack_trace = free_stack_trace,
611 .tty_config = tty_config,
612 },
578 std.debug.FormatStackTrace{ .stack_trace = entry.value_ptr.getStackTrace(.alloc) },
579 std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
613580 });
614581 }
615582
......@@ -701,7 +668,7 @@ pub fn DebugAllocator(comptime config: Config) type {
701668
702669 if (config.retain_metadata and entry.value_ptr.freed) {
703670 if (config.safety) {
704 reportDoubleFree(self.tty_config, ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
671 reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
705672 return;
706673 } else {
707674 unreachable;
......@@ -712,18 +679,11 @@ pub fn DebugAllocator(comptime config: Config) type {
712679 @branchHint(.cold);
713680 var addr_buf: [stack_n]usize = undefined;
714681 const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf);
715 const tty_config = self.tty_config;
716682 log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
717683 entry.value_ptr.bytes.len,
718684 old_mem.len,
719 std.debug.FormatStackTrace{
720 .stack_trace = entry.value_ptr.getStackTrace(.alloc),
721 .tty_config = tty_config,
722 },
723 std.debug.FormatStackTrace{
724 .stack_trace = free_stack_trace,
725 .tty_config = tty_config,
726 },
685 std.debug.FormatStackTrace{ .stack_trace = entry.value_ptr.getStackTrace(.alloc) },
686 std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
727687 });
728688 }
729689
......@@ -924,7 +884,6 @@ pub fn DebugAllocator(comptime config: Config) type {
924884 if (!is_used) {
925885 if (config.safety) {
926886 reportDoubleFree(
927 self.tty_config,
928887 return_address,
929888 bucketStackTrace(bucket, slot_count, slot_index, .alloc),
930889 bucketStackTrace(bucket, slot_count, slot_index, .free),
......@@ -946,34 +905,24 @@ pub fn DebugAllocator(comptime config: Config) type {
946905 const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf);
947906 if (old_memory.len != requested_size) {
948907 @branchHint(.cold);
949 const tty_config = self.tty_config;
950908 log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
951909 requested_size,
952910 old_memory.len,
953911 std.debug.FormatStackTrace{
954912 .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
955 .tty_config = tty_config,
956 },
957 std.debug.FormatStackTrace{
958 .stack_trace = free_stack_trace,
959 .tty_config = tty_config,
960913 },
914 std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
961915 });
962916 }
963917 if (alignment != slot_alignment) {
964918 @branchHint(.cold);
965 const tty_config = self.tty_config;
966919 log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{
967920 slot_alignment.toByteUnits(),
968921 alignment.toByteUnits(),
969922 std.debug.FormatStackTrace{
970923 .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
971 .tty_config = tty_config,
972 },
973 std.debug.FormatStackTrace{
974 .stack_trace = free_stack_trace,
975 .tty_config = tty_config,
976924 },
925 std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
977926 });
978927 }
979928 }
......@@ -1040,7 +989,6 @@ pub fn DebugAllocator(comptime config: Config) type {
1040989 const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0;
1041990 if (!is_used) {
1042991 reportDoubleFree(
1043 self.tty_config,
1044992 return_address,
1045993 bucketStackTrace(bucket, slot_count, slot_index, .alloc),
1046994 bucketStackTrace(bucket, slot_count, slot_index, .free),
......@@ -1058,34 +1006,24 @@ pub fn DebugAllocator(comptime config: Config) type {
10581006 const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf);
10591007 if (memory.len != requested_size) {
10601008 @branchHint(.cold);
1061 const tty_config = self.tty_config;
10621009 log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
10631010 requested_size,
10641011 memory.len,
10651012 std.debug.FormatStackTrace{
10661013 .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
1067 .tty_config = tty_config,
1068 },
1069 std.debug.FormatStackTrace{
1070 .stack_trace = free_stack_trace,
1071 .tty_config = tty_config,
10721014 },
1015 std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
10731016 });
10741017 }
10751018 if (alignment != slot_alignment) {
10761019 @branchHint(.cold);
1077 const tty_config = self.tty_config;
10781020 log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{
10791021 slot_alignment.toByteUnits(),
10801022 alignment.toByteUnits(),
10811023 std.debug.FormatStackTrace{
10821024 .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
1083 .tty_config = tty_config,
1084 },
1085 std.debug.FormatStackTrace{
1086 .stack_trace = free_stack_trace,
1087 .tty_config = tty_config,
10881025 },
1026 std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
10891027 });
10901028 }
10911029 }
lib/std/log.zig+45-19
......@@ -15,7 +15,7 @@
1515//!
1616//! For an example implementation of the `logFn` function, see `defaultLog`,
1717//! which is the default implementation. It outputs to stderr, using color if
18//! the detected `std.Io.tty.Config` supports it. Its output looks like this:
18//! supported. Its output looks like this:
1919//! ```
2020//! error: this is an error
2121//! error(scope): this is an error with a non-default scope
......@@ -80,8 +80,6 @@ pub fn logEnabled(comptime level: Level, comptime scope: @EnumLiteral()) bool {
8080 return @intFromEnum(level) <= @intFromEnum(std.options.log_level);
8181}
8282
83var static_threaded_io: std.Io.Threaded = .init_single_threaded;
84
8583/// The default implementation for the log function. Custom log functions may
8684/// forward log messages to this function.
8785///
......@@ -93,36 +91,64 @@ pub fn defaultLog(
9391 comptime format: []const u8,
9492 args: anytype,
9593) void {
96 return defaultLogIo(level, scope, format, args, static_threaded_io.io());
94 var buffer: [64]u8 = undefined;
95 const stderr = std.debug.lockStderrWriter(&buffer);
96 defer std.debug.unlockStderrWriter();
97 return defaultLogFileWriter(level, scope, format, args, stderr);
9798}
9899
99pub fn defaultLogIo(
100pub fn defaultLogFileWriter(
100101 comptime level: Level,
101102 comptime scope: @EnumLiteral(),
102103 comptime format: []const u8,
103104 args: anytype,
104 io: std.Io,
105 fw: *std.Io.File.Writer,
105106) void {
106 var buffer: [64]u8 = undefined;
107 const stderr, const ttyconf = io.lockStderrWriter(&buffer);
108 defer io.unlockStderrWriter();
109 ttyconf.setColor(stderr, switch (level) {
107 fw.setColor(switch (level) {
110108 .err => .red,
111109 .warn => .yellow,
112110 .info => .green,
113111 .debug => .magenta,
114112 }) catch {};
115 ttyconf.setColor(stderr, .bold) catch {};
116 stderr.writeAll(level.asText()) catch return;
117 ttyconf.setColor(stderr, .reset) catch {};
118 ttyconf.setColor(stderr, .dim) catch {};
119 ttyconf.setColor(stderr, .bold) catch {};
113 fw.setColor(.bold) catch {};
114 fw.interface.writeAll(level.asText()) catch return;
115 fw.setColor(.reset) catch {};
116 fw.setColor(.dim) catch {};
117 fw.setColor(.bold) catch {};
120118 if (scope != .default) {
121 stderr.print("({s})", .{@tagName(scope)}) catch return;
119 fw.interface.print("({s})", .{@tagName(scope)}) catch return;
120 }
121 fw.interface.writeAll(": ") catch return;
122 fw.setColor(.reset) catch {};
123 fw.interface.print(format ++ "\n", decorateArgs(args, fw.mode)) catch return;
124}
125
126fn DecorateArgs(comptime Args: type) type {
127 const fields = @typeInfo(Args).@"struct".fields;
128 var new_fields: [fields.len]type = undefined;
129 for (fields, &new_fields) |old, *new| {
130 if (old.type == std.debug.FormatStackTrace) {
131 new.* = std.debug.FormatStackTrace.Decorated;
132 } else {
133 new.* = old.type;
134 }
135 }
136 return @Tuple(&new_fields);
137}
138
139fn decorateArgs(args: anytype, file_writer_mode: std.Io.File.Writer.Mode) DecorateArgs(@TypeOf(args)) {
140 var new_args: DecorateArgs(@TypeOf(args)) = undefined;
141 inline for (args, &new_args) |old, *new| {
142 if (@TypeOf(old) == std.debug.FormatStackTrace) {
143 new.* = .{
144 .stack_trace = old.stack_trace,
145 .file_writer_mode = file_writer_mode,
146 };
147 } else {
148 new.* = old;
149 }
122150 }
123 stderr.writeAll(": ") catch return;
124 ttyconf.setColor(stderr, .reset) catch {};
125 stderr.print(format ++ "\n", args) catch return;
151 return new_args;
126152}
127153
128154/// Returns a scoped logging namespace that logs all messages using the scope
lib/std/process.zig+4-4
......@@ -439,25 +439,25 @@ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError
439439}
440440
441441/// On Windows, `key` must be valid WTF-8.
442pub fn hasEnvVarConstant(comptime key: []const u8) bool {
442pub inline fn hasEnvVarConstant(comptime key: []const u8) bool {
443443 if (native_os == .windows) {
444444 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
445445 return getenvW(key_w) != null;
446446 } else if (native_os == .wasi and !builtin.link_libc) {
447 @compileError("hasEnvVarConstant is not supported for WASI without libc");
447 return false;
448448 } else {
449449 return posix.getenv(key) != null;
450450 }
451451}
452452
453453/// On Windows, `key` must be valid WTF-8.
454pub fn hasNonEmptyEnvVarConstant(comptime key: []const u8) bool {
454pub inline fn hasNonEmptyEnvVarConstant(comptime key: []const u8) bool {
455455 if (native_os == .windows) {
456456 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
457457 const value = getenvW(key_w) orelse return false;
458458 return value.len != 0;
459459 } else if (native_os == .wasi and !builtin.link_libc) {
460 @compileError("hasNonEmptyEnvVarConstant is not supported for WASI without libc");
460 return false;
461461 } else {
462462 const value = posix.getenv(key) orelse return false;
463463 return value.len != 0;
src/main.zig-2
......@@ -247,8 +247,6 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
247247 threaded.stack_size = thread_stack_size;
248248 const io = threaded.io();
249249
250 debug_allocator.tty_config = .detect(io, .stderr());
251
252250 const cmd = args[1];
253251 const cmd_args = args[2..];
254252 if (mem.eql(u8, cmd, "build-exe")) {