diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2025-12-17 15:47:33 -0800 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2025-12-23 22:15:09 -0800 |
| commit | aa57793b680b3da05f1d888b4df15807905e57c8 (patch) | |
| tree | d88a1c6f56796942c9b21aee4bbb88e72045d298 /lib | |
| parent | 97f106f949891870433bfcc6b7cf4c2a6709402e (diff) | |
| download | zig-aa57793b680b3da05f1d888b4df15807905e57c8.tar.gz zig-aa57793b680b3da05f1d888b4df15807905e57c8.zip | |
std: rework locking stderr
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/std/Io.zig | 46 | ||||
| -rw-r--r-- | lib/std/Io/File/Reader.zig | 34 | ||||
| -rw-r--r-- | lib/std/Io/File/Writer.zig | 250 | ||||
| -rw-r--r-- | lib/std/Io/Terminal.zig | 154 | ||||
| -rw-r--r-- | lib/std/Io/Threaded.zig | 83 | ||||
| -rw-r--r-- | lib/std/Io/Writer.zig | 2 | ||||
| -rw-r--r-- | lib/std/Progress.zig | 36 | ||||
| -rw-r--r-- | lib/std/debug.zig | 232 | ||||
| -rw-r--r-- | lib/std/log.zig | 34 | ||||
| -rw-r--r-- | lib/std/process.zig | 7 |
10 files changed, 409 insertions, 469 deletions
diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 83a2e940bf..6fa05b16d2 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -557,13 +557,6 @@ pub const net = @import("Io/net.zig"); userdata: ?*anyopaque, vtable: *const VTable, -/// This is the global, process-wide protection to coordinate stderr writes. -/// -/// The primary motivation for recursive mutex here is so that a panic while -/// stderr mutex is held still dumps the stack trace and other debug -/// information. -pub var stderr_thread_mutex: std.Thread.Mutex.Recursive = .init; - pub const VTable = struct { /// If it returns `null` it means `result` has been already populated and /// `await` will be a no-op. @@ -719,9 +712,9 @@ pub const VTable = struct { processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, - lockStderrWriter: *const fn (?*anyopaque, buffer: []u8) Cancelable!*File.Writer, - tryLockStderrWriter: *const fn (?*anyopaque, buffer: []u8) ?*File.Writer, - unlockStderrWriter: *const fn (?*anyopaque) void, + lockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!LockedStderr, + tryLockStderr: *const fn (?*anyopaque, buffer: []u8) Cancelable!?LockedStderr, + unlockStderr: *const fn (?*anyopaque) void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, @@ -763,6 +756,7 @@ pub const UnexpectedError = error{ pub const Dir = @import("Io/Dir.zig"); pub const File = @import("Io/File.zig"); +pub const Terminal = @import("Io/Terminal.zig"); pub const Clock = enum { /// A settable system-wide clock that measures real (i.e. wall-clock) @@ -2177,22 +2171,34 @@ pub fn select(io: Io, s: anytype) Cancelable!SelectUnion(@TypeOf(s)) { } } +pub const LockedStderr = struct { + file_writer: *File.Writer, + terminal_mode: Terminal.Mode, + + pub fn terminal(ls: LockedStderr) Terminal { + return .{ + .writer = &ls.file_writer.interface, + .mode = ls.terminal_mode, + }; + } +}; + /// For doing application-level writes to the standard error stream. /// Coordinates also with debug-level writes that are ignorant of Io interface -/// and implementations. When this returns, `stderr_thread_mutex` will be -/// locked. +/// and implementations. When this returns, `std.process.stderr_thread_mutex` +/// will be locked. /// /// See also: -/// * `tryLockStderrWriter` -pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*File.Writer { - return io.vtable.lockStderrWriter(io.userdata, buffer); +/// * `tryLockStderr` +pub fn lockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!LockedStderr { + return io.vtable.lockStderr(io.userdata, buffer, terminal_mode); } -/// Same as `lockStderrWriter` but uncancelable and non-blocking. -pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*File.Writer { - return io.vtable.tryLockStderrWriter(io.userdata, buffer); +/// Same as `lockStderr` but non-blocking. +pub fn tryLockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!?LockedStderr { + return io.vtable.tryLockStderr(io.userdata, buffer, terminal_mode); } -pub fn unlockStderrWriter(io: Io) void { - return io.vtable.unlockStderrWriter(io.userdata); +pub fn unlockStderr(io: Io) void { + return io.vtable.unlockStderr(io.userdata); } diff --git a/lib/std/Io/File/Reader.zig b/lib/std/Io/File/Reader.zig index 8b2074a0aa..792b20a716 100644 --- a/lib/std/Io/File/Reader.zig +++ b/lib/std/Io/File/Reader.zig @@ -64,24 +64,24 @@ pub const Mode = enum { streaming, positional, /// Avoid syscalls other than `read` and `readv`. - streaming_reading, + streaming_simple, /// Avoid syscalls other than `pread` and `preadv`. - positional_reading, + positional_simple, /// Indicates reading cannot continue because of a seek failure. failure, pub fn toStreaming(m: @This()) @This() { return switch (m) { .positional, .streaming => .streaming, - .positional_reading, .streaming_reading => .streaming_reading, + .positional_simple, .streaming_simple => .streaming_simple, .failure => .failure, }; } - pub fn toReading(m: @This()) @This() { + pub fn toSimple(m: @This()) @This() { return switch (m) { - .positional, .positional_reading => .positional_reading, - .streaming, .streaming_reading => .streaming_reading, + .positional, .positional_simple => .positional_simple, + .streaming, .streaming_simple => .streaming_simple, .failure => .failure, }; } @@ -153,10 +153,10 @@ pub fn getSize(r: *Reader) SizeError!u64 { pub fn seekBy(r: *Reader, offset: i64) SeekError!void { const io = r.io; switch (r.mode) { - .positional, .positional_reading => { + .positional, .positional_simple => { setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); }, - .streaming, .streaming_reading => { + .streaming, .streaming_simple => { const seek_err = r.seek_err orelse e: { if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| { setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); @@ -183,10 +183,10 @@ pub fn seekBy(r: *Reader, offset: i64) SeekError!void { pub fn seekTo(r: *Reader, offset: u64) SeekError!void { const io = r.io; switch (r.mode) { - .positional, .positional_reading => { + .positional, .positional_simple => { setLogicalPos(r, offset); }, - .streaming, .streaming_reading => { + .streaming, .streaming_simple => { const logical_pos = logicalPos(r); if (offset >= logical_pos) return seekBy(r, @intCast(offset - logical_pos)); if (r.seek_err) |err| return err; @@ -225,19 +225,19 @@ pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Mode) Io.Rea switch (mode) { .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { error.Unimplemented => { - r.mode = r.mode.toReading(); + r.mode = r.mode.toSimple(); return 0; }, else => |e| return e, }, - .positional_reading => { + .positional_simple => { const dest = limit.slice(try w.writableSliceGreedy(1)); var data: [1][]u8 = .{dest}; const n = try readVecPositional(r, &data); w.advance(n); return n; }, - .streaming_reading => { + .streaming_simple => { const dest = limit.slice(try w.writableSliceGreedy(1)); var data: [1][]u8 = .{dest}; const n = try readVecStreaming(r, &data); @@ -251,8 +251,8 @@ pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Mode) Io.Rea fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); switch (r.mode) { - .positional, .positional_reading => return readVecPositional(r, data), - .streaming, .streaming_reading => return readVecStreaming(r, data), + .positional, .positional_simple => return readVecPositional(r, data), + .streaming, .streaming_simple => return readVecStreaming(r, data), .failure => return error.ReadFailed, } } @@ -320,7 +320,7 @@ fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { const io = r.io; const file = r.file; switch (r.mode) { - .positional, .positional_reading => { + .positional, .positional_simple => { const size = r.getSize() catch { r.mode = r.mode.toStreaming(); return 0; @@ -330,7 +330,7 @@ fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { setLogicalPos(r, logical_pos + delta); return delta; }, - .streaming, .streaming_reading => { + .streaming, .streaming_simple => { // Unfortunately we can't seek forward without knowing the // size because the seek syscalls provided to us will not // return the true end position if a seek would exceed the diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig index 03e4ad8a15..1d0494c674 100644 --- a/lib/std/Io/File/Writer.zig +++ b/lib/std/Io/File/Writer.zig @@ -18,172 +18,7 @@ write_file_err: ?WriteFileError = null, seek_err: ?SeekError = null, interface: Io.Writer, -pub const Mode = union(enum) { - /// Uses `Io.VTable.fileWriteFileStreaming` if possible. Not a terminal. - /// `setColor` does nothing. - streaming, - /// Uses `Io.VTable.fileWriteFilePositional` if possible. Not a terminal. - /// `setColor` does nothing. - positional, - /// Avoids `Io.VTable.fileWriteFileStreaming`. Not a terminal. `setColor` - /// does nothing. - streaming_simple, - /// Avoids `Io.VTable.fileWriteFilePositional`. Not a terminal. `setColor` - /// does nothing. - positional_simple, - /// It's a terminal. Writes are escaped so as to strip escape sequences. - /// Color is enabled. - terminal_escaped, - /// It's a terminal. Colors are enabled via calling - /// SetConsoleTextAttribute. Writes are not escaped. - terminal_winapi: TerminalWinapi, - /// Indicates writing cannot continue because of a seek failure. - failure, - - pub fn toStreaming(m: @This()) @This() { - return switch (m) { - .positional, .streaming => .streaming, - .positional_simple, .streaming_simple => .streaming_simple, - inline else => |_, x| x, - }; - } - - pub fn toSimple(m: @This()) @This() { - return switch (m) { - .positional, .positional_simple => .positional_simple, - .streaming, .streaming_simple => .streaming_simple, - inline else => |_, x| x, - }; - } - - pub fn toUnescaped(m: @This()) @This() { - return switch (m) { - .terminal_escaped => .streaming_simple, - inline else => |_, x| x, - }; - } - - pub const TerminalWinapi = if (!is_windows) noreturn else struct { - handle: File.Handle, - reset_attributes: u16, - }; - - /// Detect suitable TTY configuration options for the given file (commonly - /// stdout/stderr). - /// - /// Will attempt to enable ANSI escape code support if necessary/possible. - pub fn detect(io: Io, file: File, want_color: bool, fallback: Mode) Io.Cancelable!Mode { - if (!want_color) return if (try file.isTty(io)) .terminal_escaped else fallback; - - if (file.enableAnsiEscapeCodes(io)) |_| { - return .terminal_escaped; - } else |err| switch (err) { - error.Canceled => return error.Canceled, - error.NotTerminalDevice, error.Unexpected => {}, - } - - if (is_windows and file.isTty(io)) { - const windows = std.os.windows; - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.FALSE) { - return .{ .terminal_winapi = .{ - .handle = file.handle, - .reset_attributes = info.wAttributes, - } }; - } - return .terminal_escaped; - } - - return fallback; - } - - pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error; - - pub fn setColor(mode: Mode, io_w: *Io.Writer, color: Color) SetColorError!void { - switch (mode) { - .streaming, .positional, .streaming_simple, .positional_simple, .failure => return, - .terminal_escaped => { - const color_string = switch (color) { - .black => "\x1b[30m", - .red => "\x1b[31m", - .green => "\x1b[32m", - .yellow => "\x1b[33m", - .blue => "\x1b[34m", - .magenta => "\x1b[35m", - .cyan => "\x1b[36m", - .white => "\x1b[37m", - .bright_black => "\x1b[90m", - .bright_red => "\x1b[91m", - .bright_green => "\x1b[92m", - .bright_yellow => "\x1b[93m", - .bright_blue => "\x1b[94m", - .bright_magenta => "\x1b[95m", - .bright_cyan => "\x1b[96m", - .bright_white => "\x1b[97m", - .bold => "\x1b[1m", - .dim => "\x1b[2m", - .reset => "\x1b[0m", - }; - try io_w.writeAll(color_string); - }, - .terminal_winapi => |ctx| { - const windows = std.os.windows; - const attributes: windows.WORD = switch (color) { - .black => 0, - .red => windows.FOREGROUND_RED, - .green => windows.FOREGROUND_GREEN, - .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, - .blue => windows.FOREGROUND_BLUE, - .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, - .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .bright_black => windows.FOREGROUND_INTENSITY, - .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, - .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - // "dim" is not supported using basic character attributes, but let's still make it do *something*. - // This matches the old behavior of TTY.Color before the bright variants were added. - .dim => windows.FOREGROUND_INTENSITY, - .reset => ctx.reset_attributes, - }; - try io_w.flush(); - try windows.SetConsoleTextAttribute(ctx.handle, attributes); - }, - } - } - - fn DecorateArgs(comptime Args: type) type { - const fields = @typeInfo(Args).@"struct".fields; - var new_fields: [fields.len]type = undefined; - for (fields, &new_fields) |old, *new| { - if (old.type == std.debug.FormatStackTrace) { - new.* = std.debug.FormatStackTrace.Decorated; - } else { - new.* = old.type; - } - } - return @Tuple(&new_fields); - } - - pub fn decorateArgs(file_writer_mode: std.Io.File.Writer.Mode, args: anytype) DecorateArgs(@TypeOf(args)) { - var new_args: DecorateArgs(@TypeOf(args)) = undefined; - inline for (args, &new_args) |old, *new| { - if (@TypeOf(old) == std.debug.FormatStackTrace) { - new.* = .{ - .stack_trace = old.stack_trace, - .file_writer_mode = file_writer_mode, - }; - } else { - new.* = old; - } - } - return new_args; - } -}; +pub const Mode = File.Reader.Mode; pub const Error = error{ DiskQuota, @@ -277,8 +112,7 @@ pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); switch (w.mode) { .positional, .positional_simple => return drainPositional(w, data, splat), - .streaming, .streaming_simple, .terminal_winapi => return drainStreaming(w, data, splat), - .terminal_escaped => return drainEscaping(w, data, splat), + .streaming, .streaming_simple => return drainStreaming(w, data, splat), .failure => return error.WriteFailed, } } @@ -319,38 +153,13 @@ fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer. return w.interface.consume(n); } -fn findTerminalEscape(buffer: []const u8) ?usize { - return std.mem.findScalar(u8, buffer, 0x1b); -} - -fn drainEscaping(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { - const io = w.io; - const header = w.interface.buffered(); - if (findTerminalEscape(header)) |i| { - _ = i; - // TODO strip terminal escape sequences here - } - for (data) |d| { - if (findTerminalEscape(d)) |i| { - _ = i; - // TODO strip terminal escape sequences here - } - } - const n = io.vtable.fileWriteStreaming(io.userdata, w.file, header, data, splat) catch |err| { - w.err = err; - return error.WriteFailed; - }; - w.pos += n; - return w.interface.consume(n); -} - pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); switch (w.mode) { .positional => return sendFilePositional(w, file_reader, limit), .positional_simple => return error.Unimplemented, .streaming => return sendFileStreaming(w, file_reader, limit), - .streaming_simple, .terminal_escaped, .terminal_winapi => return error.Unimplemented, + .streaming_simple => return error.Unimplemented, .failure => return error.WriteFailed, } } @@ -454,60 +263,7 @@ pub fn end(w: *Writer) EndError!void { .streaming, .streaming_simple, - .terminal_escaped, - .terminal_winapi, .failure, => {}, } } - -pub const Color = enum { - black, - red, - green, - yellow, - blue, - magenta, - cyan, - white, - bright_black, - bright_red, - bright_green, - bright_yellow, - bright_blue, - bright_magenta, - bright_cyan, - bright_white, - dim, - bold, - reset, -}; - -pub fn setColor(w: *Writer, color: Color) Io.Writer.Error!void { - return w.mode.setColor(&w.interface, color) catch |err| switch (err) { - error.WriteFailed => |e| return e, - else => |e| w.err = e, - }; -} - -pub fn disableEscape(w: *Writer) Mode { - const prev = w.mode; - w.mode = w.mode.toUnescaped(); - return prev; -} - -pub fn restoreEscape(w: *Writer, mode: Mode) void { - w.mode = mode; -} - -pub fn writeAllUnescaped(w: *Writer, bytes: []const u8) Io.Writer.Error!void { - const prev_mode = w.disableEscape(); - defer w.restoreEscape(prev_mode); - return w.interface.writeAll(bytes); -} - -pub fn printUnescaped(w: *Writer, comptime fmt: []const u8, args: anytype) Io.Writer.Error!void { - const prev_mode = w.disableEscape(); - defer w.restoreEscape(prev_mode); - return w.interface.print(fmt, args); -} diff --git a/lib/std/Io/Terminal.zig b/lib/std/Io/Terminal.zig new file mode 100644 index 0000000000..cd0ec56caa --- /dev/null +++ b/lib/std/Io/Terminal.zig @@ -0,0 +1,154 @@ +/// Abstraction for writing to a stream that might support terminal escape +/// codes. +const Terminal = @This(); + +const builtin = @import("builtin"); +const is_windows = builtin.os.tag == .windows; + +const std = @import("std"); +const Io = std.Io; +const File = std.Io.File; + +writer: *Io.Writer, +mode: Mode, + +pub const Color = enum { + black, + red, + green, + yellow, + blue, + magenta, + cyan, + white, + bright_black, + bright_red, + bright_green, + bright_yellow, + bright_blue, + bright_magenta, + bright_cyan, + bright_white, + dim, + bold, + reset, +}; + +pub const Mode = union(enum) { + no_color, + escape_codes, + windows_api: WindowsApi, + + pub const WindowsApi = if (!is_windows) noreturn else struct { + handle: File.Handle, + reset_attributes: u16, + }; + + /// Detect suitable TTY configuration options for the given file (commonly + /// stdout/stderr). + /// + /// Will attempt to enable ANSI escape code support if necessary/possible. + pub fn detect(io: Io, file: File) Io.Cancelable!Mode { + if (file.enableAnsiEscapeCodes(io)) |_| { + return .escape_codes; + } else |err| switch (err) { + error.Canceled => return error.Canceled, + error.NotTerminalDevice, error.Unexpected => {}, + } + + if (is_windows and file.isTty(io)) { + const windows = std.os.windows; + var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != 0) { + return .{ .terminal_winapi = .{ + .handle = file.handle, + .reset_attributes = info.wAttributes, + } }; + } + return .escape_codes; + } + + return .no_color; + } +}; + +pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error; + +pub fn setColor(t: Terminal, color: Color) Io.Writer.Error!void { + switch (t.mode) { + .no_color => return, + .escape_codes => { + const color_string = switch (color) { + .black => "\x1b[30m", + .red => "\x1b[31m", + .green => "\x1b[32m", + .yellow => "\x1b[33m", + .blue => "\x1b[34m", + .magenta => "\x1b[35m", + .cyan => "\x1b[36m", + .white => "\x1b[37m", + .bright_black => "\x1b[90m", + .bright_red => "\x1b[91m", + .bright_green => "\x1b[92m", + .bright_yellow => "\x1b[93m", + .bright_blue => "\x1b[94m", + .bright_magenta => "\x1b[95m", + .bright_cyan => "\x1b[96m", + .bright_white => "\x1b[97m", + .bold => "\x1b[1m", + .dim => "\x1b[2m", + .reset => "\x1b[0m", + }; + try t.writer.writeAll(color_string); + }, + .windows_api => |wa| { + const windows = std.os.windows; + const attributes: windows.WORD = switch (color) { + .black => 0, + .red => windows.FOREGROUND_RED, + .green => windows.FOREGROUND_GREEN, + .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, + .blue => windows.FOREGROUND_BLUE, + .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, + .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .bright_black => windows.FOREGROUND_INTENSITY, + .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, + .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + // "dim" is not supported using basic character attributes, but let's still make it do *something*. + // This matches the old behavior of TTY.Color before the bright variants were added. + .dim => windows.FOREGROUND_INTENSITY, + .reset => wa.reset_attributes, + }; + try t.writer.flush(); + try windows.SetConsoleTextAttribute(wa.handle, attributes); + }, + } +} + +pub fn disableEscape(t: *Terminal) Mode { + const prev = t.mode; + t.mode = t.mode.toUnescaped(); + return prev; +} + +pub fn restoreEscape(t: *Terminal, mode: Mode) void { + t.mode = mode; +} + +pub fn writeAllUnescaped(t: *Terminal, bytes: []const u8) Io.Writer.Error!void { + const prev_mode = t.disableEscape(); + defer t.restoreEscape(prev_mode); + return t.interface.writeAll(bytes); +} + +pub fn printUnescaped(t: *Terminal, comptime fmt: []const u8, args: anytype) Io.Writer.Error!void { + const prev_mode = t.disableEscape(); + defer t.restoreEscape(prev_mode); + return t.interface.print(fmt, args); +} diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f375bbcb7c..3e7b6950d4 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -82,8 +82,8 @@ stderr_writer: File.Writer = .{ .io = undefined, .interface = Io.File.Writer.initInterface(&.{}), .file = if (is_windows) undefined else .stderr(), - .mode = undefined, }, +stderr_mode: Io.Terminal.Mode = .no_color, stderr_writer_initialized: bool = false, pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum { @@ -755,9 +755,9 @@ pub fn io(t: *Threaded) Io { .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, - .lockStderrWriter = lockStderrWriter, - .tryLockStderrWriter = tryLockStderrWriter, - .unlockStderrWriter = unlockStderrWriter, + .lockStderr = lockStderr, + .tryLockStderr = tryLockStderr, + .unlockStderr = unlockStderr, .now = now, .sleep = sleep, @@ -887,9 +887,9 @@ pub fn ioBasic(t: *Threaded) Io { .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, - .lockStderrWriter = lockStderrWriter, - .tryLockStderrWriter = tryLockStderrWriter, - .unlockStderrWriter = unlockStderrWriter, + .lockStderr = lockStderr, + .tryLockStderr = tryLockStderr, + .unlockStderr = unlockStderr, .now = now, .sleep = sleep, @@ -10090,47 +10090,70 @@ fn netLookupFallible( return error.OptionUnsupported; } -fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*File.Writer { +fn lockStderr( + userdata: ?*anyopaque, + buffer: []u8, + terminal_mode: ?Io.Terminal.Mode, +) Io.Cancelable!Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. - Io.stderr_thread_mutex.lock(); - if (!t.stderr_writer_initialized) { - const io_t = ioBasic(t); - if (is_windows) t.stderr_writer.file = .stderr(); - t.stderr_writer.io = io_t; - t.stderr_writer.mode = try .detect(io_t, t.stderr_writer.file, true, .streaming_simple); - t.stderr_writer_initialized = true; - } - std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {}; - t.stderr_writer.interface.flush() catch {}; - t.stderr_writer.interface.buffer = buffer; - return &t.stderr_writer; + std.process.stderr_thread_mutex.lock(); + return initLockedStderr(t, buffer, terminal_mode); } -fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*File.Writer { +fn tryLockStderr( + userdata: ?*anyopaque, + buffer: []u8, + terminal_mode: ?Io.Terminal.Mode, +) Io.Cancelable!?Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. - if (!Io.stderr_thread_mutex.tryLock()) return null; + if (!std.process.stderr_thread_mutex.tryLock()) return null; + return try initLockedStderr(t, buffer, terminal_mode); +} + +fn initLockedStderr( + t: *Threaded, + buffer: []u8, + terminal_mode: ?Io.Terminal.Mode, +) Io.Cancelable!Io.LockedStderr { if (!t.stderr_writer_initialized) { const io_t = ioBasic(t); if (is_windows) t.stderr_writer.file = .stderr(); t.stderr_writer.io = io_t; - t.stderr_writer.mode = File.Writer.Mode.detect(io_t, t.stderr_writer.file, true, .streaming_simple) catch - return null; t.stderr_writer_initialized = true; + t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file); } - std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {}; - t.stderr_writer.interface.flush() catch {}; + std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch |err| switch (err) { + error.WriteFailed => switch (t.stderr_writer.err.?) { + error.Canceled => |e| return e, + else => {}, + }, + }; + t.stderr_writer.interface.flush() catch |err| switch (err) { + error.WriteFailed => switch (t.stderr_writer.err.?) { + error.Canceled => |e| return e, + else => {}, + }, + }; t.stderr_writer.interface.buffer = buffer; - return &t.stderr_writer; + return .{ + .file_writer = &t.stderr_writer, + .terminal_mode = t.stderr_mode, + }; } -fn unlockStderrWriter(userdata: ?*anyopaque) void { +fn unlockStderr(userdata: ?*anyopaque) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); - t.stderr_writer.interface.flush() catch {}; + t.stderr_writer.interface.flush() catch |err| switch (err) { + error.WriteFailed => switch (t.stderr_writer.err.?) { + error.Canceled => @panic("TODO make this uncancelable"), + else => {}, + }, + }; t.stderr_writer.interface.end = 0; t.stderr_writer.interface.buffer = &.{}; - Io.stderr_thread_mutex.unlock(); + std.process.stderr_thread_mutex.unlock(); } pub const PosixAddress = extern union { diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index 63ae6d93a0..5349eef104 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -961,7 +961,7 @@ pub fn sendFileAll(w: *Writer, file_reader: *File.Reader, limit: Limit) FileAllE const n = sendFile(w, file_reader, .limited(remaining)) catch |err| switch (err) { error.EndOfStream => break, error.Unimplemented => { - file_reader.mode = file_reader.mode.toReading(); + file_reader.mode = file_reader.mode.toSimple(); remaining -= try w.sendFileReadingAll(file_reader, .limited(remaining)); break; }, diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 6edbf9e471..54b52d48af 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -565,10 +565,10 @@ fn updateThreadRun(io: Io) void { maybeUpdateSize(resize_flag); const buffer, _ = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter(&.{})) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; } } @@ -576,18 +576,18 @@ fn updateThreadRun(io: Io) void { const resize_flag = wait(io, global_progress.refresh_rate_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - const fw = io.lockStderrWriter(&.{}) catch return; - defer io.unlockStderrWriter(); - return clearWrittenWithEscapeCodes(fw) catch {}; + const stderr = io.lockStderr(&.{}, null) catch return; + defer io.unlockStderr(); + return clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; } maybeUpdateSize(resize_flag); const buffer, _ = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter(&.{})) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; } } } @@ -609,11 +609,11 @@ fn windowsApiUpdateThreadRun(io: Io) void { maybeUpdateSize(resize_flag); const buffer, const nl_n = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter()) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); windowsApiWriteMarker(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; windowsApiMoveToMarker(nl_n) catch return; } } @@ -622,20 +622,20 @@ fn windowsApiUpdateThreadRun(io: Io) void { const resize_flag = wait(io, global_progress.refresh_rate_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - _ = io.lockStderrWriter() catch return; - defer io.unlockStderrWriter(); + _ = io.lockStderr(&.{}, null) catch return; + defer io.unlockStderr(); return clearWrittenWindowsApi() catch {}; } maybeUpdateSize(resize_flag); const buffer, const nl_n = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter()) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); clearWrittenWindowsApi() catch return; windowsApiWriteMarker(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; windowsApiMoveToMarker(nl_n) catch return; } } @@ -766,7 +766,7 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize { pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) Io.Writer.Error!void { if (noop_impl or !global_progress.need_clear) return; - try file_writer.writeAllUnescaped(clear ++ progress_remove); + try file_writer.interface.writeAll(clear ++ progress_remove); global_progress.need_clear = false; } diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 67f7d3a9fe..fd3a456ff7 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -265,29 +265,34 @@ pub const sys_can_stack_trace = switch (builtin.cpu.arch) { /// separate from the application's `Io` instance. var static_single_threaded_io: Io.Threaded = .init_single_threaded; -/// Allows the caller to freely write to stderr until `unlockStderrWriter` is called. +/// Allows the caller to freely write to stderr until `unlockStderr` is called. /// /// During the lock, any `std.Progress` information is cleared from the terminal. /// -/// The lock is recursive, so it is valid for the same thread to call `lockStderrWriter` multiple -/// times. The primary motivation is that this allows the panic handler to safely dump the stack -/// trace and panic message even if the mutex was held at the panic site. +/// The lock is recursive, so it is valid for the same thread to call +/// `lockStderr` multiple times, allowing the panic handler to safely +/// dump the stack trace and panic message even if the mutex was held at the +/// panic site. /// /// The returned `Writer` does not need to be manually flushed: flushing is -/// performed automatically when the matching `unlockStderrWriter` call occurs. +/// performed automatically when the matching `unlockStderr` call occurs. /// /// This is a low-level debugging primitive that bypasses the `Io` interface, /// writing directly to stderr using the most basic syscalls available. This /// function does not switch threads, switch stacks, or suspend. /// -/// Alternatively, use the higher-level `Io.lockStderrWriter` to integrate with -/// the application's chosen `Io` implementation. -pub fn lockStderrWriter(buffer: []u8) *File.Writer { - return static_single_threaded_io.ioBasic().lockStderrWriter(buffer) catch unreachable; +/// Alternatively, use the higher-level `Io.lockStderr` to integrate with the +/// application's chosen `Io` implementation. +pub fn lockStderr(buffer: []u8) Io.Terminal { + return (static_single_threaded_io.ioBasic().lockStderr(buffer, null) catch |err| switch (err) { + // Impossible to cancel because no calls to cancel using + // `static_single_threaded_io` exist. + error.Canceled => unreachable, + }).terminal(); } -pub fn unlockStderrWriter() void { - static_single_threaded_io.ioBasic().unlockStderrWriter(); +pub fn unlockStderr() void { + static_single_threaded_io.ioBasic().unlockStderr(); } /// Writes to stderr, ignoring errors. @@ -299,14 +304,14 @@ pub fn unlockStderrWriter() void { /// Uses a 64-byte buffer for formatted printing which is flushed before this /// function returns. /// -/// Alternatively, use the higher-level `std.log` or `Io.lockStderrWriter` to +/// Alternatively, use the higher-level `std.log` or `Io.lockStderr` to /// integrate with the application's chosen `Io` implementation. pub fn print(comptime fmt: []const u8, args: anytype) void { nosuspend { var buffer: [64]u8 = undefined; - const stderr = lockStderrWriter(&buffer); - defer unlockStderrWriter(); - stderr.interface.print(fmt, stderr.mode.decorateArgs(args)) catch return; + const stderr = lockStderr(&buffer); + defer unlockStderr(); + stderr.writer.print(fmt, args) catch return; } } @@ -322,43 +327,44 @@ pub inline fn getSelfDebugInfo() !*SelfInfo { /// Tries to print a hexadecimal view of the bytes, unbuffered, and ignores any error returned. /// Obtains the stderr mutex while dumping. pub fn dumpHex(bytes: []const u8) void { - const bw, const ttyconf = lockStderrWriter(&.{}); - defer unlockStderrWriter(); - dumpHexFallible(bw, ttyconf, bytes) catch {}; + const stderr = lockStderr(&.{}); + defer unlockStderr(); + dumpHexFallible(stderr, bytes) catch {}; } /// Prints a hexadecimal view of the bytes, returning any error that occurs. -pub fn dumpHexFallible(bw: *Writer, fwm: File.Writer.Mode, bytes: []const u8) !void { +pub fn dumpHexFallible(t: Io.Terminal, bytes: []const u8) !void { + const w = t.writer; var chunks = mem.window(u8, bytes, 16, 16); while (chunks.next()) |window| { // 1. Print the address. const address = (@intFromPtr(bytes.ptr) + 0x10 * (std.math.divCeil(usize, chunks.index orelse bytes.len, 16) catch unreachable)) - 0x10; - try fwm.setColor(bw, .dim); + try t.setColor(.dim); // We print the address in lowercase and the bytes in uppercase hexadecimal to distinguish them more. // Also, make sure all lines are aligned by padding the address. - try bw.print("{x:0>[1]} ", .{ address, @sizeOf(usize) * 2 }); - try fwm.setColor(bw, .reset); + try w.print("{x:0>[1]} ", .{ address, @sizeOf(usize) * 2 }); + try t.setColor(.reset); // 2. Print the bytes. for (window, 0..) |byte, index| { - try bw.print("{X:0>2} ", .{byte}); - if (index == 7) try bw.writeByte(' '); + try w.print("{X:0>2} ", .{byte}); + if (index == 7) try w.writeByte(' '); } - try bw.writeByte(' '); + try w.writeByte(' '); if (window.len < 16) { var missing_columns = (16 - window.len) * 3; if (window.len < 8) missing_columns += 1; - try bw.splatByteAll(' ', missing_columns); + try w.splatByteAll(' ', missing_columns); } // 3. Print the characters. for (window) |byte| { if (std.ascii.isPrint(byte)) { - try bw.writeByte(byte); + try w.writeByte(byte); } else { // Related: https://github.com/ziglang/zig/issues/7600 - if (fwm == .terminal_winapi) { - try bw.writeByte('.'); + if (t.mode == .windows_api) { + try w.writeByte('.'); continue; } @@ -366,14 +372,14 @@ pub fn dumpHexFallible(bw: *Writer, fwm: File.Writer.Mode, bytes: []const u8) !v // We don't want to do this for all control codes because most control codes apart from // the ones that Zig has escape sequences for are likely not very useful to print as symbols. switch (byte) { - '\n' => try bw.writeAll("␊"), - '\r' => try bw.writeAll("␍"), - '\t' => try bw.writeAll("␉"), - else => try bw.writeByte('.'), + '\n' => try w.writeAll("␊"), + '\r' => try w.writeAll("␍"), + '\t' => try w.writeAll("␉"), + else => try w.writeByte('.'), } } } - try bw.writeByte('\n'); + try w.writeByte('\n'); } } @@ -545,26 +551,27 @@ pub fn defaultPanic( _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); + const writer = stderr.writer; if (builtin.single_threaded) { - stderr.interface.print("panic: ", .{}) catch break :trace; + writer.print("panic: ", .{}) catch break :trace; } else { const current_thread_id = std.Thread.getCurrentId(); - stderr.interface.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; + writer.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; } - stderr.interface.print("{s}\n", .{msg}) catch break :trace; + writer.print("{s}\n", .{msg}) catch break :trace; if (@errorReturnTrace()) |t| if (t.index > 0) { - stderr.interface.writeAll("error return context:\n") catch break :trace; - writeStackTrace(t, &stderr.interface, stderr.mode) catch break :trace; - stderr.interface.writeAll("\nstack trace:\n") catch break :trace; + writer.writeAll("error return context:\n") catch break :trace; + writeStackTrace(t, stderr) catch break :trace; + writer.writeAll("\nstack trace:\n") catch break :trace; }; writeCurrentStackTrace(.{ .first_address = first_trace_addr orelse @returnAddress(), .allow_unsafe_unwind = true, // we're crashing anyway, give it our all! - }, &stderr.interface, stderr.mode) catch break :trace; + }, stderr) catch break :trace; } waitForOtherThreadToFinishPanicking(); @@ -574,8 +581,8 @@ pub fn defaultPanic( // A panic happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr = lockStderrWriter(&.{}); - stderr.interface.writeAll("aborting due to recursive panic\n") catch {}; + const stderr = lockStderr(&.{}); + stderr.writer.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } @@ -656,28 +663,29 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: /// Write the current stack trace to `writer`, annotated with source locations. /// /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing. -pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void { +pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, t: Io.Terminal) Writer.Error!void { + const writer = t.writer; if (!std.options.allow_stack_tracing) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } const di_gpa = getDebugInfoAllocator(); const di = getSelfDebugInfo() catch |err| switch (err) { error.UnsupportedTarget => { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: debug info unavailable for target\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; }, }; var it: StackIterator = .init(options.context); defer it.deinit(); if (!it.stratOk(options.allow_unsafe_unwind)) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: safe unwind unavailable for target\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } var total_frames: usize = 0; @@ -701,31 +709,31 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri error.Unexpected => "unexpected error", }; if (it.stratOk(options.allow_unsafe_unwind)) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print( "Unwind error at address `{s}:0x{x}` ({s}), remaining frames may be incorrect\n", .{ module_name, unwind_error.address, caption }, ); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; } else { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print( "Unwind error at address `{s}:0x{x}` ({s}), stopping trace early\n", .{ module_name, unwind_error.address, caption }, ); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } }, .end => break, .frame => |ret_addr| { if (total_frames > 10_000) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print( "Stopping trace after {d} frames (large frame count may indicate broken debug info)\n", .{total_frames}, ); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } total_frames += 1; @@ -735,7 +743,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm); + try printSourceAtAddress(di_gpa, io, di, t, ret_addr -| StackIterator.ra_call_offset); printed_any_frame = true; }, }; @@ -743,8 +751,8 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } /// A thin wrapper around `writeCurrentStackTrace` which writes to stderr and ignores write errors. pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); writeCurrentStackTrace(.{ .first_address = a: { if (options.first_address) |a| break :a a; @@ -753,38 +761,28 @@ pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { }, .context = options.context, .allow_unsafe_unwind = options.allow_unsafe_unwind, - }, &stderr.interface, stderr.mode) catch |err| switch (err) { + }, stderr) catch |err| switch (err) { error.WriteFailed => {}, }; } pub const FormatStackTrace = struct { stack_trace: StackTrace, + terminal_mode: Io.Terminal.Mode = .no_color, - pub const Decorated = struct { - stack_trace: StackTrace, - file_writer_mode: File.Writer.Mode, - - pub fn format(decorated: Decorated, writer: *Writer) Writer.Error!void { - try writer.writeByte('\n'); - try writeStackTrace(&decorated.stack_trace, writer, decorated.file_writer_mode); - } - }; - - pub fn format(context: FormatStackTrace, writer: *Writer) Writer.Error!void { - return Decorated.format(.{ - .stack_trace = context.stack_trace, - .file_writer_mode = .streaming, - }, writer); + pub fn format(fst: FormatStackTrace, writer: *Writer) Writer.Error!void { + try writer.writeByte('\n'); + try writeStackTrace(&fst.stack_trace, .{ .writer = writer, .mode = fst.terminal_mode }); } }; /// Write a previously captured stack trace to `writer`, annotated with source locations. -pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void { +pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void { + const writer = t.writer; if (!std.options.allow_stack_tracing) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } @@ -795,9 +793,9 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer. const di_gpa = getDebugInfoAllocator(); const di = getSelfDebugInfo() catch |err| switch (err) { error.UnsupportedTarget => { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: debug info unavailable for target\n\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; }, }; @@ -806,19 +804,19 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer. for (st.instruction_addresses[0..captured_frames]) |ret_addr| { // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm); + try printSourceAtAddress(di_gpa, io, di, t, ret_addr -| StackIterator.ra_call_offset); } if (n_frames > captured_frames) { - fwm.setColor(writer, .bold) catch {}; + t.setColor(.bold) catch {}; try writer.print("({d} additional stack frames skipped...)\n", .{n_frames - captured_frames}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; } } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. pub fn dumpStackTrace(st: *const StackTrace) void { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); - writeStackTrace(st, &stderr.interface, stderr.mode) catch |err| switch (err) { + const stderr = lockStderr(&.{}); + defer unlockStderr(); + writeStackTrace(st, stderr) catch |err| switch (err) { error.WriteFailed => {}, }; } @@ -1117,9 +1115,8 @@ fn printSourceAtAddress( gpa: Allocator, io: Io, debug_info: *SelfInfo, - writer: *Writer, + t: Io.Terminal, address: usize, - fwm: File.Writer.Mode, ) Writer.Error!void { const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) { error.MissingDebugInfo, @@ -1127,40 +1124,39 @@ fn printSourceAtAddress( error.InvalidDebugInfo, => .unknown, error.ReadFailed, error.Unexpected, error.Canceled => s: { - fwm.setColor(writer, .dim) catch {}; - try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.dim) catch {}; + try t.writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); + t.setColor(.reset) catch {}; break :s .unknown; }, error.OutOfMemory => s: { - fwm.setColor(writer, .dim) catch {}; - try writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.dim) catch {}; + try t.writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{}); + t.setColor(.reset) catch {}; break :s .unknown; }, }; defer if (symbol.source_location) |sl| gpa.free(sl.file_name); return printLineInfo( io, - writer, + t, symbol.source_location, address, symbol.name orelse "???", symbol.compile_unit_name orelse debug_info.getModuleName(gpa, address) catch "???", - fwm, ); } fn printLineInfo( io: Io, - writer: *Writer, + t: Io.Terminal, source_location: ?SourceLocation, address: usize, symbol_name: []const u8, compile_unit_name: []const u8, - fwm: File.Writer.Mode, ) Writer.Error!void { nosuspend { - fwm.setColor(writer, .bold) catch {}; + const writer = t.writer; + t.setColor(.bold) catch {}; if (source_location) |*sl| { try writer.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column }); @@ -1168,11 +1164,11 @@ fn printLineInfo( try writer.writeAll("???:?:?"); } - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; try writer.writeAll(": "); - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name }); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; try writer.writeAll("\n"); // Show the matching source code line if possible @@ -1183,9 +1179,9 @@ fn printLineInfo( const space_needed = @as(usize, @intCast(sl.column - 1)); try writer.splatByteAll(' ', space_needed); - fwm.setColor(writer, .green) catch {}; + t.setColor(.green) catch {}; try writer.writeAll("^"); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; } try writer.writeAll("\n"); } else |_| { @@ -1554,19 +1550,19 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); if (addr) |a| { - stderr.interface.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace; + stderr.writer.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace; } else { - stderr.interface.print("{s} (no address available)\n", .{name}) catch break :trace; + stderr.writer.print("{s} (no address available)\n", .{name}) catch break :trace; } if (opt_ctx) |context| { writeCurrentStackTrace(.{ .context = context, .allow_unsafe_unwind = true, // we're crashing anyway, give it our all! - }, &stderr.interface, stderr.mode) catch break :trace; + }, stderr) catch break :trace; } } }, @@ -1575,8 +1571,8 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex // A segfault happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr = lockStderrWriter(&.{}); - stderr.interface.writeAll("aborting due to recursive panic\n") catch {}; + const stderr = lockStderr(&.{}); + stderr.writer.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } @@ -1682,21 +1678,21 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize pub fn dump(t: @This()) void { if (!enabled) return; - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); const end = @min(t.index, size); for (t.addrs[0..end], 0..) |frames_array, i| { - stderr.interface.print("{s}:\n", .{t.notes[i]}) catch return; + stderr.writer.print("{s}:\n", .{t.notes[i]}) catch return; var frames_array_mutable = frames_array; const frames = mem.sliceTo(frames_array_mutable[0..], 0); const stack_trace: StackTrace = .{ .index = frames.len, .instruction_addresses = frames, }; - writeStackTrace(&stack_trace, &stderr.interface, stderr.mode) catch return; + writeStackTrace(&stack_trace, stderr) catch return; } if (t.index > end) { - stderr.interface.print("{d} more traces not shown; consider increasing trace size\n", .{ + stderr.writer.print("{d} more traces not shown; consider increasing trace size\n", .{ t.index - end, }) catch return; } diff --git a/lib/std/log.zig b/lib/std/log.zig index b75bada580..8579bb04e0 100644 --- a/lib/std/log.zig +++ b/lib/std/log.zig @@ -92,35 +92,33 @@ pub fn defaultLog( args: anytype, ) void { var buffer: [64]u8 = undefined; - const stderr = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); - return defaultLogFileWriter(level, scope, format, args, stderr); + const stderr = std.debug.lockStderr(&buffer); + defer std.debug.unlockStderr(); + return defaultLogFileTerminal(level, scope, format, args, stderr) catch {}; } -pub fn defaultLogFileWriter( +pub fn defaultLogFileTerminal( comptime level: Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype, - fw: *std.Io.File.Writer, -) void { - fw.setColor(switch (level) { + t: std.Io.Terminal, +) std.Io.Writer.Error!void { + t.setColor(switch (level) { .err => .red, .warn => .yellow, .info => .green, .debug => .magenta, }) catch {}; - fw.setColor(.bold) catch {}; - fw.interface.writeAll(level.asText()) catch return; - fw.setColor(.reset) catch {}; - fw.setColor(.dim) catch {}; - fw.setColor(.bold) catch {}; - if (scope != .default) { - fw.interface.print("({s})", .{@tagName(scope)}) catch return; - } - fw.interface.writeAll(": ") catch return; - fw.setColor(.reset) catch {}; - fw.interface.print(format ++ "\n", fw.mode.decorateArgs(args)) catch return; + t.setColor(.bold) catch {}; + try t.writer.writeAll(level.asText()); + t.setColor(.reset) catch {}; + t.setColor(.dim) catch {}; + t.setColor(.bold) catch {}; + if (scope != .default) try t.writer.print("({t})", .{scope}); + try t.writer.writeAll(": "); + t.setColor(.reset) catch {}; + try t.writer.print(format ++ "\n", args); } /// Returns a scoped logging namespace that logs all messages using the scope diff --git a/lib/std/process.zig b/lib/std/process.zig index 4b4c27fe20..45cd575133 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -21,6 +21,13 @@ pub const changeCurDirZ = posix.chdirZ; pub const GetCwdError = posix.GetCwdError; +/// This is the global, process-wide protection to coordinate stderr writes. +/// +/// The primary motivation for recursive mutex here is so that a panic while +/// stderr mutex is held still dumps the stack trace and other debug +/// information. +pub var stderr_thread_mutex: std.Thread.Mutex.Recursive = .init; + /// The result is a slice of `out_buffer`, from index `0`. /// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On other platforms, the result is an opaque sequence of bytes with no particular encoding. |
