std.Threaded: replace console kernel32 functions with ntdll

This commit is contained in:
Jacob Young 2026-02-04 18:12:29 -05:00
parent f150759953
commit c77e7146f5
9 changed files with 477 additions and 322 deletions

View file

@ -335,11 +335,9 @@ pub const Operation = union(enum) {
.wasi => noreturn,
.windows => struct {
file: File,
IoControlCode: std.os.windows.CTL_CODE,
InputBuffer: ?*const anyopaque,
InputBufferLength: u32,
OutputBuffer: ?*anyopaque,
OutputBufferLength: u32,
code: std.os.windows.CTL_CODE,
in: []const u8 = &.{},
out: []u8 = &.{},
pub const Result = std.os.windows.IO_STATUS_BLOCK;
},

View file

@ -40,7 +40,8 @@ pub const Mode = union(enum) {
windows_api: WindowsApi,
pub const WindowsApi = if (!is_windows) noreturn else struct {
handle: File.Handle,
io: Io,
file: File,
reset_attributes: u16,
};
@ -65,20 +66,21 @@ pub const Mode = union(enum) {
}
if (is_windows and try 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 .{ .windows_api = .{
.handle = file.handle,
.reset_attributes = info.wAttributes,
} };
var get_console_info = std.os.windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
switch (try get_console_info.operate(io, file)) {
.SUCCESS => return .{ .windows_api = .{
.io = io,
.file = file,
.reset_attributes = get_console_info.Data.wAttributes,
} },
else => {},
}
}
return if (force_color == true) .escape_codes else .no_color;
}
};
pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error;
pub const SetColorError = Io.Cancelable || Io.UnexpectedError || Io.Writer.Error;
pub fn setColor(t: Terminal, color: Color) SetColorError!void {
switch (t.mode) {
@ -132,7 +134,11 @@ pub fn setColor(t: Terminal, color: Color) SetColorError!void {
.reset => wa.reset_attributes,
};
try t.writer.flush();
try windows.SetConsoleTextAttribute(wa.handle, attributes);
var set_text_attribute = windows.CONSOLE.USER_IO.SET_TEXT_ATTRIBUTE(attributes);
switch (try set_text_attribute.operate(wa.io, wa.file)) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
},
}
}

View file

