authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-28 12:31:10-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-28 12:31:10-07:00
log3a3d2187f986066859cfb793fb7ee1cae4dfea08
tree950c39722d71cdd6f2af75c255ee92a318cda516
parent40afac40b8d9f274d63448a11f9f4259a1f68528

std.Progress: better Windows support

* Merge a bunch of related state together into TerminalMode. Windows sometimes follows the same path as posix via ansi_escape_codes, sometimes not. * Use a different thread entry point for Windows API but share the same entry point on Windows when the terminal is in ansi_escape_codes mode. * Only clear the terminal when the stderr lock is held. * Don't try to clear the terminal when nothing has been written yet. * Don't try to clear the terminal in IPC mode. * Fix size detection logic bug under error conditions.

1 files changed, 115 insertions(+), 101 deletions(-)

lib/std/Progress.zig+115-101
...@@ -8,19 +8,13 @@ const assert = std.debug.assert;...@@ -8,19 +8,13 @@ const assert = std.debug.assert;
8const Progress = @This();8const Progress = @This();
9const posix = std.posix;9const posix = std.posix;
10const is_big_endian = builtin.cpu.arch.endian() == .big;10const is_big_endian = builtin.cpu.arch.endian() == .big;
11const is_windows = builtin.os.tag == .windows;
1112
12/// `null` if the current node (and its children) should13/// `null` if the current node (and its children) should
13/// not print on update()14/// not print on update()
14terminal: ?std.fs.File,15terminal: std.fs.File,
1516
16/// Is this a windows API terminal (note: this is not the same as being run on windows17terminal_mode: TerminalMode,
17/// because other terminals exist like MSYS/git-bash)
18is_windows_terminal: bool,
19/// The output code page of the console (only set if the console is a Windows API terminal)
20console_code_page: if (builtin.os.tag == .windows) windows.UINT else void,
21
22/// Whether the terminal supports ANSI escape codes.
23supports_ansi_escape_codes: bool,
2418
25update_thread: ?std.Thread,19update_thread: ?std.Thread,
2620
...@@ -53,6 +47,19 @@ node_freelist: []Node.OptionalIndex,...@@ -53,6 +47,19 @@ node_freelist: []Node.OptionalIndex,
53node_freelist_first: Node.OptionalIndex,47node_freelist_first: Node.OptionalIndex,
54node_end_index: u32,48node_end_index: u32,
5549
50pub const TerminalMode = union(enum) {
51 off,
52 ansi_escape_codes,
53 /// This is not the same as being run on windows because other terminals
54 /// exist like MSYS/git-bash.
55 windows_api: if (is_windows) WindowsApi else void,
56
57 pub const WindowsApi = struct {
58 /// The output code page of the console.
59 code_page: windows.UINT,
60 };
61};
62
56pub const Options = struct {63pub const Options = struct {
57 /// User-provided buffer with static lifetime.64 /// User-provided buffer with static lifetime.
58 ///65 ///
...@@ -297,10 +304,8 @@ pub const Node = struct {...@@ -297,10 +304,8 @@ pub const Node = struct {
297};304};
298305
299var global_progress: Progress = .{306var global_progress: Progress = .{
300 .terminal = null,307 .terminal = undefined,
301 .is_windows_terminal = false,308 .terminal_mode = .off,
302 .console_code_page = if (builtin.os.tag == .windows) undefined else {},
303 .supports_ansi_escape_codes = false,
304 .update_thread = null,309 .update_thread = null,
305 .redraw_event = .{},310 .redraw_event = .{},
306 .refresh_rate_ns = undefined,311 .refresh_rate_ns = undefined,
...@@ -376,20 +381,16 @@ pub fn start(options: Options) Node {...@@ -376,20 +381,16 @@ pub fn start(options: Options) Node {
376 return .{ .index = .none };381 return .{ .index = .none };
377 }382 }
378 const stderr = std.io.getStdErr();383 const stderr = std.io.getStdErr();
384 global_progress.terminal = stderr;
379 if (stderr.supportsAnsiEscapeCodes()) {385 if (stderr.supportsAnsiEscapeCodes()) {
380 global_progress.terminal = stderr;386 global_progress.terminal_mode = .ansi_escape_codes;
381 global_progress.supports_ansi_escape_codes = true;387 } else if (is_windows and stderr.isTty()) {
382 } else if (builtin.os.tag == .windows and stderr.isTty()) {388 global_progress.terminal_mode = TerminalMode{ .windows_api = .{
383 global_progress.is_windows_terminal = true;389 .code_page = windows.kernel32.GetConsoleOutputCP(),
384 global_progress.console_code_page = windows.kernel32.GetConsoleOutputCP();390 } };
385 global_progress.terminal = stderr;
386 } else if (builtin.os.tag != .windows) {
387 // we are in a "dumb" terminal like in acme or writing to a file
388 global_progress.terminal = stderr;
389 }391 }
390392
391 const can_clear_terminal = global_progress.supports_ansi_escape_codes or global_progress.is_windows_terminal;393 if (global_progress.terminal_mode == .off) {
392 if (global_progress.terminal == null or !can_clear_terminal) {
393 return .{ .index = .none };394 return .{ .index = .none };
394 }395 }
395396
...@@ -404,7 +405,11 @@ pub fn start(options: Options) Node {...@@ -404,7 +405,11 @@ pub fn start(options: Options) Node {
404 };405 };
405 }406 }
406407
407 if (std.Thread.spawn(.{}, updateThreadRun, .{})) |thread| {408 if (switch (global_progress.terminal_mode) {
409 .off => unreachable, // handled a few lines above
410 .ansi_escape_codes => std.Thread.spawn(.{}, updateThreadRun, .{}),
411 .windows_api => if (is_windows) std.Thread.spawn(.{}, windowsApiUpdateThreadRun, .{}) else unreachable,
412 }) |thread| {
408 global_progress.update_thread = thread;413 global_progress.update_thread = thread;
409 } else |err| {414 } else |err| {
410 std.log.warn("unable to spawn thread for printing progress to terminal: {s}", .{@errorName(err)});415 std.log.warn("unable to spawn thread for printing progress to terminal: {s}", .{@errorName(err)});
...@@ -438,13 +443,42 @@ fn updateThreadRun() void {...@@ -438,13 +443,42 @@ fn updateThreadRun() void {
438443
439 {444 {
440 const resize_flag = wait(global_progress.initial_delay_ns);445 const resize_flag = wait(global_progress.initial_delay_ns);
446 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
441 maybeUpdateSize(resize_flag);447 maybeUpdateSize(resize_flag);
442448
449 const buffer = computeRedraw(&serialized_buffer);
450 if (stderr_mutex.tryLock()) {
451 defer stderr_mutex.unlock();
452 write(buffer) catch return;
453 }
454 }
455
456 while (true) {
457 const resize_flag = wait(global_progress.refresh_rate_ns);
458
443 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) {459 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) {
444 stderr_mutex.lock();460 stderr_mutex.lock();
445 defer stderr_mutex.unlock();461 defer stderr_mutex.unlock();
446 return clearTerminal();462 return clearWrittenWithEscapeCodes() catch {};
463 }
464
465 maybeUpdateSize(resize_flag);
466
467 const buffer = computeRedraw(&serialized_buffer);
468 if (stderr_mutex.tryLock()) {
469 defer stderr_mutex.unlock();
470 write(buffer) catch return;
447 }471 }
472 }
473}
474
475fn windowsApiUpdateThreadRun() void {
476 var serialized_buffer: Serialized.Buffer = undefined;
477
478 {
479 const resize_flag = wait(global_progress.initial_delay_ns);
480 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
481 maybeUpdateSize(resize_flag);
448482
449 const buffer = computeRedraw(&serialized_buffer);483 const buffer = computeRedraw(&serialized_buffer);
450 if (stderr_mutex.tryLock()) {484 if (stderr_mutex.tryLock()) {
...@@ -455,17 +489,19 @@ fn updateThreadRun() void {...@@ -455,17 +489,19 @@ fn updateThreadRun() void {
455489
456 while (true) {490 while (true) {
457 const resize_flag = wait(global_progress.refresh_rate_ns);491 const resize_flag = wait(global_progress.refresh_rate_ns);
458 maybeUpdateSize(resize_flag);
459492
460 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) {493 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) {
461 stderr_mutex.lock();494 stderr_mutex.lock();
462 defer stderr_mutex.unlock();495 defer stderr_mutex.unlock();
463 return clearTerminal();496 return clearWrittenWindowsApi() catch {};
464 }497 }
465498
499 maybeUpdateSize(resize_flag);
500
466 const buffer = computeRedraw(&serialized_buffer);501 const buffer = computeRedraw(&serialized_buffer);
467 if (stderr_mutex.tryLock()) {502 if (stderr_mutex.tryLock()) {
468 defer stderr_mutex.unlock();503 defer stderr_mutex.unlock();
504 clearWrittenWindowsApi() catch return;
469 write(buffer) catch return;505 write(buffer) catch return;
470 }506 }
471 }507 }
...@@ -476,7 +512,7 @@ fn updateThreadRun() void {...@@ -476,7 +512,7 @@ fn updateThreadRun() void {
476/// During the lock, any `std.Progress` information is cleared from the terminal.512/// During the lock, any `std.Progress` information is cleared from the terminal.
477pub fn lockStdErr() void {513pub fn lockStdErr() void {
478 stderr_mutex.lock();514 stderr_mutex.lock();
479 clearTerminal();515 clearWrittenWithEscapeCodes() catch {};
480}516}
481517
482pub fn unlockStdErr() void {518pub fn unlockStdErr() void {
...@@ -504,7 +540,7 @@ fn ipcThreadRun(fd: posix.fd_t) anyerror!void {...@@ -504,7 +540,7 @@ fn ipcThreadRun(fd: posix.fd_t) anyerror!void {
504 _ = wait(global_progress.refresh_rate_ns);540 _ = wait(global_progress.refresh_rate_ns);
505541
506 if (@atomicLoad(bool, &global_progress.done, .seq_cst))542 if (@atomicLoad(bool, &global_progress.done, .seq_cst))
507 return clearTerminal();543 return;
508544
509 const serialized = serialize(&serialized_buffer);545 const serialized = serialize(&serialized_buffer);
510 writeIpc(fd, serialized) catch |err| switch (err) {546 writeIpc(fd, serialized) catch |err| switch (err) {
...@@ -569,41 +605,36 @@ const TreeSymbol = enum {...@@ -569,41 +605,36 @@ const TreeSymbol = enum {
569 var max: usize = 0;605 var max: usize = 0;
570 inline for (@typeInfo(Encoding).Enum.fields) |field| {606 inline for (@typeInfo(Encoding).Enum.fields) |field| {
571 const len = symbol.bytes(@field(Encoding, field.name)).len;607 const len = symbol.bytes(@field(Encoding, field.name)).len;
572 if (len > max) max = len;608 max = @max(max, len);
573 }609 }
574 return max;610 return max;
575 }611 }
576};612};
577613
578fn appendTreeSymbol(comptime symbol: TreeSymbol, buf: []u8, start_i: usize) usize {614fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
579 if (builtin.os.tag == .windows and global_progress.is_windows_terminal) {615 switch (global_progress.terminal_mode) {
580 const bytes = switch (global_progress.console_code_page) {616 .off => unreachable,
581 // Code page 437 is the default code page and contains the box drawing symbols617 .ansi_escape_codes => {
582 437 => symbol.bytes(.code_page_437),618 const bytes = symbol.escapeSeq();
583 // UTF-8619 buf[start_i..][0..bytes.len].* = bytes.*;
584 65001 => symbol.bytes(.utf8),620 return start_i + bytes.len;
585 // Fall back to ASCII approximation621 },
586 else => symbol.bytes(.ascii),622 .windows_api => |windows_api| {
587 };623 const bytes = if (!is_windows) unreachable else switch (windows_api.code_page) {
588 @memcpy(buf[start_i..][0..bytes.len], bytes);624 // Code page 437 is the default code page and contains the box drawing symbols
589 return start_i + bytes.len;625 437 => symbol.bytes(.code_page_437),
626 // UTF-8
627 65001 => symbol.bytes(.utf8),
628 // Fall back to ASCII approximation
629 else => symbol.bytes(.ascii),
630 };
631 @memcpy(buf[start_i..][0..bytes.len], bytes);
632 return start_i + bytes.len;
633 },
590 }634 }
591
592 // Drawing the tree is disabled when ansi escape codes are not supported
593 assert(global_progress.supports_ansi_escape_codes);
594
595 const bytes = symbol.escapeSeq();
596 buf[start_i..][0..bytes.len].* = bytes.*;
597 return start_i + bytes.len;
598}635}
599636
600fn clearTerminal() void {637fn clearWrittenWithEscapeCodes() anyerror!void {
601 if (builtin.os.tag == .windows and global_progress.is_windows_terminal) {
602 return clearTerminalWindowsApi() catch {
603 global_progress.terminal = null;
604 };
605 }
606
607 if (global_progress.written_newline_count == 0) return;638 if (global_progress.written_newline_count == 0) return;
608639
609 var i: usize = 0;640 var i: usize = 0;
...@@ -618,9 +649,7 @@ fn clearTerminal() void {...@@ -618,9 +649,7 @@ fn clearTerminal() void {
618 i += finish_sync.len;649 i += finish_sync.len;
619650
620 global_progress.accumulated_newline_count = 0;651 global_progress.accumulated_newline_count = 0;
621 write(buf[0..i]) catch {652 try write(buf[0..i]);
622 global_progress.terminal = null;
623 };
624}653}
625654
626fn computeClear(buf: []u8, start_i: usize) usize {655fn computeClear(buf: []u8, start_i: usize) usize {
...@@ -645,7 +674,7 @@ fn computeClear(buf: []u8, start_i: usize) usize {...@@ -645,7 +674,7 @@ fn computeClear(buf: []u8, start_i: usize) usize {
645/// U+25BA or ►674/// U+25BA or ►
646const windows_api_start_marker = 0x25BA;675const windows_api_start_marker = 0x25BA;
647676
648fn clearTerminalWindowsApi() error{Unexpected}!void {677fn clearWrittenWindowsApi() error{Unexpected}!void {
649 // This uses a 'marker' strategy. The idea is:678 // This uses a 'marker' strategy. The idea is:
650 // - Always write a marker (in this case U+25BA or ►) at the beginning of the progress679 // - Always write a marker (in this case U+25BA or ►) at the beginning of the progress
651 // - Get the current cursor position (at the end of the progress)680 // - Get the current cursor position (at the end of the progress)
...@@ -667,7 +696,7 @@ fn clearTerminalWindowsApi() error{Unexpected}!void {...@@ -667,7 +696,7 @@ fn clearTerminalWindowsApi() error{Unexpected}!void {
667 // like any of the available attributes are invisible/benign.696 // like any of the available attributes are invisible/benign.
668 const prev_nl_n = global_progress.written_newline_count;697 const prev_nl_n = global_progress.written_newline_count;
669 if (prev_nl_n > 0) {698 if (prev_nl_n > 0) {
670 const handle = (global_progress.terminal orelse return).handle;699 const handle = global_progress.terminal.handle;
671 const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;700 const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
672701
673 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;702 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
...@@ -777,14 +806,14 @@ const SavedMetadata = struct {...@@ -777,14 +806,14 @@ const SavedMetadata = struct {
777 nodes_len: u8,806 nodes_len: u8,
778807
779 fn getIpcFd(metadata: SavedMetadata) posix.fd_t {808 fn getIpcFd(metadata: SavedMetadata) posix.fd_t {
780 return if (builtin.os.tag == .windows)809 return if (is_windows)
781 @ptrFromInt(@as(usize, metadata.ipc_fd) << 2)810 @ptrFromInt(@as(usize, metadata.ipc_fd) << 2)
782 else811 else
783 metadata.ipc_fd;812 metadata.ipc_fd;
784 }813 }
785814
786 fn setIpcFd(fd: posix.fd_t) u16 {815 fn setIpcFd(fd: posix.fd_t) u16 {
787 return @intCast(if (builtin.os.tag == .windows)816 return @intCast(if (is_windows)
788 @shrExact(@intFromPtr(fd), 2)817 @shrExact(@intFromPtr(fd), 2)
789 else818 else
790 fd);819 fd);
...@@ -1019,35 +1048,21 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {...@@ -1019,35 +1048,21 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
1019 var i: usize = 0;1048 var i: usize = 0;
1020 const buf = global_progress.draw_buffer;1049 const buf = global_progress.draw_buffer;
10211050
1022 if (global_progress.supports_ansi_escape_codes) {1051 buf[i..][0..start_sync.len].* = start_sync.*;
1023 buf[i..][0..start_sync.len].* = start_sync.*;1052 i += start_sync.len;
1024 i += start_sync.len;
1025
1026 i = computeClear(buf, i);
1027 } else if (builtin.os.tag == .windows and global_progress.is_windows_terminal) {
1028 clearTerminalWindowsApi() catch {
1029 global_progress.terminal = null;
1030 return buf[0..0];
1031 };
10321053
1033 // Write the marker that we will use to find the beginning of the progress when clearing.1054 switch (global_progress.terminal_mode) {
1034 // Note: This doesn't have to use WriteConsoleW, but doing so avoids dealing with the code page.1055 .off => unreachable,
1035 var num_chars_written: windows.DWORD = undefined;1056 .ansi_escape_codes => i = computeClear(buf, i),
1036 const handle = (global_progress.terminal orelse return buf[0..0]).handle;1057 .windows_api => if (!is_windows) unreachable,
1037 if (windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null) == 0) {
1038 global_progress.terminal = null;
1039 return buf[0..0];
1040 }
1041 }1058 }
10421059
1043 global_progress.accumulated_newline_count = 0;1060 global_progress.accumulated_newline_count = 0;
1044 const root_node_index: Node.Index = @enumFromInt(0);1061 const root_node_index: Node.Index = @enumFromInt(0);
1045 i = computeNode(buf, i, serialized, children, root_node_index);1062 i = computeNode(buf, i, serialized, children, root_node_index);
10461063
1047 if (global_progress.supports_ansi_escape_codes) {1064 buf[i..][0..finish_sync.len].* = finish_sync.*;
1048 buf[i..][0..finish_sync.len].* = finish_sync.*;1065 i += finish_sync.len;
1049 i += finish_sync.len;
1050 }
10511066
1052 return buf[0..i];1067 return buf[0..i];
1053}1068}
...@@ -1075,15 +1090,15 @@ fn computePrefix(...@@ -1075,15 +1090,15 @@ fn computePrefix(
1075 buf[i..][0..prefix.len].* = prefix.*;1090 buf[i..][0..prefix.len].* = prefix.*;
1076 i += prefix.len;1091 i += prefix.len;
1077 } else {1092 } else {
1078 const upper_bound_len = TreeSymbol.line.maxByteLen() + line_upper_bound_len;1093 const upper_bound_len = comptime (TreeSymbol.line.maxByteLen() + line_upper_bound_len);
1079 if (i + upper_bound_len > buf.len) return buf.len;1094 if (i + upper_bound_len > buf.len) return buf.len;
1080 i = appendTreeSymbol(.line, buf, i);1095 i = appendTreeSymbol(.line, buf, i);
1081 }1096 }
1082 return i;1097 return i;
1083}1098}
10841099
1085const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) + "[4294967296/4294967296] ".len +1100const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) +
1086 Node.max_name_len + finish_sync.len;1101 "[4294967296/4294967296] ".len + Node.max_name_len + finish_sync.len;
10871102
1088fn computeNode(1103fn computeNode(
1089 buf: []u8,1104 buf: []u8,
...@@ -1157,8 +1172,7 @@ fn withinRowLimit(p: *Progress) bool {...@@ -1157,8 +1172,7 @@ fn withinRowLimit(p: *Progress) bool {
1157}1172}
11581173
1159fn write(buf: []const u8) anyerror!void {1174fn write(buf: []const u8) anyerror!void {
1160 const tty = global_progress.terminal orelse return;1175 try global_progress.terminal.writeAll(buf);
1161 try tty.writeAll(buf);
1162 global_progress.written_newline_count = global_progress.accumulated_newline_count;1176 global_progress.written_newline_count = global_progress.accumulated_newline_count;
1163}1177}
11641178
...@@ -1218,23 +1232,23 @@ fn writeIpc(fd: posix.fd_t, serialized: Serialized) error{BrokenPipe}!void {...@@ -1218,23 +1232,23 @@ fn writeIpc(fd: posix.fd_t, serialized: Serialized) error{BrokenPipe}!void {
1218fn maybeUpdateSize(resize_flag: bool) void {1232fn maybeUpdateSize(resize_flag: bool) void {
1219 if (!resize_flag) return;1233 if (!resize_flag) return;
12201234
1221 const fd = (global_progress.terminal orelse return).handle;1235 const fd = global_progress.terminal.handle;
12221236
1223 if (builtin.os.tag == .windows) {1237 if (is_windows) {
1224 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;1238 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
12251239
1226 if (windows.kernel32.GetConsoleScreenBufferInfo(fd, &info) == windows.FALSE) {1240 if (windows.kernel32.GetConsoleScreenBufferInfo(fd, &info) != windows.FALSE) {
1241 // In the old Windows console, dwSize.Y is the line count of the
1242 // entire scrollback buffer, so we use this instead so that we
1243 // always get the size of the screen.
1244 const screen_height = info.srWindow.Bottom - info.srWindow.Top;
1245 global_progress.rows = @intCast(screen_height);
1246 global_progress.cols = @intCast(info.dwSize.X);
1247 } else {
1227 std.log.debug("failed to determine terminal size; using conservative guess 80x25", .{});1248 std.log.debug("failed to determine terminal size; using conservative guess 80x25", .{});
1228 global_progress.rows = 25;1249 global_progress.rows = 25;
1229 global_progress.cols = 80;1250 global_progress.cols = 80;
1230 }1251 }
1231
1232 // In the old Windows console, dwSize.Y is the line count of the entire
1233 // scrollback buffer, so we use this instead so that we always get the
1234 // size of the screen.
1235 const screen_height = info.srWindow.Bottom - info.srWindow.Top;
1236 global_progress.rows = @intCast(screen_height);
1237 global_progress.cols = @intCast(info.dwSize.X);
1238 } else {1252 } else {
1239 var winsize: posix.winsize = .{1253 var winsize: posix.winsize = .{
1240 .ws_row = 0,1254 .ws_row = 0,