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.
This commit is contained in:
Andrew Kelley 2025-12-09 22:10:12 -08:00
parent 78d262d96e
commit ffcbd48a12
10 changed files with 448 additions and 400 deletions

View file

@ -82,8 +82,6 @@ pub const Limit = enum(usize) {
pub const Reader = @import("Io/Reader.zig");
pub const Writer = @import("Io/Writer.zig");
pub const tty = @import("Io/tty.zig");
pub fn poll(
gpa: Allocator,
comptime StreamEnum: type,
@ -535,7 +533,6 @@ test {
_ = net;
_ = Reader;
_ = Writer;
_ = tty;
_ = Evented;
_ = Threaded;
_ = @import("Io/test.zig");
@ -720,6 +717,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,
now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp,
sleep: *const fn (?*anyopaque, Timeout) SleepError!void,
@ -740,10 +740,6 @@ pub const VTable = struct {
netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface,
netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name,
netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) net.HostName.LookupError!void,
lockStderrWriter: *const fn (?*anyopaque, buffer: []u8) Cancelable!*Writer,
tryLockStderrWriter: *const fn (?*anyopaque, buffer: []u8) ?*Writer,
unlockStderrWriter: *const fn (?*anyopaque) void,
};
pub const Cancelable = error{
@ -2186,13 +2182,17 @@ pub fn select(io: Io, s: anytype) Cancelable!SelectUnion(@TypeOf(s)) {
///
/// See also:
/// * `tryLockStderrWriter`
pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*Writer {
return io.vtable.lockStderrWriter(io.userdata, buffer);
pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*File.Writer {
const result = try io.vtable.lockStderrWriter(io.userdata, buffer);
result.io = io;
return result;
}
/// Same as `lockStderrWriter` but uncancelable and non-blocking.
pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*Writer {
return io.vtable.tryLockStderrWriter(io.userdata, buffer);
pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*File.Writer {
const result = io.vtable.tryLockStderrWriter(io.userdata, buffer) orelse return null;
result.io = io;
return result;
}
pub fn unlockStderrWriter(io: Io) void {

View file

@ -1,4 +1,6 @@
const Writer = @This();
const builtin = @import("builtin");
const is_windows = builtin.os.tag == .windows;
const std = @import("../../std.zig");
const Io = std.Io;
@ -16,7 +18,144 @@ write_file_err: ?WriteFileError = null,
seek_err: ?SeekError = null,
interface: Io.Writer,
pub const Mode = File.Reader.Mode;
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) Mode.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);
},
}
}
};
pub const Error = error{
DiskQuota,
@ -74,6 +213,16 @@ pub fn initStreaming(file: File, io: Io, buffer: []u8) Writer {
};
}
/// Detects if `file` is terminal and sets the mode accordingly.
pub fn initDetect(file: File, io: Io, buffer: []u8) Io.Cancelable!Writer {
return .{
.io = io,
.file = file,
.interface = initInterface(buffer),
.mode = try .detect(io, file, true, .positional),
};
}
pub fn initInterface(buffer: []u8) Io.Writer {
return .{
.vtable = &.{
@ -99,8 +248,9 @@ pub fn moveToReader(w: *Writer) File.Reader {
pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
switch (w.mode) {
.positional, .positional_reading => return drainPositional(w, data, splat),
.streaming, .streaming_reading => return drainStreaming(w, data, splat),
.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),
.failure => return error.WriteFailed,
}
}
@ -141,13 +291,38 @@ 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;
@panic("TODO strip terminal escape sequence");
}
for (data) |d| {
if (findTerminalEscape(d)) |i| {
_ = i;
@panic("TODO strip terminal escape sequence");
}
}
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_reading => return error.Unimplemented,
.positional_simple => return error.Unimplemented,
.streaming => return sendFileStreaming(w, file_reader, limit),
.streaming_reading => return error.Unimplemented,
.streaming_simple, .terminal_escaped, .terminal_winapi => return error.Unimplemented,
.failure => return error.WriteFailed,
}
}
@ -214,10 +389,10 @@ pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void {
assert(w.interface.buffered().len == 0);
const io = w.io;
switch (w.mode) {
.positional, .positional_reading => {
.positional, .positional_simple => {
w.pos = offset;
},
.streaming, .streaming_reading => {
.streaming, .streaming_simple, .terminal_escaped, .terminal_winapi => {
if (w.seek_err) |err| return err;
io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| {
w.seek_err = err;
@ -243,15 +418,65 @@ pub fn end(w: *Writer) EndError!void {
try w.interface.flush();
switch (w.mode) {
.positional,
.positional_reading,
.positional_simple,
=> w.file.setLength(io, w.pos) catch |err| switch (err) {
error.NonResizable => return,
else => |e| return e,
},
.streaming,
.streaming_reading,
.streaming_simple,
.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 const SetColorError = Mode.SetColorError;
pub fn setColor(w: *Writer, color: Color) SetColorError!void {
return w.mode.setColor(&w.interface, color);
}
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.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.Error!void {
const prev_mode = w.disableEscape();
defer w.restoreEscape(prev_mode);
return w.interface.print(fmt, args);
}

View file

@ -77,7 +77,13 @@ use_sendfile: UseSendfile = .default,
use_copy_file_range: UseCopyFileRange = .default,
use_fcopyfile: UseFcopyfile = .default,
stderr_writer: Io.Writer,
stderr_writer: File.Writer = .{
.io = undefined,
.interface = Io.File.Writer.initInterface(&.{}),
.file = if (is_windows) undefined else .stderr(),
.mode = undefined,
},
stderr_writer_initialized: bool = false,
pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum {
enabled,
@ -737,6 +743,9 @@ pub fn io(t: *Threaded) Io {
.processExecutableOpen = processExecutableOpen,
.processExecutablePath = processExecutablePath,
.lockStderrWriter = lockStderrWriter,
.tryLockStderrWriter = tryLockStderrWriter,
.unlockStderrWriter = unlockStderrWriter,
.now = now,
.sleep = sleep,
@ -864,6 +873,9 @@ pub fn ioBasic(t: *Threaded) Io {
.processExecutableOpen = processExecutableOpen,
.processExecutablePath = processExecutablePath,
.lockStderrWriter = lockStderrWriter,
.tryLockStderrWriter = tryLockStderrWriter,
.unlockStderrWriter = unlockStderrWriter,
.now = now,
.sleep = sleep,
@ -9516,33 +9528,42 @@ fn netLookupFallible(
return error.OptionUnsupported;
}
fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*Io.Writer {
fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*File.Writer {
const t: *Threaded = @ptrCast(@alignCast(userdata));
// Only global mutex since this is Threaded.
Io.stderr_thread_mutex.lock();
if (is_windows) t.stderr_writer.file = .stderr();
if (!t.stderr_writer_initialized) {
if (is_windows) t.stderr_writer.file = .stderr();
t.stderr_writer.mode = try .detect(ioBasic(t), t.stderr_writer.file, true, .streaming_simple);
t.stderr_writer_initialized = true;
}
std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {};
t.stderr_writer.flush() catch {};
t.stderr_writer.buffer = buffer;
t.stderr_writer.interface.flush() catch {};
t.stderr_writer.interface.buffer = buffer;
return &t.stderr_writer;
}
fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*Io.Writer {
fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*File.Writer {
const t: *Threaded = @ptrCast(@alignCast(userdata));
// Only global mutex since this is Threaded.
if (!Io.stderr_thread_mutex.tryLock()) return null;
std.Progress.clearWrittenWithEscapeCodes(t.io()) catch {};
if (is_windows) t.stderr_writer.file = .stderr();
t.stderr_writer.flush() catch {};
t.stderr_writer.buffer = buffer;
if (!t.stderr_writer_initialized) {
if (is_windows) t.stderr_writer.file = .stderr();
t.stderr_writer.mode = File.Writer.Mode.detect(ioBasic(t), t.stderr_writer.file, true, .streaming_simple) catch
return null;
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;
}
fn unlockStderrWriter(userdata: ?*anyopaque) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
t.stderr_writer.flush() catch {};
t.stderr_writer.end = 0;
t.stderr_writer.buffer = &.{};
t.stderr_writer.interface.flush() catch {};
t.stderr_writer.interface.end = 0;
t.stderr_writer.interface.buffer = &.{};
Io.stderr_thread_mutex.unlock();
}

View file

@ -1,135 +0,0 @@
const builtin = @import("builtin");
const native_os = builtin.os.tag;
const std = @import("std");
const Io = std.Io;
const File = std.Io.File;
const process = std.process;
const windows = std.os.windows;
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,
};
/// Provides simple functionality for manipulating the terminal in some way,
/// such as coloring text, etc.
pub const Config = union(enum) {
no_color,
escape_codes,
windows_api: if (native_os == .windows) WindowsContext else noreturn,
/// Detect suitable TTY configuration options for the given file (commonly stdout/stderr).
/// This includes feature checks for ANSI escape codes and the Windows console API, as well as
/// respecting the `NO_COLOR` and `CLICOLOR_FORCE` environment variables to override the default.
/// Will attempt to enable ANSI escape code support if necessary/possible.
pub fn detect(io: Io, file: File) Config {
const force_color: ?bool = if (builtin.os.tag == .wasi)
null // wasi does not support environment variables
else if (process.hasNonEmptyEnvVarConstant("NO_COLOR"))
false
else if (process.hasNonEmptyEnvVarConstant("CLICOLOR_FORCE"))
true
else
null;
if (force_color == false) return .no_color;
if (file.enableAnsiEscapeCodes(io)) |_| {
return .escape_codes;
} else |_| {}
if (native_os == .windows and file.isTty()) {
var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) == windows.FALSE) {
return if (force_color == true) .escape_codes else .no_color;
}
return .{ .windows_api = .{
.handle = file.handle,
.reset_attributes = info.wAttributes,
} };
}
return if (force_color == true) .escape_codes else .no_color;
}
pub const WindowsContext = struct {
handle: File.Handle,
reset_attributes: u16,
};
pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error;
pub fn setColor(conf: Config, w: *Io.Writer, color: Color) SetColorError!void {
nosuspend switch (conf) {
.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 w.writeAll(color_string);
},
.windows_api => |ctx| {
const attributes = 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 w.flush();
try windows.SetConsoleTextAttribute(ctx.handle, attributes);
},
};
}
};

View file

@ -755,10 +755,9 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
}
}
fn clearWrittenWithEscapeCodes(w: *Io.Writer) anyerror!void {
pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) anyerror!void {
if (noop_impl or !global_progress.need_clear) return;
try w.writeAll(clear ++ progress_remove);
try file_writer.interface.writeAllUnescaped(clear ++ progress_remove);
global_progress.need_clear = false;
}

View file

@ -1,7 +1,6 @@
const std = @import("std.zig");
const Io = std.Io;
const Writer = std.Io.Writer;
const tty = std.Io.tty;
const math = std.math;
const mem = std.mem;
const posix = std.posix;
@ -262,6 +261,10 @@ pub const sys_can_stack_trace = switch (builtin.cpu.arch) {
else => true,
};
/// This is used for debug information and debug printing. It is intentionally
/// 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.
///
/// 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) {
///
/// Alternatively, use the higher-level `Io.lockStderrWriter` to integrate with
/// the application's chosen `Io` implementation.
pub fn lockStderrWriter(buffer: []u8) struct { *Writer, tty.Config } {
Io.stderr_thread_mutex.lock();
const w = std.Progress.lockStderrWriter(buffer);
// The stderr lock also locks access to `global.conf`.
if (StderrWriter.singleton.tty_config == null) {
StderrWriter.singleton.tty_config = .detect(io, .stderr());
}
return .{ w, global.conf.? };
pub fn lockStderrWriter(buffer: []u8) *File.Writer {
return static_single_threaded_io.ioBasic().lockStderrWriter(buffer) catch unreachable;
}
pub fn unlockStderrWriter() void {
std.Progress.unlockStderrWriter();
static_single_threaded_io.ioBasic().unlockStderrWriter();
}
/// Writes to stderr, ignoring errors.
@ -305,39 +302,13 @@ pub fn unlockStderrWriter() void {
/// Alternatively, use the higher-level `std.log` or `Io.lockStderrWriter` to
/// integrate with the application's chosen `Io` implementation.
pub fn print(comptime fmt: []const u8, args: anytype) void {
var buffer: [64]u8 = undefined;
const bw, _ = lockStderrWriter(&buffer);
defer unlockStderrWriter();
nosuspend bw.print(fmt, args) catch return;
}
const StderrWriter = struct {
interface: Writer,
tty_config: ?tty.Config,
var singleton: StderrWriter = .{
.interface = .{
.buffer = &.{},
.vtable = &.{ .drain = drain },
},
.tty_config = null,
};
fn drain(io_w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
var n: usize = 0;
const header = w.interface.buffered();
if (header.len != 0) n += try std.Io.Threaded.debugWrite(header);
for (data[0 .. data.len - 1]) |d| {
if (d.len != 0) n += try std.Io.Threaded.debugWrite(d);
}
const pattern = data[data.len - 1];
if (pattern.len != 0) {
for (0..splat) |_| n += try std.Io.Threaded.debugWrite(pattern);
}
return io_w.consume(n);
nosuspend {
var buffer: [64]u8 = undefined;
const stderr = lockStderrWriter(&buffer);
defer unlockStderrWriter();
stderr.interface.print(fmt, args) catch return;
}
};
}
/// Marked `inline` to propagate a comptime-known error to callers.
pub inline fn getSelfDebugInfo() !*SelfInfo {
@ -357,16 +328,16 @@ pub fn dumpHex(bytes: []const u8) void {
}
/// Prints a hexadecimal view of the bytes, returning any error that occurs.
pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) !void {
pub fn dumpHexFallible(bw: *Writer, fwm: File.Writer.Mode, bytes: []const u8) !void {
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 tty_config.setColor(bw, .dim);
try fwm.setColor(bw, .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 tty_config.setColor(bw, .reset);
try fwm.setColor(bw, .reset);
// 2. Print the bytes.
for (window, 0..) |byte, index| {
@ -386,7 +357,7 @@ pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) !
try bw.writeByte(byte);
} else {
// Related: https://github.com/ziglang/zig/issues/7600
if (tty_config == .windows_api) {
if (fwm == .terminal_winapi) {
try bw.writeByte('.');
continue;
}
@ -408,11 +379,11 @@ pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) !
test dumpHexFallible {
const bytes: []const u8 = &.{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x12, 0x13 };
var aw: Writer.Allocating = .init(std.testing.allocator);
var aw: Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try dumpHexFallible(&aw.writer, .no_color, bytes);
const expected = try std.fmt.allocPrint(std.testing.allocator,
const expected = try std.fmt.allocPrint(testing.allocator,
\\{x:0>[2]} 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........
\\{x:0>[2]} 01 12 13 ...
\\
@ -421,8 +392,8 @@ test dumpHexFallible {
@intFromPtr(bytes.ptr) + 16,
@sizeOf(usize) * 2,
});
defer std.testing.allocator.free(expected);
try std.testing.expectEqualStrings(expected, aw.written());
defer testing.allocator.free(expected);
try testing.expectEqualStrings(expected, aw.written());
}
/// 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
/// away, and in fact the optimizer is able to use the assertion in its
/// heuristics.
///
/// Inside a test block, it is best to use the `std.testing` module rather than
/// Inside a test block, it is best to use the `testing` module rather than
/// this function, because this function may not detect a test failure in
/// ReleaseFast and ReleaseSmall mode. Outside of a test block, this assert
/// function is the correct function to use.
@ -574,26 +545,26 @@ pub fn defaultPanic(
_ = panicking.fetchAdd(1, .seq_cst);
trace: {
const stderr, const tty_config = lockStderrWriter(&.{});
const stderr = lockStderrWriter(&.{});
defer unlockStderrWriter();
if (builtin.single_threaded) {
stderr.print("panic: ", .{}) catch break :trace;
stderr.interface.print("panic: ", .{}) catch break :trace;
} else {
const current_thread_id = std.Thread.getCurrentId();
stderr.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
stderr.interface.print("thread {d} panic: ", .{current_thread_id}) catch break :trace;
}
stderr.print("{s}\n", .{msg}) catch break :trace;
stderr.interface.print("{s}\n", .{msg}) catch break :trace;
if (@errorReturnTrace()) |t| if (t.index > 0) {
stderr.writeAll("error return context:\n") catch break :trace;
writeStackTrace(t, stderr, tty_config) catch break :trace;
stderr.writeAll("\nstack trace:\n") catch break :trace;
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;
};
writeCurrentStackTrace(.{
.first_address = first_trace_addr orelse @returnAddress(),
.allow_unsafe_unwind = true, // we're crashing anyway, give it our all!
}, stderr, tty_config) catch break :trace;
}, &stderr.interface, stderr.mode) catch break :trace;
}
waitForOtherThreadToFinishPanicking();
@ -603,8 +574,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.writeAll("aborting due to recursive panic\n") catch {};
const stderr = lockStderrWriter(&.{});
stderr.interface.writeAll("aborting due to recursive panic\n") catch {};
},
else => {}, // Panicked while printing the recursive panic message.
}
@ -651,8 +622,7 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:
defer it.deinit();
if (!it.stratOk(options.allow_unsafe_unwind)) return empty_trace;
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
const io = static_single_threaded_io.ioBasic();
var total_frames: usize = 0;
var index: usize = 0;
@ -686,36 +656,34 @@ 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, tty_config: tty.Config) Writer.Error!void {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void {
if (!std.options.allow_stack_tracing) {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
return;
}
const di_gpa = getDebugInfoAllocator();
const di = getSelfDebugInfo() catch |err| switch (err) {
error.UnsupportedTarget => {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("Cannot print stack trace: debug info unavailable for target\n", .{});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
return;
},
};
var it: StackIterator = .init(options.context);
defer it.deinit();
if (!it.stratOk(options.allow_unsafe_unwind)) {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("Cannot print stack trace: safe unwind unavailable for target\n", .{});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
return;
}
var total_frames: usize = 0;
var wait_for = options.first_address;
var printed_any_frame = false;
const io = static_single_threaded_io.ioBasic();
while (true) switch (it.next(io)) {
.switch_to_fp => |unwind_error| {
switch (StackIterator.fp_usability) {
@ -733,31 +701,31 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri
error.Unexpected => "unexpected error",
};
if (it.stratOk(options.allow_unsafe_unwind)) {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .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 },
);
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
} else {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print(
"Unwind error at address `{s}:0x{x}` ({s}), stopping trace early\n",
.{ module_name, unwind_error.address, caption },
);
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
return;
}
},
.end => break,
.frame => |ret_addr| {
if (total_frames > 10_000) {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print(
"Stopping trace after {d} frames (large frame count may indicate broken debug info)\n",
.{total_frames},
);
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
return;
}
total_frames += 1;
@ -767,7 +735,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, tty_config);
try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm);
printed_any_frame = true;
},
};
@ -775,7 +743,7 @@ 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, const tty_config = lockStderrWriter(&.{});
const stderr = lockStderrWriter(&.{});
defer unlockStderrWriter();
writeCurrentStackTrace(.{
.first_address = a: {
@ -785,33 +753,40 @@ pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void {
},
.context = options.context,
.allow_unsafe_unwind = options.allow_unsafe_unwind,
}, stderr, tty_config) catch |err| switch (err) {
}, &stderr.interface, stderr.mode) catch |err| switch (err) {
error.WriteFailed => {},
};
}
pub const FormatStackTrace = struct {
stack_trace: StackTrace,
tty_config: tty.Config,
pub fn format(context: @This(), writer: *Writer) Writer.Error!void {
try writer.writeAll("\n");
try writeStackTrace(&context.stack_trace, writer, context.tty_config);
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);
}
};
/// Write a previously captured stack trace to `writer`, annotated with source locations.
pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void {
pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void {
if (!std.options.allow_stack_tracing) {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
return;
}
// We use an independent Io implementation here in case there was a problem
// with the application's Io implementation itself.
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
// Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if
// `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
const di_gpa = getDebugInfoAllocator();
const di = getSelfDebugInfo() catch |err| switch (err) {
error.UnsupportedTarget => {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("Cannot print stack trace: debug info unavailable for target\n\n", .{});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
return;
},
};
const io = static_single_threaded_io.ioBasic();
const captured_frames = @min(n_frames, st.instruction_addresses.len);
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, tty_config);
try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm);
}
if (n_frames > captured_frames) {
tty_config.setColor(writer, .bold) catch {};
fwm.setColor(writer, .bold) catch {};
try writer.print("({d} additional stack frames skipped...)\n", .{n_frames - captured_frames});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
}
}
/// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors.
@ -1143,7 +1119,7 @@ fn printSourceAtAddress(
debug_info: *SelfInfo,
writer: *Writer,
address: usize,
tty_config: tty.Config,
fwm: File.Writer.Mode,
) Writer.Error!void {
const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) {
error.MissingDebugInfo,
@ -1151,15 +1127,15 @@ fn printSourceAtAddress(
error.InvalidDebugInfo,
=> .unknown,
error.ReadFailed, error.Unexpected, error.Canceled => s: {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
break :s .unknown;
},
error.OutOfMemory => s: {
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{});
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
break :s .unknown;
},
};
@ -1171,7 +1147,7 @@ fn printSourceAtAddress(
address,
symbol.name orelse "???",
symbol.compile_unit_name orelse debug_info.getModuleName(gpa, address) catch "???",
tty_config,
fwm,
);
}
fn printLineInfo(
@ -1181,10 +1157,10 @@ fn printLineInfo(
address: usize,
symbol_name: []const u8,
compile_unit_name: []const u8,
tty_config: tty.Config,
fwm: File.Writer.Mode,
) Writer.Error!void {
nosuspend {
tty_config.setColor(writer, .bold) catch {};
fwm.setColor(writer, .bold) catch {};
if (source_location) |*sl| {
try writer.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column });
@ -1192,11 +1168,11 @@ fn printLineInfo(
try writer.writeAll("???:?:?");
}
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
try writer.writeAll(": ");
tty_config.setColor(writer, .dim) catch {};
fwm.setColor(writer, .dim) catch {};
try writer.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name });
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
try writer.writeAll("\n");
// Show the matching source code line if possible
@ -1207,9 +1183,9 @@ fn printLineInfo(
const space_needed = @as(usize, @intCast(sl.column - 1));
try writer.splatByteAll(' ', space_needed);
tty_config.setColor(writer, .green) catch {};
fwm.setColor(writer, .green) catch {};
try writer.writeAll("^");
tty_config.setColor(writer, .reset) catch {};
fwm.setColor(writer, .reset) catch {};
}
try writer.writeAll("\n");
} else |_| {
@ -1250,18 +1226,18 @@ fn printLineFromFile(io: Io, writer: *Writer, source_location: SourceLocation) !
}
test printLineFromFile {
const io = std.testing.io;
const gpa = std.testing.allocator;
const io = testing.io;
const gpa = testing.allocator;
var aw: Writer.Allocating = .init(gpa);
defer aw.deinit();
const output_stream = &aw.writer;
const join = std.fs.path.join;
const expectError = std.testing.expectError;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectError = testing.expectError;
const expectEqualStrings = testing.expectEqualStrings;
var test_dir = std.testing.tmpDir(.{});
var test_dir = testing.tmpDir(.{});
defer test_dir.cleanup();
// Relies on testing.tmpDir internals which is not ideal, but SourceLocation requires paths.
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
_ = panicking.fetchAdd(1, .seq_cst);
trace: {
const stderr, const tty_config = lockStderrWriter(&.{});
const stderr = lockStderrWriter(&.{});
defer unlockStderrWriter();
if (addr) |a| {
stderr.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
stderr.interface.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace;
} else {
stderr.print("{s} (no address available)\n", .{name}) catch break :trace;
stderr.interface.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, tty_config) catch break :trace;
}, &stderr.interface, stderr.mode) catch break :trace;
}
}
},
@ -1599,8 +1575,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.writeAll("aborting due to recursive panic\n") catch {};
const stderr = lockStderrWriter(&.{});
stderr.interface.writeAll("aborting due to recursive panic\n") catch {};
},
else => {}, // Panicked while printing the recursive panic message.
}
@ -1632,9 +1608,9 @@ test "manage resources correctly" {
return @returnAddress();
}
};
const gpa = std.testing.allocator;
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
const gpa = testing.allocator;
const io = testing.io;
var discarding: Writer.Discarding = .init(&.{});
var di: SelfInfo = .init;
defer di.deinit(gpa);