@ -3083,19 +3083,23 @@ fn batchAwaitWindows(b: *Io.Batch, concurrency: bool) error{ Canceled, Concurren
}
},
.device_io_control => |o| {
const NtControlFile = switch (o.code.DeviceType) {
.FILE_SYSTEM, .NAMED_PIPE => &windows.ntdll.NtFsControlFile,
else => &windows.ntdll.NtDeviceIoControlFile,
};
if (o.file.flags.nonblocking) {
context.file = o.file.handle;
switch (windows.ntdll.NtDeviceIoControlFile(
switch (NtControlFile(
o.file.handle,
null, // event
&batchApc,
b,
&context.iosb,
o.IoControlCode,
o.InputBuffer,
o.InputBufferLength,
o.OutputBuffer,
o.OutputBufferLength,
o.code,
if (o.in.len > 0) o.in.ptr else null,
@intCast(o.in.len),
if (o.out.len > 0) o.out.ptr else null,
@intCast(o.out.len),
)) {
.PENDING, .SUCCESS => {},
.CANCELLED => unreachable,
@ -3108,17 +3112,17 @@ fn batchAwaitWindows(b: *Io.Batch, concurrency: bool) error{ Canceled, Concurren
if (concurrency) return error.ConcurrencyUnavailable;
const syscall: Syscall = try .start();
while (true) switch (windows.ntdll.NtDeviceIoControlFile(
while (true) switch (NtControlFile(
o.file.handle,
null, // event
null, // APC routine
null, // APC context
&context.iosb,
o.IoControlCode,
o.InputBuffer,
o.InputBufferLength,
o.OutputBuffer,
o.OutputBufferLength,
o.code,
if (o.in.len > 0) o.in.ptr else null,
@intCast(o.in.len),
if (o.out.len > 0) o.out.ptr else null,
@intCast(o.out.len),
)) {
.PENDING => unreachable, // unrecoverable: wrong File nonblocking flag
.CANCELLED => {
@ -8547,29 +8551,24 @@ fn fileSyncWasi(userdata: ?*anyopaque, file: File) File.SyncError!void {
fn fileIsTty(userdata: ?*anyopaque, file: File) Io.Cancelable!bool {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
return isTty(file);
return t.isTty(file);
}
fn isTty(file: File) Io.Cancelable!bool {
fn isTty(t: *Threaded, file: File) Io.Cancelable!bool {
if (is_windows) {
if (try isCygwinPty(file)) return true;
var out: windows.DWORD = undefined;
const syscall: Syscall = try .start();
while (windows.kernel32.GetConsoleMode(file.handle, &out) == 0) {
switch (windows.GetLastError()) {
.OPERATION_ABORTED => {
try syscall.checkCancel();
continue;
},
else => {
syscall.finish();
return false;
},
}
var get_console_mode = windows.CONSOLE.USER_IO.GET_MODE;
switch ((try t.deviceIoControl(&.{
.file = .{
.handle = windows.peb().ProcessParameters.ConsoleHandle,
.flags = .{ .nonblocking = false },
},
.code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
.in = @ptrCast(&get_console_mode.request(file, 0, .{}, 0, .{})),
})).u.Status) {
.SUCCESS => return true,
.INVALID_HANDLE => return isCygwinPty(file),
else => return false,
}
syscall.finish();
return true;
}
if (builtin.link_libc) {
@ -8637,35 +8636,26 @@ fn isTty(file: File) Io.Cancelable!bool {
fn fileEnableAnsiEscapeCodes(userdata: ?*anyopaque, file: File) File.EnableAnsiEscapeCodesError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
if (!is_windows) {
if (try supportsAnsiEscapeCodes(file)) return;
return error.NotTerminalDevice;
}
if (!is_windows) return if (!try t.supportsAnsiEscapeCodes(file)) error.NotTerminalDevice;
// For Windows Terminal, VT Sequences processing is enabled by default.
var original_console_mode: windows.DWORD = 0;
{
const syscall: Syscall = try .start();
while (windows.kernel32.GetConsoleMode(file.handle, &original_console_mode) == 0) {
switch (windows.GetLastError()) {
.OPERATION_ABORTED => {
try syscall.checkCancel();
continue;
},
else => {
syscall.finish();
if (try isCygwinPty(file)) return;
return error.NotTerminalDevice;
},
}
}
syscall.finish();
const console: File = .{
.handle = windows.peb().ProcessParameters.ConsoleHandle,
.flags = .{ .nonblocking = false },
};
var get_console_mode = windows.CONSOLE.USER_IO.GET_MODE;
switch ((try t.deviceIoControl(&.{
.file = console,
.code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
.in = @ptrCast(&get_console_mode.request(file, 0, .{}, 0, .{})),
})).u.Status) {
.SUCCESS => {},
.INVALID_HANDLE => return if (!try isCygwinPty(file)) error.NotTerminalDevice,
else => return error.NotTerminalDevice,
}
if (original_console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return;
if (get_console_mode.Data & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return;
// For Windows Console, VT Sequences processing support was added in Windows 10 build 14361, but disabled by default.
// https://devblogs.microsoft.com/commandline/tmux-support-arrives-for-bash-on-ubuntu-on-windows/
@ -8678,58 +8668,40 @@ fn fileEnableAnsiEscapeCodes(userdata: ?*anyopaque, file: File) File.EnableAnsiE
// Additionally, the default console mode in Windows Terminal does not have
// `DISABLE_NEWLINE_AUTO_RETURN` set, so by only enabling `ENABLE_VIRTUAL_TERMINAL_PROCESSING`
// we end up matching the mode of Windows Terminal.
const requested_console_modes = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
const console_mode = original_console_mode | requested_console_modes;
{
const syscall: Syscall = try .start();
while (windows.kernel32.SetConsoleMode(file.handle, console_mode) == 0) {
switch (windows.GetLastError()) {
.OPERATION_ABORTED => {
try syscall.checkCancel();
continue;
},
else => {
syscall.finish();
if (try isCygwinPty(file)) return;
return error.NotTerminalDevice;
},
}
}
syscall.finish();
var set_console_mode = windows.CONSOLE.USER_IO.SET_MODE(
get_console_mode.Data | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING,
);
switch ((try t.deviceIoControl(&.{
.file = console,
.code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
.in = @ptrCast(&set_console_mode.request(file, 0, .{}, 0, .{})),
})).u.Status) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
}
fn fileSupportsAnsiEscapeCodes(userdata: ?*anyopaque, file: File) Io.Cancelable!bool {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
return supportsAnsiEscapeCodes(file);
return t.supportsAnsiEscapeCodes(file);
}
fn supportsAnsiEscapeCodes(file: File) Io.Cancelable!bool {
fn supportsAnsiEscapeCodes(t: *Threaded, file: File) Io.Cancelable!bool {
if (is_windows) {
var console_mode: windows.DWORD = 0;
const syscall: Syscall = try .start();
while (windows.kernel32.GetConsoleMode(file.handle, &console_mode) == 0) {
switch (windows.GetLastError()) {
.OPERATION_ABORTED => {
try syscall.checkCancel();
continue;
},
else => {
syscall.finish();
break;
},
}
} else {
syscall.finish();
if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) {
return true;
}
var get_console_mode = windows.CONSOLE.USER_IO.GET_MODE;
switch ((try t.deviceIoControl(&.{
.file = .{
.handle = windows.peb().ProcessParameters.ConsoleHandle,
.flags = .{ .nonblocking = false },
},
.code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
.in = @ptrCast(&get_console_mode.request(file, 0, .{}, 0, .{})),
})).u.Status) {
.SUCCESS => if (get_console_mode.Data & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0)
return true,
.INVALID_HANDLE => return isCygwinPty(file),
else => return false,
}
return isCygwinPty(file);
}
if (native_os == .wasi) {
@ -8739,7 +8711,7 @@ fn supportsAnsiEscapeCodes(file: File) Io.Cancelable!bool {
return false;
}
if (try isTty(file)) return true;
if (try t.isTty(file)) return true;
return false;
}
@ -14111,12 +14083,14 @@ fn initLockedStderr(t: *Threaded, terminal_mode: ?Io.Terminal.Mode) Io.Cancelabl
fn unlockStderr(userdata: ?*anyopaque) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
t.stderr_writer.interface.flush() catch |err| switch (err) {
error.WriteFailed => switch (t.stderr_writer.err.?) {
if (t.stderr_writer.err == null) t.stderr_writer.interface.flush() catch {};
if (t.stderr_writer.err) |err| {
switch (err) {
error.Canceled => recancelInner(),
else => {},
},
};
}
t.stderr_writer.err = null;
}
t.stderr_writer.interface.end = 0;
t.stderr_writer.interface.buffer = &.{};
@ -18848,20 +18822,24 @@ fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError!
fn deviceIoControl(t: *Threaded, o: *const Io.Operation.DeviceIoControl) Io.Cancelable!Io.Operation.DeviceIoControl.Result {
_ = t;
if (is_windows) {
const NtControlFile = switch (o.code.DeviceType) {
.FILE_SYSTEM, .NAMED_PIPE => &windows.ntdll.NtFsControlFile,
else => &windows.ntdll.NtDeviceIoControlFile,
};
var iosb: windows.IO_STATUS_BLOCK = undefined;
if (o.file.flags.nonblocking) {
var done: bool = false;
switch (windows.ntdll.NtDeviceIoControlFile(
switch (NtControlFile(
o.file.handle,
null, // event
flagApc,
&done, // APC context
&iosb,
o.IoControlCode,
o.InputBuffer,
o.InputBufferLength,
o.OutputBuffer,
o.OutputBufferLength,
o.code,
if (o.in.len > 0) o.in.ptr else null,
@intCast(o.in.len),
if (o.out.len > 0) o.out.ptr else null,
@intCast(o.out.len),
)) {
// We must wait for the APC routine.
.PENDING, .SUCCESS => while (!done) {
@ -18882,17 +18860,17 @@ fn deviceIoControl(t: *Threaded, o: *const Io.Operation.DeviceIoControl) Io.Canc
}
} else {
const syscall: Syscall = try .start();
while (true) switch (windows.ntdll.NtDeviceIoControlFile(
while (true) switch (NtControlFile(
o.file.handle,
null, // event
null, // APC routine
null, // APC context
&iosb,
o.IoControlCode,
o.InputBuffer,
o.InputBufferLength,
o.OutputBuffer,
o.OutputBufferLength,
o.code,
if (o.in.len > 0) o.in.ptr else null,
@intCast(o.in.len),
if (o.out.len > 0) o.out.ptr else null,
@intCast(o.out.len),
)) {
.PENDING => unreachable, // unrecoverable: wrong asynchronous flag
.CANCELLED => {

View file

@ -157,7 +157,7 @@ pub const TerminalMode = union(enum) {
ansi_escape_codes,
/// This is not the same as being run on windows because other terminals
/// exist like MSYS/git-bash.
windows_api: if (is_windows) WindowsApi else void,
windows_api: if (is_windows) WindowsApi else noreturn,
pub const WindowsApi = struct {
/// The output code page of the console.
@ -614,33 +614,39 @@ pub fn start(io: Io, options: Options) Node {
if (stderr.enableAnsiEscapeCodes(io)) |_| {
global_progress.terminal_mode = .ansi_escape_codes;
} else |_| if (is_windows) {
if (stderr.isTty(io)) |is_tty| {
if (is_tty) global_progress.terminal_mode = TerminalMode{ .windows_api = .{
.code_page = windows.kernel32.GetConsoleOutputCP(),
} };
} else |err| switch (err) {
var get_console_cp = windows.CONSOLE.USER_IO.GET_CP(.Output);
// Normally, we would pass `null` to `operate` here as the kernel32
// function does not accept a handle, however, if we pass one anyway,
// then we will get an error if the handle is not associated with
// this process's console, effectively combining an `isTty` check
// into the same syscall.
switch (get_console_cp.operate(io, stderr) catch |err| switch (err) {
error.Canceled => {
io.recancel();
return .none;
},
}) {
.SUCCESS => global_progress.terminal_mode = .{ .windows_api = .{
.code_page = get_console_cp.Data.CodePage,
} },
.INVALID_HANDLE => {},
else => {},
}
}
if (global_progress.terminal_mode == .off) return .none;
if (have_sigwinch) {
const act: posix.Sigaction = .{
.handler = .{ .sigaction = handleSigWinch },
.mask = posix.sigemptyset(),
.flags = (posix.SA.SIGINFO | posix.SA.RESTART),
};
posix.sigaction(.WINCH, &act, null);
}
if (switch (global_progress.terminal_mode) {
.off => unreachable, // handled a few lines above
.ansi_escape_codes => io.concurrent(updateTask, .{io}),
.windows_api => if (is_windows) io.concurrent(windowsApiUpdateTask, .{io}) else unreachable,
if (future: switch (global_progress.terminal_mode) {
.off => return .none,
.ansi_escape_codes => {
if (have_sigwinch) {
const act: posix.Sigaction = .{
.handler = .{ .sigaction = handleSigWinch },
.mask = posix.sigemptyset(),
.flags = (posix.SA.SIGINFO | posix.SA.RESTART),
};
posix.sigaction(.WINCH, &act, null);
}
break :future io.concurrent(updateTask, .{io});
},
.windows_api => io.concurrent(windowsApiUpdateTask, .{io}),
}) |future| {
global_progress.update_worker = future;
} else |err| {
@ -715,12 +721,24 @@ fn updateTask(io: Io) WorkerError!void {
}
}
fn windowsApiWriteMarker() void {
const WindowsApiError = Io.Cancelable || Io.UnexpectedError;
fn windowsApiWriteMarker(io: Io) WindowsApiError!void {
// Write the marker that we will use to find the beginning of the progress when clearing.
// Note: This doesn't have to use WriteConsoleW, but doing so avoids dealing with the code page.
var num_chars_written: windows.DWORD = undefined;
const handle = global_progress.terminal.handle;
_ = windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null);
const terminal = global_progress.terminal;
var write_console = windows.CONSOLE.USER_IO.WRITE(.WideCharacter);
const buffer = [1]windows.WCHAR{windows_api_start_marker};
switch ((try io.operate(.{ .device_io_control = .{
.file = terminal,
.code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
.in = @ptrCast(&write_console.request(null, 1, .{
.{ .Size = @sizeOf(@TypeOf(buffer)), .Pointer = &buffer },
}, 0, .{})),
} })).device_io_control.u.Status) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
}
fn windowsApiUpdateTask(io: Io) WorkerError!void {
@ -743,19 +761,19 @@ fn windowsApiUpdateTask(io: Io) WorkerError!void {
error.Canceled => unreachable, // blocked
};
defer io.unlockStderr();
clearWrittenWindowsApi() catch {};
clearWrittenWindowsApi(io) catch {};
}
while (true) {
const buffer, const nl_n = try computeRedraw(io, &serialized_buffer);
if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| {
defer io.unlockStderr();
try clearWrittenWindowsApi();
windowsApiWriteMarker();
try clearWrittenWindowsApi(io);
try windowsApiWriteMarker(io);
global_progress.need_clear = true;
locked_stderr.file_writer.interface.writeAll(buffer) catch |err| switch (err) {
error.WriteFailed => return locked_stderr.file_writer.err.?,
};
windowsApiMoveToMarker(nl_n) catch return;
windowsApiMoveToMarker(io, nl_n) catch return;
}
try maybeUpdateSize(io, try wait(io, global_progress.refresh_rate_ns));
@ -859,7 +877,7 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
return start_i + bytes.len;
},
.windows_api => |windows_api| {
const bytes = if (!is_windows) unreachable else switch (windows_api.code_page) {
const bytes = switch (windows_api.code_page) {
// Code page 437 is the default code page and contains the box drawing symbols
437 => symbol.bytes(.code_page_437),
// UTF-8
@ -882,7 +900,7 @@ pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) Io.Writer.Error
/// U+25BA or
const windows_api_start_marker = 0x25BA;
fn clearWrittenWindowsApi() error{Unexpected}!void {
fn clearWrittenWindowsApi(io: Io) WindowsApiError!void {
// This uses a 'marker' strategy. The idea is:
// - Always write a marker (in this case U+25BA or ) at the beginning of the progress
// - Get the current cursor position (at the end of the progress)
@ -903,43 +921,60 @@ fn clearWrittenWindowsApi() error{Unexpected}!void {
// character in order to be readable via ReadConsoleOutputAttribute. It doesn't seem
// like any of the available attributes are invisible/benign.
if (!global_progress.need_clear) return;
const handle = global_progress.terminal.handle;
const terminal = global_progress.terminal;
const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
return error.Unexpected;
var get_console_info = windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
switch (try get_console_info.operate(io, terminal)) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
var num_chars_written: windows.DWORD = undefined;
if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, console_info.dwCursorPosition, &num_chars_written) == 0) {
return error.Unexpected;
var fill_spaces = windows.CONSOLE.USER_IO.FILL(
.{ .WideCharacter = ' ' },
screen_area,
get_console_info.Data.dwCursorPosition,
);
switch (try fill_spaces.operate(io, terminal)) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
}
fn windowsApiMoveToMarker(nl_n: usize) error{Unexpected}!void {
const handle = global_progress.terminal.handle;
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
return error.Unexpected;
fn windowsApiMoveToMarker(io: Io, nl_n: usize) WindowsApiError!void {
const terminal = global_progress.terminal;
var get_console_info = windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
switch (try get_console_info.operate(io, terminal)) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
const cursor_pos = console_info.dwCursorPosition;
const cursor_pos = get_console_info.Data.dwCursorPosition;
const expected_y = cursor_pos.Y - @as(i16, @intCast(nl_n));
var start_pos: windows.COORD = .{ .X = 0, .Y = expected_y };
while (start_pos.Y >= 0) {
var wchar: [1]u16 = undefined;
var num_console_chars_read: windows.DWORD = undefined;
if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) {
return error.Unexpected;
while (start_pos.Y >= 0) : (start_pos.Y -= 1) {
var read_output_char = windows.CONSOLE.USER_IO.READ_OUTPUT_CHARACTER(start_pos, .WideCharacter);
var buffer: [1]windows.WCHAR = undefined;
switch ((try io.operate(.{ .device_io_control = .{
.file = .{
.handle = windows.peb().ProcessParameters.ConsoleHandle,
.flags = .{ .nonblocking = false },
},
.code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
.in = @ptrCast(&read_output_char.request(terminal, 0, .{}, 1, .{
.{ .Size = @sizeOf(@TypeOf(buffer)), .Pointer = &buffer },
})),
} })).device_io_control.u.Status) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
if (wchar[0] == windows_api_start_marker) break;
start_pos.Y -= 1;
if (read_output_char.Data.nLength >= 1 and buffer[0] == windows_api_start_marker) break;
} else {
// If we couldn't find the marker, then just assume that no lines wrapped
start_pos = .{ .X = 0, .Y = expected_y };
}
if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) {
return error.Unexpected;
var set_cursor_position = windows.CONSOLE.USER_IO.SET_CURSOR_POSITION(start_pos);
switch (try set_cursor_position.operate(io, terminal)) {
.SUCCESS => {},
else => |status| return windows.unexpectedStatus(status),
}
}
@ -1279,7 +1314,7 @@ fn computeRedraw(io: Io, serialized_buffer: *Serialized.Buffer) !struct { []u8,
buf[i..][0..clear.len].* = clear.*;
i += clear.len;
},
.windows_api => if (!is_windows) unreachable,
.windows_api => {},
}
const root_node_index: Node.Index = @enumFromInt(0);
@ -1491,19 +1526,17 @@ fn maybeUpdateSize(io: Io, resize_flag: bool) !void {
const file = global_progress.terminal;
if (is_windows) {
var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.FALSE) {
// In the old Windows console, dwSize.Y is the line count of the
// entire scrollback buffer, so we use this instead so that we
// always get the size of the screen.
const screen_height = info.srWindow.Bottom - info.srWindow.Top;
global_progress.rows = @intCast(screen_height);
global_progress.cols = @intCast(info.dwSize.X);
} else {
std.log.debug("failed to determine terminal size; using conservative guess 80x25", .{});
global_progress.rows = 25;
global_progress.cols = 80;
var get_console_info = windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
switch (try get_console_info.operate(io, file)) {
.SUCCESS => {
global_progress.rows = @intCast(get_console_info.Data.dwWindowSize.Y);
global_progress.cols = @intCast(get_console_info.Data.dwWindowSize.X);
},
else => {
std.log.debug("failed to determine terminal size; using conservative guess 80x25", .{});
global_progress.rows = 25;
global_progress.cols = 80;
},
}
} else {
var winsize: posix.winsize = .{

View file

@ -80,7 +80,7 @@ pub fn logEnabled(comptime level: Level, comptime scope: @EnumLiteral()) bool {
return @intFromEnum(level) <= @intFromEnum(std.options.log_level);
}
pub const terminalMode = std.options.logTerminalMode;
pub const terminalMode = std.Options.logTerminalMode;
pub fn defaultTerminalMode() std.Io.Terminal.Mode {
const stderr = std.debug.lockStderr(&.{}).terminal();
@ -99,6 +99,9 @@ pub fn defaultLog(
comptime format: []const u8,
args: anytype,
) void {
const io = std.Options.debug_io;
const prev = io.swapCancelProtection(.blocked);
defer _ = io.swapCancelProtection(prev);
var buffer: [64]u8 = undefined;
const stderr = std.debug.lockStderr(&buffer).terminal();
defer std.debug.unlockStderr();

View file

@ -649,6 +649,235 @@ pub const FILE = struct {
};
};
pub const CONSOLE = struct {
pub const USER_IO = struct {
pub const INFO = struct {
pub const CP = extern struct {
/// GetCP: output
/// SetCP: input
CodePage: UINT,
/// input
Mode: MODE,
pub const MODE = enum(BOOLEAN) {
Input = FALSE,
Output = TRUE,
};
};
pub const WRITE = extern struct {
/// output, in bytes
Size: DWORD,
/// input
Mode: MODE,
pub const MODE = enum(BOOLEAN) {
Character = FALSE,
WideCharacter = TRUE,
};
};
pub const FILL = extern struct {
/// input
dwWriteCoord: COORD,
/// input
Tag: WITH.Tag,
/// input
With: WITH.Payload,
/// input/output, in characters
nLength: DWORD,
pub const WITH = union(enum(DWORD)) {
Character: CHAR = 1,
WideCharacter: WCHAR = 2,
Attribute: WORD = 3,
pub const Tag = @typeInfo(WITH).@"union".tag_type.?;
pub const Payload = PAYLOAD: {
const with_fields = @typeInfo(WITH).@"union".fields;
var field_names: [with_fields.len][]const u8 = undefined;
var field_types: [with_fields.len]type = undefined;
for (with_fields, &field_names, &field_types) |field, *field_name, *field_type| {
field_name.* = field.name;
field_type.* = field.type;
}
break :PAYLOAD @Union(.@"extern", null, &field_names, &field_types, &@splat(.{}));
};
};
};
/// all output
pub const SCREEN_BUFFER = extern struct {
dwSize: COORD,
dwCursorPosition: COORD,
dwWindowPosition: COORD,
wAttributes: WORD,
dwWindowSize: COORD,
dwMaximumWindowSize: COORD,
wPopupAttributes: WORD,
bFullscreenSupported: BOOL,
ColorTable: [16]COLORREF,
};
pub const READ_OUTPUT_CHARACTER = extern struct {
/// input
dwReadCoord: COORD,
Mode: MODE,
/// output, in characters
nLength: DWORD,
pub const MODE = enum(DWORD) {
Character = 1,
WideCharacter = 2,
};
};
};
pub fn GET_CP(mode: INFO.CP.MODE) Header.With(INFO.CP) {
return .init(.GetCP, .{ .CodePage = undefined, .Mode = mode });
}
pub const GET_MODE: Header.With(DWORD) = .init(.GetMode, undefined);
pub fn SET_MODE(mode: DWORD) Header.With(DWORD) {
return .init(.SetMode, mode);
}
pub fn WRITE(mode: INFO.WRITE.MODE) Header.With(INFO.WRITE) {
return .init(.Write, .{ .Size = undefined, .Mode = mode });
}
pub fn FILL(with: INFO.FILL.WITH, len: DWORD, coord: COORD) Header.With(INFO.FILL) {
return .init(.Fill, .{
.dwWriteCoord = coord,
.Tag = with,
.With = switch (with) {
inline else => |payload, tag| @unionInit(
INFO.FILL.WITH.Payload,
@tagName(tag),
payload,
),
},
.nLength = len,
});
}
pub fn SET_CP(mode: INFO.CP.MODE, cp: UINT) Header.With(INFO.CP) {
return .init(.SetCP, .{ .CodePage = cp, .Mode = mode });
}
pub const GET_SCREEN_BUFFER_INFO: Header.With(INFO.SCREEN_BUFFER) =
.init(.GetScreenBufferInfo, undefined);
pub fn SET_CURSOR_POSITION(coord: COORD) Header.With(COORD) {
return .init(.SetCursorPosition, coord);
}
pub fn SET_TEXT_ATTRIBUTE(attribute: WORD) Header.With(WORD) {
return .init(.SetTextAttribute, attribute);
}
pub fn READ_OUTPUT_CHARACTER(
coord: COORD,
mode: INFO.READ_OUTPUT_CHARACTER.MODE,
) Header.With(INFO.READ_OUTPUT_CHARACTER) {
return .init(.ReadOutputCharacter, .{
.dwReadCoord = coord,
.Mode = mode,
.nLength = undefined,
});
}
pub const InputBuffer = extern struct {
Size: u32,
Pointer: *const anyopaque,
};
pub const OutputBuffer = extern struct {
Size: u32,
Pointer: *anyopaque,
};
pub fn Request(comptime in_len: u32, comptime out_len: u32) type {
return extern struct {
Handle: ?HANDLE,
InputBuffersLength: u32,
OutputBuffersLength: u32,
InputBuffers: [in_len]InputBuffer,
OutputBuffers: [out_len]OutputBuffer,
pub fn init(
handle: ?HANDLE,
in: [in_len]InputBuffer,
out: [out_len]OutputBuffer,
) @This() {
return .{
.Handle = handle,
.InputBuffersLength = in_len,
.OutputBuffersLength = out_len,
.InputBuffers = in,
.OutputBuffers = out,
};
}
};
}
pub const Header = extern struct {
Operation: Operation,
Size: u32,
pub fn With(comptime Data: type) type {
return extern struct {
Header: Header,
Data: Data,
pub fn init(operation: Operation, data: Data) @This() {
return .{
.Header = .{ .Operation = operation, .Size = @sizeOf(Data) },
.Data = data,
};
}
pub fn request(
with: *@This(),
file: ?Io.File,
comptime in_len: u32,
in: [in_len]InputBuffer,
comptime out_len: u32,
out: [out_len]OutputBuffer,
) Request(1 + in_len, 1 + out_len) {
return .init(
if (file) |f| f.handle else null,
[1]InputBuffer{.{
.Size = @offsetOf(@This(), "Data") + @sizeOf(Data),
.Pointer = with,
}} ++ in,
[1]OutputBuffer{.{ .Size = @sizeOf(Data), .Pointer = &with.Data }} ++ out,
);
}
pub fn operate(with: *@This(), io: Io, file: ?Io.File) Io.Cancelable!NTSTATUS {
return (try io.operate(.{ .device_io_control = .{
.file = .{
.handle = peb().ProcessParameters.ConsoleHandle,
.flags = .{ .nonblocking = false },
},
.code = IOCTL.CONDRV.ISSUE_USER_IO,
.in = @ptrCast(&with.request(file, 0, .{}, 0, .{})),
} })).device_io_control.u.Status;
}
};
}
};
pub const Operation = enum(u32) {
GetCP = 0x1000000,
GetMode = 0x1000001,
SetMode = 0x1000002,
Read = 0x1000005,
Write = 0x1000006,
Fill = 0x2000000,
SetCP = 0x2000004,
GetScreenBufferInfo = 0x2000007,
SetCursorPosition = 0x200000a,
SetTextAttribute = 0x200000d,
ReadOutputCharacter = 0x200000f,
_,
};
};
};
// ref: km/ntddk.h
pub const PROCESSINFOCLASS = enum(c_int) {
@ -1160,6 +1389,22 @@ pub const CTL_CODE = packed struct(ULONG) {
};
pub const IOCTL = struct {
pub const CONDRV = struct {
pub const READ_IO: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 1, .Method = .OUT_DIRECT, .Access = .ANY };
pub const COMPLETE_IO: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 2, .Method = .NEITHER, .Access = .ANY };
pub const READ_INPUT: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 3, .Method = .NEITHER, .Access = .ANY };
pub const WRITE_OUTPUT: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 4, .Method = .NEITHER, .Access = .ANY };
pub const ISSUE_USER_IO: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 5, .Method = .OUT_DIRECT, .Access = .ANY };
pub const DISCONNECT_PIPE: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 6, .Method = .NEITHER, .Access = .ANY };
pub const SET_SERVER_INFORMATION: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 7, .Method = .NEITHER, .Access = .ANY };
pub const GET_SERVER_PID: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 8, .Method = .NEITHER, .Access = .ANY };
pub const GET_DISPLAY_SIZE: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 9, .Method = .NEITHER, .Access = .ANY };
pub const UPDATE_DISPLAY: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 10, .Method = .NEITHER, .Access = .ANY };
pub const SET_CURSOR: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 11, .Method = .NEITHER, .Access = .ANY };
pub const ALLOW_VIA_UIACCESS: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 12, .Method = .NEITHER, .Access = .ANY };
pub const LAUNCH_SERVER: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 13, .Method = .NEITHER, .Access = .ANY };
pub const GET_FONT_SIZE: CTL_CODE = .{ .DeviceType = .CONSOLE, .Function = 14, .Method = .NEITHER, .Access = .ANY };
};
pub const KSEC = struct {
pub const GEN_RANDOM: CTL_CODE = .{ .DeviceType = .KSEC, .Function = 2, .Method = .BUFFERED, .Access = .ANY };
};
@ -2663,29 +2908,6 @@ pub fn NtFreeVirtualMemory(hProcess: HANDLE, addr: ?*PVOID, size: *SIZE_T, free_
};
}
pub const SetConsoleTextAttributeError = error{Unexpected};
pub fn SetConsoleTextAttribute(hConsoleOutput: HANDLE, wAttributes: WORD) SetConsoleTextAttributeError!void {
if (kernel32.SetConsoleTextAttribute(hConsoleOutput, wAttributes) == 0) {
switch (GetLastError()) {
else => |err| return unexpectedError(err),
}
}
}
pub fn SetConsoleCtrlHandler(handler_routine: ?HANDLER_ROUTINE, add: bool) !void {
const success = kernel32.SetConsoleCtrlHandler(
handler_routine,
if (add) TRUE else FALSE,
);
if (success == FALSE) {
return switch (GetLastError()) {
else => |err| unexpectedError(err),
};
}
}
pub fn SetFileCompletionNotificationModes(handle: HANDLE, flags: UCHAR) !void {
const success = kernel32.SetFileCompletionNotificationModes(handle, flags);
if (success == FALSE) {
@ -3244,6 +3466,7 @@ pub const ULONGLONG = u64;
pub const LONGLONG = i64;
pub const HLOCAL = HANDLE;
pub const LANGID = c_ushort;
pub const COLORREF = DWORD;
pub const WPARAM = usize;
pub const LPARAM = LONG_PTR;
@ -3784,21 +4007,17 @@ pub const FileNotifyChangeFilter = packed struct(DWORD) {
_pad: u20 = 0,
};
pub const CONSOLE_SCREEN_BUFFER_INFO = extern struct {
dwSize: COORD,
dwCursorPosition: COORD,
wAttributes: WORD,
srWindow: SMALL_RECT,
dwMaximumWindowSize: COORD,
};
pub const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4;
pub const DISABLE_NEWLINE_AUTO_RETURN = 0x8;
pub const FOREGROUND_BLUE = 1;
pub const FOREGROUND_GREEN = 2;
pub const FOREGROUND_RED = 4;
pub const FOREGROUND_INTENSITY = 8;
pub const FOREGROUND_BLUE = 0x0001;
pub const FOREGROUND_GREEN = 0x0002;
pub const FOREGROUND_RED = 0x0004;
pub const FOREGROUND_INTENSITY = 0x0008;
pub const BACKGROUND_BLUE = 0x0010;
pub const BACKGROUND_GREEN = 0x0020;
pub const BACKGROUND_RED = 0x0040;
pub const BACKGROUND_INTENSITY = 0x0080;
pub const LIST_ENTRY = extern struct {
Flink: *LIST_ENTRY,

View file

@ -4,7 +4,6 @@ const windows = std.os.windows;
const ACCESS_MASK = windows.ACCESS_MASK;
const BOOL = windows.BOOL;
const CONDITION_VARIABLE = windows.CONDITION_VARIABLE;
const CONSOLE_SCREEN_BUFFER_INFO = windows.CONSOLE_SCREEN_BUFFER_INFO;
const COORD = windows.COORD;
const DWORD = windows.DWORD;
const FARPROC = windows.FARPROC;
@ -191,89 +190,6 @@ pub extern "kernel32" fn CreateThread(
lpThreadId: ?*DWORD,
) callconv(.winapi) ?HANDLE;
// Locks, critical sections, initializers
// TODO:
// - dwMilliseconds -> LARGE_INTEGER.
// - RtlSleepConditionVariableSRW
// - return rc != .TIMEOUT
pub extern "kernel32" fn SleepConditionVariableSRW(
ConditionVariable: *CONDITION_VARIABLE,
SRWLock: *SRWLOCK,
dwMilliseconds: DWORD,
Flags: ULONG,
) callconv(.winapi) BOOL;
// Console management
pub extern "kernel32" fn GetConsoleMode(
hConsoleHandle: HANDLE,
lpMode: *DWORD,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn SetConsoleMode(
hConsoleHandle: HANDLE,
dwMode: DWORD,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn GetConsoleScreenBufferInfo(
hConsoleOutput: HANDLE,
lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn SetConsoleTextAttribute(
hConsoleOutput: HANDLE,
wAttributes: WORD,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn SetConsoleCtrlHandler(
HandlerRoutine: ?HANDLER_ROUTINE,
Add: BOOL,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn SetConsoleOutputCP(
wCodePageID: UINT,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn GetConsoleOutputCP() callconv(.winapi) UINT;
pub extern "kernel32" fn FillConsoleOutputAttribute(
hConsoleOutput: HANDLE,
wAttribute: WORD,
nLength: DWORD,
dwWriteCoord: COORD,
lpNumberOfAttrsWritten: *DWORD,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn FillConsoleOutputCharacterW(
hConsoleOutput: HANDLE,
cCharacter: WCHAR,
nLength: DWORD,
dwWriteCoord: COORD,
lpNumberOfCharsWritten: *DWORD,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn SetConsoleCursorPosition(
hConsoleOutput: HANDLE,
dwCursorPosition: COORD,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn WriteConsoleW(
hConsoleOutput: HANDLE,
lpBuffer: [*]const u16,
nNumberOfCharsToWrite: DWORD,
lpNumberOfCharsWritten: ?*DWORD,
lpReserved: ?LPVOID,
) callconv(.winapi) BOOL;
pub extern "kernel32" fn ReadConsoleOutputCharacterW(
hConsoleOutput: HANDLE,
lpCharacter: [*]u16,
nLength: DWORD,
dwReadCoord: COORD,
lpNumberOfCharsRead: *DWORD,
) callconv(.winapi) BOOL;
// Code Libraries/Modules
// TODO: Wrapper around LdrGetDllFullName.

View file

@ -135,8 +135,6 @@ pub const Options = struct {
args: anytype,
) void = log.defaultLog,
logTerminalMode: fn () Io.Terminal.Mode = log.defaultTerminalMode,
/// Overrides `std.heap.page_size_min`.
page_size_min: ?usize = null,
/// Overrides `std.heap.page_size_max`.
@ -176,6 +174,10 @@ pub const Options = struct {
/// stack traces will just print an error to the relevant `Io.Writer` and return.
allow_stack_tracing: bool = !@import("builtin").strip_debug_info,
/// TODO This is a separate decl instead of a field as a workaround around
/// compilation errors due to zig not being lazy enough.
pub const logTerminalMode: fn () Io.Terminal.Mode = log.defaultTerminalMode;
/// TODO This is a separate decl instead of a field as a workaround around
/// compilation errors due to zig not being lazy enough.
pub const elf_debug_info_search_paths: ?fn (exe_path: []const u8) switch (@import("builtin").object_format) {

View file

@ -347,7 +347,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
if (msg.kind == .@"fatal error" or msg.kind == .@"error") {
msg.write(stderr.terminal(), true) catch |err| switch (err) {
error.WriteFailed => return stderr.file_writer.err.?,
error.Unexpected => |e| return e,
error.Canceled, error.Unexpected => |e| return e,
};
return error.AroPreprocessorFailed;
}