| ... | ... | @@ -13,16 +13,18 @@ const assert = std.debug.assert; |
| 13 | 13 | const posix = std.posix; |
| 14 | 14 | const Writer = std.Io.Writer; |
| 15 | 15 | |
| 16 | | /// `null` if the current node (and its children) should |
| 17 | | /// not print on update() |
| 16 | /// Currently this API only supports this value being set to stderr, which |
| 17 | /// happens automatically inside `start`. |
| 18 | 18 | terminal: Io.File, |
| 19 | 19 | |
| 20 | io: Io, |
| 21 | |
| 20 | 22 | terminal_mode: TerminalMode, |
| 21 | 23 | |
| 22 | | update_thread: ?std.Thread, |
| 24 | update_worker: ?Io.Future(void), |
| 23 | 25 | |
| 24 | 26 | /// Atomically set by SIGWINCH as well as the root done() function. |
| 25 | | redraw_event: std.Thread.ResetEvent, |
| 27 | redraw_event: Io.ResetEvent, |
| 26 | 28 | /// Indicates a request to shut down and reset global state. |
| 27 | 29 | /// Accessed atomically. |
| 28 | 30 | done: bool, |
| ... | ... | @@ -95,9 +97,9 @@ pub const Options = struct { |
| 95 | 97 | /// Must be at least 200 bytes. |
| 96 | 98 | draw_buffer: []u8 = &default_draw_buffer, |
| 97 | 99 | /// How many nanoseconds between writing updates to the terminal. |
| 98 | | refresh_rate_ns: u64 = 80 * std.time.ns_per_ms, |
| 100 | refresh_rate_ns: Io.Duration = .fromMilliseconds(80), |
| 99 | 101 | /// How many nanoseconds to keep the output hidden |
| 100 | | initial_delay_ns: u64 = 200 * std.time.ns_per_ms, |
| 102 | initial_delay_ns: Io.Duration = .fromMilliseconds(200), |
| 101 | 103 | /// If provided, causes the progress item to have a denominator. |
| 102 | 104 | /// 0 means unknown. |
| 103 | 105 | estimated_total_items: usize = 0, |
| ... | ... | @@ -330,7 +332,7 @@ pub const Node = struct { |
| 330 | 332 | } else { |
| 331 | 333 | @atomicStore(bool, &global_progress.done, true, .monotonic); |
| 332 | 334 | global_progress.redraw_event.set(); |
| 333 | | if (global_progress.update_thread) |thread| thread.join(); |
| 335 | if (global_progress.update_worker) |worker| worker.await(global_progress.io); |
| 334 | 336 | } |
| 335 | 337 | } |
| 336 | 338 | |
| ... | ... | @@ -391,9 +393,10 @@ pub const Node = struct { |
| 391 | 393 | }; |
| 392 | 394 | |
| 393 | 395 | var global_progress: Progress = .{ |
| 396 | .io = undefined, |
| 394 | 397 | .terminal = undefined, |
| 395 | 398 | .terminal_mode = .off, |
| 396 | | .update_thread = null, |
| 399 | .update_worker = null, |
| 397 | 400 | .redraw_event = .unset, |
| 398 | 401 | .refresh_rate_ns = undefined, |
| 399 | 402 | .initial_delay_ns = undefined, |
| ... | ... | @@ -403,6 +406,7 @@ var global_progress: Progress = .{ |
| 403 | 406 | .done = false, |
| 404 | 407 | .need_clear = false, |
| 405 | 408 | .status = .working, |
| 409 | .start_failure = .unstarted, |
| 406 | 410 | |
| 407 | 411 | .node_parents = &node_parents_buffer, |
| 408 | 412 | .node_storage = &node_storage_buffer, |
| ... | ... | @@ -411,6 +415,13 @@ var global_progress: Progress = .{ |
| 411 | 415 | .node_end_index = 0, |
| 412 | 416 | }; |
| 413 | 417 | |
| 418 | pub const StartFailure = union(enum) { |
| 419 | unstarted, |
| 420 | spawn_ipc_worker: error{ConcurrencyUnavailable}, |
| 421 | spawn_update_worker: error{ConcurrencyUnavailable}, |
| 422 | parse_env_var: error{}, |
| 423 | }; |
| 424 | |
| 414 | 425 | const node_storage_buffer_len = 83; |
| 415 | 426 | var node_parents_buffer: [node_storage_buffer_len]Node.Parent = undefined; |
| 416 | 427 | var node_storage_buffer: [node_storage_buffer_len]Node.Storage = undefined; |
| ... | ... | @@ -437,7 +448,7 @@ const noop_impl = builtin.single_threaded or switch (builtin.os.tag) { |
| 437 | 448 | /// Asserts there is only one global Progress instance. |
| 438 | 449 | /// |
| 439 | 450 | /// Call `Node.end` when done. |
| 440 | | pub fn start(options: Options) Node { |
| 451 | pub fn start(options: Options, io: Io) Node { |
| 441 | 452 | // Ensure there is only 1 global Progress object. |
| 442 | 453 | if (global_progress.node_end_index != 0) { |
| 443 | 454 | debug_start_trace.dump(); |
| ... | ... | @@ -458,10 +469,10 @@ pub fn start(options: Options) Node { |
| 458 | 469 | if (noop_impl) |
| 459 | 470 | return Node.none; |
| 460 | 471 | |
| 461 | | const io = static_threaded_io.io(); |
| 472 | global_progress.io = io; |
| 462 | 473 | |
| 463 | 474 | if (std.process.parseEnvVarInt("ZIG_PROGRESS", u31, 10)) |ipc_fd| { |
| 464 | | global_progress.update_thread = std.Thread.spawn(.{}, ipcThreadRun, .{ |
| 475 | global_progress.update_worker = io.concurrent(ipcThreadRun, .{ |
| 465 | 476 | io, |
| 466 | 477 | @as(Io.File, .{ .handle = switch (@typeInfo(posix.fd_t)) { |
| 467 | 478 | .int => ipc_fd, |
| ... | ... | @@ -469,7 +480,7 @@ pub fn start(options: Options) Node { |
| 469 | 480 | else => @compileError("unsupported fd_t of " ++ @typeName(posix.fd_t)), |
| 470 | 481 | } }), |
| 471 | 482 | }) catch |err| { |
| 472 | | std.log.warn("failed to spawn IPC thread for communicating progress to parent: {s}", .{@errorName(err)}); |
| 483 | global_progress.start_failure = .{ .spawn_ipc_worker = err }; |
| 473 | 484 | return Node.none; |
| 474 | 485 | }; |
| 475 | 486 | } else |env_err| switch (env_err) { |
| ... | ... | @@ -502,17 +513,17 @@ pub fn start(options: Options) Node { |
| 502 | 513 | |
| 503 | 514 | if (switch (global_progress.terminal_mode) { |
| 504 | 515 | .off => unreachable, // handled a few lines above |
| 505 | | .ansi_escape_codes => std.Thread.spawn(.{}, updateThreadRun, .{io}), |
| 506 | | .windows_api => if (is_windows) std.Thread.spawn(.{}, windowsApiUpdateThreadRun, .{io}) else unreachable, |
| 507 | | }) |thread| { |
| 508 | | global_progress.update_thread = thread; |
| 516 | .ansi_escape_codes => io.concurrent(updateThreadRun, .{io}), |
| 517 | .windows_api => if (is_windows) io.concurrent(windowsApiUpdateThreadRun, .{io}) else unreachable, |
| 518 | }) |future| { |
| 519 | global_progress.update_worker = future; |
| 509 | 520 | } else |err| { |
| 510 | | std.log.warn("unable to spawn thread for printing progress to terminal: {s}", .{@errorName(err)}); |
| 521 | global_progress.start_failure = .{ .spawn_update_worker = err }; |
| 511 | 522 | return Node.none; |
| 512 | 523 | } |
| 513 | 524 | }, |
| 514 | 525 | else => |e| { |
| 515 | | std.log.warn("invalid ZIG_PROGRESS file descriptor integer: {s}", .{@errorName(e)}); |
| 526 | global_progress.start_failure = .{ .parse_env_var = e }; |
| 516 | 527 | return Node.none; |
| 517 | 528 | }, |
| 518 | 529 | } |
| ... | ... | @@ -545,10 +556,10 @@ fn updateThreadRun(io: Io) void { |
| 545 | 556 | maybeUpdateSize(resize_flag); |
| 546 | 557 | |
| 547 | 558 | const buffer, _ = computeRedraw(&serialized_buffer); |
| 548 | | if (stderr_mutex.tryLock()) { |
| 549 | | defer stderr_mutex.unlock(); |
| 550 | | write(io, buffer) catch return; |
| 559 | if (io.tryLockStderrWriter(&.{})) |w| { |
| 560 | defer io.unlockStderrWriter(); |
| 551 | 561 | global_progress.need_clear = true; |
| 562 | w.writeAll(buffer) catch return; |
| 552 | 563 | } |
| 553 | 564 | } |
| 554 | 565 | |
| ... | ... | @@ -556,18 +567,18 @@ fn updateThreadRun(io: Io) void { |
| 556 | 567 | const resize_flag = wait(global_progress.refresh_rate_ns); |
| 557 | 568 | |
| 558 | 569 | if (@atomicLoad(bool, &global_progress.done, .monotonic)) { |
| 559 | | stderr_mutex.lock(); |
| 560 | | defer stderr_mutex.unlock(); |
| 561 | | return clearWrittenWithEscapeCodes(io) catch {}; |
| 570 | const w = io.lockStderrWriter(&.{}) catch return; |
| 571 | defer io.unlockStderrWriter(); |
| 572 | return clearWrittenWithEscapeCodes(w) catch {}; |
| 562 | 573 | } |
| 563 | 574 | |
| 564 | 575 | maybeUpdateSize(resize_flag); |
| 565 | 576 | |
| 566 | 577 | const buffer, _ = computeRedraw(&serialized_buffer); |
| 567 | | if (stderr_mutex.tryLock()) { |
| 568 | | defer stderr_mutex.unlock(); |
| 569 | | write(io, buffer) catch return; |
| 578 | if (io.tryLockStderrWriter(&.{})) |w| { |
| 579 | defer io.unlockStderrWriter(); |
| 570 | 580 | global_progress.need_clear = true; |
| 581 | w.writeAll(buffer) catch return; |
| 571 | 582 | } |
| 572 | 583 | } |
| 573 | 584 | } |
| ... | ... | @@ -589,11 +600,11 @@ fn windowsApiUpdateThreadRun(io: Io) void { |
| 589 | 600 | maybeUpdateSize(resize_flag); |
| 590 | 601 | |
| 591 | 602 | const buffer, const nl_n = computeRedraw(&serialized_buffer); |
| 592 | | if (stderr_mutex.tryLock()) { |
| 593 | | defer stderr_mutex.unlock(); |
| 603 | if (io.tryLockStderrWriter()) |w| { |
| 604 | defer io.unlockStderrWriter(); |
| 594 | 605 | windowsApiWriteMarker(); |
| 595 | | write(io, buffer) catch return; |
| 596 | 606 | global_progress.need_clear = true; |
| 607 | w.writeAll(buffer) catch return; |
| 597 | 608 | windowsApiMoveToMarker(nl_n) catch return; |
| 598 | 609 | } |
| 599 | 610 | } |
| ... | ... | @@ -602,74 +613,25 @@ fn windowsApiUpdateThreadRun(io: Io) void { |
| 602 | 613 | const resize_flag = wait(global_progress.refresh_rate_ns); |
| 603 | 614 | |
| 604 | 615 | if (@atomicLoad(bool, &global_progress.done, .monotonic)) { |
| 605 | | stderr_mutex.lock(); |
| 606 | | defer stderr_mutex.unlock(); |
| 616 | _ = io.lockStderrWriter() catch return; |
| 617 | defer io.unlockStderrWriter(); |
| 607 | 618 | return clearWrittenWindowsApi() catch {}; |
| 608 | 619 | } |
| 609 | 620 | |
| 610 | 621 | maybeUpdateSize(resize_flag); |
| 611 | 622 | |
| 612 | 623 | const buffer, const nl_n = computeRedraw(&serialized_buffer); |
| 613 | | if (stderr_mutex.tryLock()) { |
| 614 | | defer stderr_mutex.unlock(); |
| 624 | if (io.tryLockStderrWriter()) |w| { |
| 625 | defer io.unlockStderrWriter(); |
| 615 | 626 | clearWrittenWindowsApi() catch return; |
| 616 | 627 | windowsApiWriteMarker(); |
| 617 | | write(io, buffer) catch return; |
| 618 | 628 | global_progress.need_clear = true; |
| 629 | w.writeAll(buffer) catch return; |
| 619 | 630 | windowsApiMoveToMarker(nl_n) catch return; |
| 620 | 631 | } |
| 621 | 632 | } |
| 622 | 633 | } |
| 623 | 634 | |
| 624 | | /// Allows the caller to freely write to stderr until `unlockStdErr` is called. |
| 625 | | /// |
| 626 | | /// During the lock, any `std.Progress` information is cleared from the terminal. |
| 627 | | /// |
| 628 | | /// The lock is recursive; the same thread may hold the lock multiple times. |
| 629 | | pub fn lockStdErr() void { |
| 630 | | const io = stderr_file_writer.io; |
| 631 | | stderr_mutex.lock(); |
| 632 | | clearWrittenWithEscapeCodes(io) catch {}; |
| 633 | | } |
| 634 | | |
| 635 | | pub fn unlockStdErr() void { |
| 636 | | stderr_mutex.unlock(); |
| 637 | | } |
| 638 | | |
| 639 | | /// Protected by `stderr_mutex`. |
| 640 | | const stderr_writer: *Writer = &stderr_file_writer.interface; |
| 641 | | /// Protected by `stderr_mutex`. |
| 642 | | var stderr_file_writer: Io.File.Writer = .{ |
| 643 | | .io = static_threaded_io.io(), |
| 644 | | .interface = Io.File.Writer.initInterface(&.{}), |
| 645 | | .file = if (is_windows) undefined else .stderr(), |
| 646 | | .mode = .streaming, |
| 647 | | }; |
| 648 | | var static_threaded_io: Io.Threaded = .init_single_threaded; |
| 649 | | |
| 650 | | /// Allows the caller to freely write to the returned `Writer`, |
| 651 | | /// initialized with `buffer`, until `unlockStderrWriter` is called. |
| 652 | | /// |
| 653 | | /// During the lock, any `std.Progress` information is cleared from the terminal. |
| 654 | | /// |
| 655 | | /// The lock is recursive; the same thread may hold the lock multiple times. |
| 656 | | pub fn lockStderrWriter(buffer: []u8) *Io.Writer { |
| 657 | | const io = stderr_file_writer.io; |
| 658 | | stderr_mutex.lock(); |
| 659 | | clearWrittenWithEscapeCodes(io) catch {}; |
| 660 | | if (is_windows) stderr_file_writer.file = .stderr(); |
| 661 | | stderr_writer.flush() catch {}; |
| 662 | | stderr_writer.buffer = buffer; |
| 663 | | return stderr_writer; |
| 664 | | } |
| 665 | | |
| 666 | | pub fn unlockStderrWriter() void { |
| 667 | | stderr_writer.flush() catch {}; |
| 668 | | stderr_writer.end = 0; |
| 669 | | stderr_writer.buffer = &.{}; |
| 670 | | stderr_mutex.unlock(); |
| 671 | | } |
| 672 | | |
| 673 | 635 | fn ipcThreadRun(io: Io, file: Io.File) anyerror!void { |
| 674 | 636 | // Store this data in the thread so that it does not need to be part of the |
| 675 | 637 | // linker data of the main executable. |
| ... | ... | @@ -793,11 +755,11 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize { |
| 793 | 755 | } |
| 794 | 756 | } |
| 795 | 757 | |
| 796 | | fn clearWrittenWithEscapeCodes(io: Io) anyerror!void { |
| 758 | fn clearWrittenWithEscapeCodes(w: *Io.Writer) anyerror!void { |
| 797 | 759 | if (noop_impl or !global_progress.need_clear) return; |
| 798 | 760 | |
| 761 | try w.writeAll(clear ++ progress_remove); |
| 799 | 762 | global_progress.need_clear = false; |
| 800 | | try write(io, clear ++ progress_remove); |
| 801 | 763 | } |
| 802 | 764 | |
| 803 | 765 | /// U+25BA or ► |
| ... | ... | @@ -997,7 +959,7 @@ fn serializeIpc(start_serialized_len: usize, serialized_buffer: *Serialized.Buff |
| 997 | 959 | const n = posix.read(fd, pipe_buf[bytes_read..]) catch |err| switch (err) { |
| 998 | 960 | error.WouldBlock => break, |
| 999 | 961 | else => |e| { |
| 1000 | | std.log.debug("failed to read child progress data: {s}", .{@errorName(e)}); |
| 962 | std.log.debug("failed to read child progress data: {t}", .{e}); |
| 1001 | 963 | main_storage.completed_count = 0; |
| 1002 | 964 | main_storage.estimated_total_count = 0; |
| 1003 | 965 | continue :main_loop; |
| ... | ... | @@ -1424,10 +1386,6 @@ fn withinRowLimit(p: *Progress, nl_n: usize) bool { |
| 1424 | 1386 | return nl_n + 2 < p.rows; |
| 1425 | 1387 | } |
| 1426 | 1388 | |
| 1427 | | fn write(io: Io, buf: []const u8) anyerror!void { |
| 1428 | | try global_progress.terminal.writeStreamingAll(io, buf); |
| 1429 | | } |
| 1430 | | |
| 1431 | 1389 | var remaining_write_trash_bytes: usize = 0; |
| 1432 | 1390 | |
| 1433 | 1391 | fn writeIpc(io: Io, file: Io.File, serialized: Serialized) error{BrokenPipe}!void { |
| ... | ... | @@ -1459,7 +1417,7 @@ fn writeIpc(io: Io, file: Io.File, serialized: Serialized) error{BrokenPipe}!voi |
| 1459 | 1417 | error.WouldBlock => return, |
| 1460 | 1418 | error.BrokenPipe => return error.BrokenPipe, |
| 1461 | 1419 | else => |e| { |
| 1462 | | std.log.debug("failed to send progress to parent process: {s}", .{@errorName(e)}); |
| 1420 | std.log.debug("failed to send progress to parent process: {t}", .{e}); |
| 1463 | 1421 | return error.BrokenPipe; |
| 1464 | 1422 | }, |
| 1465 | 1423 | } |
| ... | ... | @@ -1476,7 +1434,7 @@ fn writeIpc(io: Io, file: Io.File, serialized: Serialized) error{BrokenPipe}!voi |
| 1476 | 1434 | error.WouldBlock => {}, |
| 1477 | 1435 | error.BrokenPipe => return error.BrokenPipe, |
| 1478 | 1436 | else => |e| { |
| 1479 | | std.log.debug("failed to send progress to parent process: {s}", .{@errorName(e)}); |
| 1437 | std.log.debug("failed to send progress to parent process: {t}", .{e}); |
| 1480 | 1438 | return error.BrokenPipe; |
| 1481 | 1439 | }, |
| 1482 | 1440 | } |
| ... | ... | @@ -1568,11 +1526,6 @@ const have_sigwinch = switch (builtin.os.tag) { |
| 1568 | 1526 | else => false, |
| 1569 | 1527 | }; |
| 1570 | 1528 | |
| 1571 | | /// The primary motivation for recursive mutex here is so that a panic while |
| 1572 | | /// stderr mutex is held still dumps the stack trace and other debug |
| 1573 | | /// information. |
| 1574 | | var stderr_mutex = std.Thread.Mutex.Recursive.init; |
| 1575 | | |
| 1576 | 1529 | fn copyAtomicStore(dest: []align(@alignOf(usize)) u8, src: []const u8) void { |
| 1577 | 1530 | assert(dest.len == src.len); |
| 1578 | 1531 | const chunked_len = dest.len / @sizeOf(usize); |