View file

@ -179,8 +179,6 @@ pub fn DebugAllocator(comptime config: Config) type {
total_requested_bytes: @TypeOf(total_requested_bytes_init) = total_requested_bytes_init,
requested_memory_limit: @TypeOf(requested_memory_limit_init) = requested_memory_limit_init,
mutex: @TypeOf(mutex_init) = mutex_init,
/// Set this value differently to affect how errors and leaks are logged.
tty_config: std.Io.tty.Config = .no_color,
const Self = @This();
@ -427,7 +425,6 @@ pub fn DebugAllocator(comptime config: Config) type {
bucket: *BucketHeader,
size_class_index: usize,
used_bits_count: usize,
tty_config: std.Io.tty.Config,
) usize {
const size_class = @as(usize, 1) << @as(Log2USize, @intCast(size_class_index));
const slot_count = slot_counts[size_class_index];
@ -444,11 +441,7 @@ pub fn DebugAllocator(comptime config: Config) type {
const page_addr = @intFromPtr(bucket) & ~(page_size - 1);
const addr = page_addr + slot_index * size_class;
log.err("memory address 0x{x} leaked: {f}", .{
addr,
std.debug.FormatStackTrace{
.stack_trace = stack_trace,
.tty_config = tty_config,
},
addr, std.debug.FormatStackTrace{ .stack_trace = stack_trace },
});
leaks += 1;
}
@ -460,8 +453,6 @@ pub fn DebugAllocator(comptime config: Config) type {
/// Emits log messages for leaks and then returns the number of detected leaks (0 if no leaks were detected).
pub fn detectLeaks(self: *Self) usize {
const tty_config = self.tty_config;
var leaks: usize = 0;
for (self.buckets, 0..) |init_optional_bucket, size_class_index| {
@ -469,7 +460,7 @@ pub fn DebugAllocator(comptime config: Config) type {
const slot_count = slot_counts[size_class_index];
const used_bits_count = usedBitsCount(slot_count);
while (optional_bucket) |bucket| {
leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count, tty_config);
leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count);
optional_bucket = bucket.prev;
}
}
@ -480,10 +471,7 @@ pub fn DebugAllocator(comptime config: Config) type {
const stack_trace = large_alloc.getStackTrace(.alloc);
log.err("memory address 0x{x} leaked: {f}", .{
@intFromPtr(large_alloc.bytes.ptr),
std.debug.FormatStackTrace{
.stack_trace = stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = stack_trace },
});
leaks += 1;
}
@ -535,28 +523,14 @@ pub fn DebugAllocator(comptime config: Config) type {
@memset(addr_buf[@min(st.index, addr_buf.len)..], 0);
}
fn reportDoubleFree(
tty_config: std.Io.tty.Config,
ret_addr: usize,
alloc_stack_trace: StackTrace,
free_stack_trace: StackTrace,
) void {
fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void {
@branchHint(.cold);
var addr_buf: [stack_n]usize = undefined;
const second_free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf);
log.err("Double free detected. Allocation: {f} First free: {f} Second free: {f}", .{
std.debug.FormatStackTrace{
.stack_trace = alloc_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = second_free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = alloc_stack_trace },
std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
std.debug.FormatStackTrace{ .stack_trace = second_free_stack_trace },
});
}
@ -587,7 +561,7 @@ pub fn DebugAllocator(comptime config: Config) type {
if (config.retain_metadata and entry.value_ptr.freed) {
if (config.safety) {
reportDoubleFree(self.tty_config, ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
@panic("Unrecoverable double free");
} else {
unreachable;
@ -598,18 +572,11 @@ pub fn DebugAllocator(comptime config: Config) type {
@branchHint(.cold);
var addr_buf: [stack_n]usize = undefined;
const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf);
const tty_config = self.tty_config;
log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
entry.value_ptr.bytes.len,
old_mem.len,
std.debug.FormatStackTrace{
.stack_trace = entry.value_ptr.getStackTrace(.alloc),
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = entry.value_ptr.getStackTrace(.alloc) },
std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
});
}
@ -701,7 +668,7 @@ pub fn DebugAllocator(comptime config: Config) type {
if (config.retain_metadata and entry.value_ptr.freed) {
if (config.safety) {
reportDoubleFree(self.tty_config, ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
return;
} else {
unreachable;
@ -712,18 +679,11 @@ pub fn DebugAllocator(comptime config: Config) type {
@branchHint(.cold);
var addr_buf: [stack_n]usize = undefined;
const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf);
const tty_config = self.tty_config;
log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
entry.value_ptr.bytes.len,
old_mem.len,
std.debug.FormatStackTrace{
.stack_trace = entry.value_ptr.getStackTrace(.alloc),
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = entry.value_ptr.getStackTrace(.alloc) },
std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
});
}
@ -924,7 +884,6 @@ pub fn DebugAllocator(comptime config: Config) type {
if (!is_used) {
if (config.safety) {
reportDoubleFree(
self.tty_config,
return_address,
bucketStackTrace(bucket, slot_count, slot_index, .alloc),
bucketStackTrace(bucket, slot_count, slot_index, .free),
@ -946,34 +905,24 @@ pub fn DebugAllocator(comptime config: Config) type {
const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf);
if (old_memory.len != requested_size) {
@branchHint(.cold);
const tty_config = self.tty_config;
log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
requested_size,
old_memory.len,
std.debug.FormatStackTrace{
.stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
});
}
if (alignment != slot_alignment) {
@branchHint(.cold);
const tty_config = self.tty_config;
log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{
slot_alignment.toByteUnits(),
alignment.toByteUnits(),
std.debug.FormatStackTrace{
.stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
});
}
}
@ -1040,7 +989,6 @@ pub fn DebugAllocator(comptime config: Config) type {
const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0;
if (!is_used) {
reportDoubleFree(
self.tty_config,
return_address,
bucketStackTrace(bucket, slot_count, slot_index, .alloc),
bucketStackTrace(bucket, slot_count, slot_index, .free),
@ -1058,34 +1006,24 @@ pub fn DebugAllocator(comptime config: Config) type {
const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf);
if (memory.len != requested_size) {
@branchHint(.cold);
const tty_config = self.tty_config;
log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{
requested_size,
memory.len,
std.debug.FormatStackTrace{
.stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
});
}
if (alignment != slot_alignment) {
@branchHint(.cold);
const tty_config = self.tty_config;
log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{
slot_alignment.toByteUnits(),
alignment.toByteUnits(),
std.debug.FormatStackTrace{
.stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc),
.tty_config = tty_config,
},
std.debug.FormatStackTrace{
.stack_trace = free_stack_trace,
.tty_config = tty_config,
},
std.debug.FormatStackTrace{ .stack_trace = free_stack_trace },
});
}
}

View file

@ -15,7 +15,7 @@
//!
//! For an example implementation of the `logFn` function, see `defaultLog`,
//! which is the default implementation. It outputs to stderr, using color if
//! the detected `std.Io.tty.Config` supports it. Its output looks like this:
//! supported. Its output looks like this:
//! ```
//! error: this is an error
//! 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 {
return @intFromEnum(level) <= @intFromEnum(std.options.log_level);
}
var static_threaded_io: std.Io.Threaded = .init_single_threaded;
/// The default implementation for the log function. Custom log functions may
/// forward log messages to this function.
///
@ -93,36 +91,64 @@ pub fn defaultLog(
comptime format: []const u8,
args: anytype,
) void {
return defaultLogIo(level, scope, format, args, static_threaded_io.io());
var buffer: [64]u8 = undefined;
const stderr = std.debug.lockStderrWriter(&buffer);
defer std.debug.unlockStderrWriter();
return defaultLogFileWriter(level, scope, format, args, stderr);
}
pub fn defaultLogIo(
pub fn defaultLogFileWriter(
comptime level: Level,
comptime scope: @EnumLiteral(),
comptime format: []const u8,
args: anytype,
io: std.Io,
fw: *std.Io.File.Writer,
) void {
var buffer: [64]u8 = undefined;
const stderr, const ttyconf = io.lockStderrWriter(&buffer);
defer io.unlockStderrWriter();
ttyconf.setColor(stderr, switch (level) {
fw.setColor(switch (level) {
.err => .red,
.warn => .yellow,
.info => .green,
.debug => .magenta,
}) catch {};
ttyconf.setColor(stderr, .bold) catch {};
stderr.writeAll(level.asText()) catch return;
ttyconf.setColor(stderr, .reset) catch {};
ttyconf.setColor(stderr, .dim) catch {};
ttyconf.setColor(stderr, .bold) 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) {
stderr.print("({s})", .{@tagName(scope)}) catch return;
fw.interface.print("({s})", .{@tagName(scope)}) catch return;
}
stderr.writeAll(": ") catch return;
ttyconf.setColor(stderr, .reset) catch {};
stderr.print(format ++ "\n", args) catch return;
fw.interface.writeAll(": ") catch return;
fw.setColor(.reset) catch {};
fw.interface.print(format ++ "\n", decorateArgs(args, fw.mode)) catch return;
}
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);
}
fn decorateArgs(args: anytype, file_writer_mode: std.Io.File.Writer.Mode) 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;
}
/// Returns a scoped logging namespace that logs all messages using the scope

View file

@ -439,25 +439,25 @@ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError
}
/// On Windows, `key` must be valid WTF-8.
pub fn hasEnvVarConstant(comptime key: []const u8) bool {
pub inline fn hasEnvVarConstant(comptime key: []const u8) bool {
if (native_os == .windows) {
const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
return getenvW(key_w) != null;
} else if (native_os == .wasi and !builtin.link_libc) {
@compileError("hasEnvVarConstant is not supported for WASI without libc");
return false;
} else {
return posix.getenv(key) != null;
}
}
/// On Windows, `key` must be valid WTF-8.
pub fn hasNonEmptyEnvVarConstant(comptime key: []const u8) bool {
pub inline fn hasNonEmptyEnvVarConstant(comptime key: []const u8) bool {
if (native_os == .windows) {
const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
const value = getenvW(key_w) orelse return false;
return value.len != 0;
} else if (native_os == .wasi and !builtin.link_libc) {
@compileError("hasNonEmptyEnvVarConstant is not supported for WASI without libc");
return false;
} else {
const value = posix.getenv(key) orelse return false;
return value.len != 0;

View file

@ -247,8 +247,6 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
threaded.stack_size = thread_stack_size;
const io = threaded.io();
debug_allocator.tty_config = .detect(io, .stderr());
const cmd = args[1];
const cmd_args = args[2..];
if (mem.eql(u8, cmd, "build-exe")) {