1//! This API is non-allocating, non-fallible, thread-safe, and lock-free.
2const Progress = @This();
3
4const builtin = @import("builtin");
5const is_big_endian = builtin.cpu.arch.endian() == .big;
6const is_windows = builtin.os.tag == .windows;
7
8const std = @import("std");
9const Io = std.Io;
10const windows = std.os.windows;
11const testing = std.testing;
12const assert = std.debug.assert;
13const posix = std.posix;
14const Writer = Io.Writer;
15
16/// Currently this API only supports this value being set to stderr, which
17/// happens automatically inside `start`.
18terminal: Io.File,
19
20io: Io,
21
22terminal_mode: TerminalMode,
23
24update_worker: ?Io.Future(WorkerError!void),
25
26/// Atomically set by SIGWINCH as well as the root done() function.
27redraw_event: Io.Event,
28need_clear: bool,
29status: Status,
30
31refresh_rate_ns: u64,
32initial_delay_ns: u64,
33
34rows: u16,
35cols: u16,
36
37/// Accessed only by the update thread.
38draw_buffer: []u8,
39
40/// This is in a separate array from `node_storage` but with the same length so
41/// that it can be iterated over efficiently without trashing too much of the
42/// CPU cache.
43node_parents: [node_storage_buffer_len]Node.Parent,
44node_storage: [node_storage_buffer_len]Node.Storage,
45node_freelist_next: [node_storage_buffer_len]Node.OptionalIndex,
46node_freelist: Freelist,
47/// This is the number of elements in node arrays which have been used so far. Nodes before this
48/// index are either active, or on the freelist. The remaining nodes are implicitly free. This
49/// value may at times temporarily exceed the node count.
50node_end_index: u32,
51
52ipc_next: Ipc.SlotAtomic,
53ipc: [ipc_storage_buffer_len]Ipc,
54ipc_files: [ipc_storage_buffer_len]Io.File,
55
56start_failure: StartFailure,
57
58pub const Status = enum {
59 /// Indicates the application is progressing towards completion of a task.
60 /// Unless the application is interactive, this is the only status the
61 /// program will ever have!
62 working,
63 /// The application has completed an operation, and is now waiting for user
64 /// input rather than calling exit(0).
65 success,
66 /// The application encountered an error, and is now waiting for user input
67 /// rather than calling exit(1).
68 failure,
69 /// The application encountered at least one error, but is still working on
70 /// more tasks.
71 failure_working,
72};
73
74const Freelist = packed struct(u32) {
75 head: Node.OptionalIndex,
76 /// Whenever `node_freelist` is added to, this generation is incremented
77 /// to avoid ABA bugs when acquiring nodes. Wrapping arithmetic is used.
78 generation: u24,
79};
80
81pub const Ipc = packed struct(u32) {
82 /// mutex protecting `file` use, only locked by `serializeIpc`
83 locked: bool,
84 /// when unlocked: whether `file` is defined
85 /// when locked: whether `file` does not need to be closed
86 valid: bool,
87 unused: @Int(.unsigned, 32 - 2 - @bitSizeOf(Generation)) = 0,
88 generation: Generation,
89
90 pub const Slot = std.math.IntFittingRange(0, ipc_storage_buffer_len - 1);
91 pub const Generation = @Int(.unsigned, 32 - @bitSizeOf(Slot));
92
93 const SlotAtomic = @Int(.unsigned, std.math.ceilPowerOfTwoAssert(usize, @min(@bitSizeOf(Slot), 8)));
94
95 pub const Index = packed struct(u32) {
96 slot: Slot,
97 generation: Generation,
98 };
99
100 const Data = struct {
101 state: State,
102 bytes_read: u16,
103 main_index: u8,
104 start_index: u8,
105 nodes_len: u8,
106
107 const State = enum { unused, pending, ready };
108
109 /// No operations have been started on this file.
110 const unused: Data = .{
111 .state = .unused,
112 .bytes_read = 0,
113 .main_index = 0,
114 .start_index = 0,
115 .nodes_len = 0,
116 };
117
118 fn findLastPacket(data: *const Data, buffer: *const [max_packet_len]u8) struct { u16, u16 } {
119 assert(data.state == .ready);
120 var packet_start: u16 = 0;
121 var packet_end: u16 = 0;
122 const bytes_read = data.bytes_read;
123 while (bytes_read - packet_end >= 1) {
124 const nodes_len: u16 = buffer[packet_end];
125 const packet_len = 1 + nodes_len * (@sizeOf(Node.Storage) + @sizeOf(Node.Parent));
126 if (packet_end + packet_len > bytes_read) break;
127 packet_start = packet_end;
128 packet_end += packet_len;
129 }
130 return .{ packet_start, packet_end };
131 }
132
133 fn rebase(
134 data: *Data,
135 buffer: *[max_packet_len]u8,
136 vec: *[1][]u8,
137 batch: *std.Io.Batch,
138 slot: Slot,
139 packet_end: u16,
140 ) void {
141 assert(data.state == .ready);
142 const remaining = buffer[packet_end..data.bytes_read];
143 @memmove(buffer[0..remaining.len], remaining);
144 vec.* = .{buffer[remaining.len..]};
145 batch.addAt(slot, .{ .file_read_streaming = .{
146 .file = global_progress.ipc_files[slot],
147 .data = vec,
148 } });
149 data.state = .pending;
150 data.bytes_read = @intCast(remaining.len);
151 }
152 };
153};
154
155pub const TerminalMode = union(enum) {
156 off,
157 ansi_escape_codes,
158 /// This is not the same as being run on windows because other terminals
159 /// exist like MSYS/git-bash.
160 windows_api: if (is_windows) WindowsApi else noreturn,
161
162 pub const WindowsApi = struct {
163 /// The output code page of the console.
164 code_page: windows.UINT,
165 };
166};
167
168/// Represents one unit of progress. Each node can have children nodes, or
169/// one can use integers with `update`.
170pub const Node = struct {
171 index: OptionalIndex,
172
173 pub const none: Node = .{ .index = .none };
174
175 pub const max_name_len = 120;
176
177 const Storage = extern struct {
178 /// Little endian.
179 completed_count: u32,
180 /// 0 means unknown.
181 /// Little endian.
182 estimated_total_count: u32,
183 name: [max_name_len]u8 align(@alignOf(usize)),
184
185 /// Not thread-safe.
186 fn getIpcIndex(s: Storage) ?Ipc.Index {
187 return if (s.estimated_total_count == std.math.maxInt(u32)) @bitCast(s.completed_count) else null;
188 }
189
190 /// Thread-safe.
191 fn setIpcIndex(s: *Storage, ipc_index: Ipc.Index) void {
192 // `estimated_total_count` max int indicates the special state that
193 // causes `completed_count` to be treated as a file descriptor, so
194 // the order here matters.
195 @atomicStore(u32, &s.completed_count, @bitCast(ipc_index), .monotonic);
196 @atomicStore(u32, &s.estimated_total_count, std.math.maxInt(u32), .release); // synchronizes with acquire in `serialize`
197 }
198
199 /// Not thread-safe.
200 fn byteSwap(s: *Storage) void {
201 s.completed_count = @byteSwap(s.completed_count);
202 s.estimated_total_count = @byteSwap(s.estimated_total_count);
203 }
204
205 fn copyRoot(dest: *Node.Storage, src: *align(1) const Node.Storage) void {
206 dest.* = .{
207 .completed_count = src.completed_count,
208 .estimated_total_count = src.estimated_total_count,
209 .name = if (src.name[0] == 0) dest.name else src.name,
210 };
211 }
212
213 comptime {
214 assert((@sizeOf(Storage) % 4) == 0);
215 }
216 };
217
218 const Parent = enum(u8) {
219 /// Unallocated storage.
220 unused = std.math.maxInt(u8) - 1,
221 /// Indicates root node.
222 none = std.math.maxInt(u8),
223 /// Index into `node_storage`.
224 _,
225
226 fn unwrap(i: @This()) ?Index {
227 return switch (i) {
228 .unused, .none => return null,
229 else => @fromBackingInt(@intCast(@backingInt(i))),
230 };
231 }
232 };
233
234 pub const OptionalIndex = enum(u8) {
235 none = std.math.maxInt(u8),
236 /// Index into `node_storage`.
237 _,
238
239 pub fn unwrap(i: @This()) ?Index {
240 if (i == .none) return null;
241 return @fromBackingInt(@intCast(@backingInt(i)));
242 }
243
244 fn toParent(i: @This()) Parent {
245 assert(@backingInt(i) != @backingInt(Parent.unused));
246 return @fromBackingInt(@intCast(@backingInt(i)));
247 }
248 };
249
250 /// Index into `node_storage`.
251 pub const Index = enum(u8) {
252 _,
253
254 fn toParent(i: @This()) Parent {
255 assert(@backingInt(i) != @backingInt(Parent.unused));
256 assert(@backingInt(i) != @backingInt(Parent.none));
257 return @fromBackingInt(@intCast(@backingInt(i)));
258 }
259
260 pub fn toOptional(i: @This()) OptionalIndex {
261 return @fromBackingInt(@intCast(@backingInt(i)));
262 }
263 };
264
265 /// Create a new child progress node. Thread-safe.
266 ///
267 /// Passing 0 for `estimated_total_items` means unknown.
268 pub fn start(node: Node, name: []const u8, estimated_total_items: usize) Node {
269 if (noop_impl) {
270 assert(node.index == .none);
271 return Node.none;
272 }
273 const node_index = node.index.unwrap() orelse return Node.none;
274 const parent = node_index.toParent();
275
276 const freelist = &global_progress.node_freelist;
277 var old_freelist = @atomicLoad(Freelist, freelist, .acquire); // acquire to ensure we have the correct "next" entry
278 while (old_freelist.head.unwrap()) |free_index| {
279 const next_ptr = freelistNextByIndex(free_index);
280 const new_freelist: Freelist = .{
281 .head = @atomicLoad(Node.OptionalIndex, next_ptr, .monotonic),
282 // We don't need to increment the generation when removing nodes from the free list,
283 // only when adding them. (This choice is arbitrary; the opposite would also work.)
284 .generation = old_freelist.generation,
285 };
286 old_freelist = @cmpxchgWeak(
287 Freelist,
288 freelist,
289 old_freelist,
290 new_freelist,
291 .acquire, // not theoretically necessary, but not allowed to be weaker than the failure order
292 .acquire, // ensure we have the correct `node_freelist_next` entry on the next iteration
293 ) orelse {
294 // We won the allocation race.
295 return init(free_index, parent, name, estimated_total_items);
296 };
297 }
298
299 const free_index = @atomicRmw(u32, &global_progress.node_end_index, .Add, 1, .monotonic);
300 if (free_index >= node_storage_buffer_len) {
301 // Ran out of node storage memory. Progress for this node will not be tracked.
302 _ = @atomicRmw(u32, &global_progress.node_end_index, .Sub, 1, .monotonic);
303 return Node.none;
304 }
305
306 return init(@fromBackingInt(@intCast(free_index)), parent, name, estimated_total_items);
307 }
308
309 pub fn startFmt(node: Node, estimated_total_items: usize, comptime format: []const u8, args: anytype) Node {
310 var buffer: [max_name_len]u8 = undefined;
311 const name = std.mem.print(&buffer, format, args) catch &buffer;
312 return Node.start(node, name, estimated_total_items);
313 }
314
315 /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe.
316 pub fn completeOne(n: Node) void {
317 const index = n.index.unwrap() orelse return;
318 const storage = storageByIndex(index);
319 _ = @atomicRmw(u32, &storage.completed_count, .Add, 1, .monotonic);
320 }
321
322 /// Thread-safe. Bytes after '0' in `new_name` are ignored.
323 pub fn setName(n: Node, new_name: []const u8) void {
324 const index = n.index.unwrap() orelse return;
325 const storage = storageByIndex(index);
326
327 const name_len = @min(max_name_len, std.mem.findScalar(u8, new_name, 0) orelse new_name.len);
328
329 copyAtomicStore(storage.name[0..name_len], new_name[0..name_len]);
330 if (name_len < storage.name.len)
331 @atomicStore(u8, &storage.name[name_len], 0, .monotonic);
332 }
333
334 /// Gets the name of this `Node`.
335 /// A pointer to this array can later be passed to `setName` to restore the name.
336 pub fn getName(n: Node) [max_name_len]u8 {
337 var dest: [max_name_len]u8 align(@alignOf(usize)) = undefined;
338 if (n.index.unwrap()) |index| {
339 copyAtomicLoad(&dest, &storageByIndex(index).name);
340 }
341 return dest;
342 }
343
344 /// Thread-safe.
345 pub fn setCompletedItems(n: Node, completed_items: usize) void {
346 const index = n.index.unwrap() orelse return;
347 const storage = storageByIndex(index);
348 @atomicStore(u32, &storage.completed_count, std.math.lossyCast(u32, completed_items), .monotonic);
349 }
350
351 /// Thread-safe. 0 means unknown.
352 pub fn setEstimatedTotalItems(n: Node, count: usize) void {
353 const index = n.index.unwrap() orelse return;
354 const storage = storageByIndex(index);
355 // Avoid u32 max int which is used to indicate a special state.
356 const saturated_total_count = @min(std.math.maxInt(u32) - 1, count);
357 @atomicStore(u32, &storage.estimated_total_count, saturated_total_count, .monotonic);
358 }
359
360 /// Thread-safe.
361 pub fn increaseEstimatedTotalItems(n: Node, count: usize) void {
362 const index = n.index.unwrap() orelse return;
363 const storage = storageByIndex(index);
364 // Avoid u32 max int which is used to indicate a special state.
365 const saturated_total_count = @min(std.math.maxInt(u32) - 1, count);
366 _ = @atomicRmw(u32, &storage.estimated_total_count, .Add, saturated_total_count, .monotonic);
367 }
368
369 /// Finish a started `Node`. Thread-safe.
370 pub fn end(n: Node) void {
371 if (noop_impl) {
372 assert(n.index == .none);
373 return;
374 }
375 const index = n.index.unwrap() orelse return;
376 const io = global_progress.io;
377 const parent_ptr = parentByIndex(index);
378 if (@atomicLoad(Node.Parent, parent_ptr, .monotonic).unwrap()) |parent_index| {
379 _ = @atomicRmw(u32, &storageByIndex(parent_index).completed_count, .Add, 1, .monotonic);
380 @atomicStore(Node.Parent, parent_ptr, .unused, .monotonic);
381
382 if (storageByIndex(index).getIpcIndex()) |ipc_index| {
383 const file = global_progress.ipc_files[ipc_index.slot];
384 const ipc = @atomicRmw(
385 Ipc,
386 &global_progress.ipc[ipc_index.slot],
387 .And,
388 .{ .locked = true, .valid = false, .generation = std.math.maxInt(Ipc.Generation) },
389 .release,
390 );
391 assert(ipc.valid and ipc.generation == ipc_index.generation);
392 if (!ipc.locked) file.close(io);
393 }
394
395 const freelist = &global_progress.node_freelist;
396 var old_freelist = @atomicLoad(Freelist, freelist, .monotonic);
397 while (true) {
398 @atomicStore(Node.OptionalIndex, freelistNextByIndex(index), old_freelist.head, .monotonic);
399 old_freelist = @cmpxchgWeak(
400 Freelist,
401 freelist,
402 old_freelist,
403 .{ .head = index.toOptional(), .generation = old_freelist.generation +% 1 },
404 .release, // ensure a matching `start` sees the freelist link written above
405 .monotonic, // our write above is irrelevant if we need to retry
406 ) orelse {
407 // We won the race.
408 return;
409 };
410 }
411 } else {
412 if (global_progress.update_worker) |*worker| worker.cancel(io) catch {};
413 for (&global_progress.ipc, &global_progress.ipc_files) |ipc, ipc_file| {
414 assert(!ipc.locked or !ipc.valid); // missing call to end()
415 if (ipc.locked or ipc.valid) ipc_file.close(io);
416 }
417 }
418 }
419
420 /// Used by `std.process.Child`. Thread-safe.
421 pub fn setIpcFile(node: Node, expected_io_userdata: ?*anyopaque, file: Io.File) void {
422 const index = node.index.unwrap() orelse return;
423 const io = global_progress.io;
424 assert(io.userdata == expected_io_userdata);
425 for (0..ipc_storage_buffer_len) |_| {
426 const slot: Ipc.Slot = @truncate(
427 @atomicRmw(Ipc.SlotAtomic, &global_progress.ipc_next, .Add, 1, .monotonic),
428 );
429 if (slot >= ipc_storage_buffer_len) continue;
430 const ipc_ptr = &global_progress.ipc[slot];
431 const ipc = @atomicLoad(Ipc, ipc_ptr, .monotonic);
432 if (ipc.locked or ipc.valid) continue;
433 const generation = ipc.generation +% 1;
434 if (@cmpxchgWeak(
435 Ipc,
436 ipc_ptr,
437 ipc,
438 .{ .locked = false, .valid = true, .generation = generation },
439 .acquire,
440 .monotonic,
441 )) |_| continue;
442 global_progress.ipc_files[slot] = file;
443 storageByIndex(index).setIpcIndex(.{ .slot = slot, .generation = generation });
444 break;
445 } else {
446 // There was no IPC slot available, so we'll drop this node's IPC info and just close
447 // the fd. To avoid an old `estimated_total_items` or `completed_count` value still
448 // being rendered for the node, we'll zero that field out (and the user is not allowed
449 // to change it because they think we're doing IPC).
450 file.close(io);
451 @atomicStore(u32, &storageByIndex(index).completed_count, 0, .monotonic);
452 @atomicStore(u32, &storageByIndex(index).estimated_total_count, 0, .monotonic);
453 }
454 }
455
456 pub fn setIpcIndex(node: Node, ipc_index: Ipc.Index) void {
457 storageByIndex(node.index.unwrap() orelse return).setIpcIndex(ipc_index);
458 }
459
460 /// Not thread-safe.
461 pub fn takeIpcIndex(node: Node) ?Ipc.Index {
462 const storage = storageByIndex(node.index.unwrap() orelse return null);
463 switch (storage.estimated_total_count) {
464 std.math.maxInt(u32) => {}, // indicates that there is an IPC index in `completed_count`
465 0 => return null, // `setIpcFile` failed so we don't have an IPC index for this node
466 else => unreachable, // not an IPC node
467 }
468 @atomicStore(u32, &storage.estimated_total_count, 0, .monotonic);
469 return @bitCast(storage.completed_count);
470 }
471
472 fn storageByIndex(index: Node.Index) *Node.Storage {
473 return &global_progress.node_storage[@backingInt(index)];
474 }
475
476 fn parentByIndex(index: Node.Index) *Node.Parent {
477 return &global_progress.node_parents[@backingInt(index)];
478 }
479
480 fn freelistNextByIndex(index: Node.Index) *Node.OptionalIndex {
481 return &global_progress.node_freelist_next[@backingInt(index)];
482 }
483
484 fn init(free_index: Index, parent: Parent, name: []const u8, estimated_total_items: usize) Node {
485 assert(parent == .none or @backingInt(parent) < node_storage_buffer_len);
486
487 const storage = storageByIndex(free_index);
488 @atomicStore(u32, &storage.completed_count, 0, .monotonic);
489 // Avoid u32 max int which is used to indicate a special state.
490 const saturated_total_count = @min(std.math.maxInt(u32) - 1, estimated_total_items);
491 @atomicStore(u32, &storage.estimated_total_count, saturated_total_count, .monotonic);
492 const name_len = @min(max_name_len, name.len);
493 copyAtomicStore(storage.name[0..name_len], name[0..name_len]);
494 if (name_len < storage.name.len)
495 @atomicStore(u8, &storage.name[name_len], 0, .monotonic);
496
497 const parent_ptr = parentByIndex(free_index);
498 if (std.debug.runtime_safety) {
499 assert(@atomicLoad(Node.Parent, parent_ptr, .monotonic) == .unused);
500 }
501 @atomicStore(Node.Parent, parent_ptr, parent, .monotonic);
502
503 return .{ .index = free_index.toOptional() };
504 }
505};
506
507var global_progress: Progress = .{
508 .io = undefined,
509 .terminal = undefined,
510 .terminal_mode = .off,
511 .update_worker = null,
512 .redraw_event = .unset,
513 .refresh_rate_ns = undefined,
514 .initial_delay_ns = undefined,
515 .rows = 0,
516 .cols = 0,
517 .draw_buffer = undefined,
518 .need_clear = false,
519 .status = .working,
520
521 .node_parents = undefined,
522 .node_storage = undefined,
523 .node_freelist_next = undefined,
524 .node_freelist = .{ .head = .none, .generation = 0 },
525 .node_end_index = 0,
526
527 .ipc_next = 0,
528 .ipc = undefined,
529 .ipc_files = undefined,
530
531 .start_failure = .unstarted,
532};
533
534pub const StartFailure = union(enum) {
535 unstarted,
536 spawn_ipc_worker: error{ConcurrencyUnavailable},
537 spawn_update_worker: error{ConcurrencyUnavailable},
538 parent_ipc: error{ UnsupportedOperation, UnrecognizedFormat },
539};
540
541/// One less than a power of two ensures `max_packet_len` is already a power of two.
542const node_storage_buffer_len = ipc_storage_buffer_len - 1;
543
544/// Power of two to avoid wasted `ipc_next` increments.
545const ipc_storage_buffer_len = 128;
546
547pub const max_packet_len = std.math.ceilPowerOfTwoAssert(
548 usize,
549 1 + node_storage_buffer_len * (@sizeOf(Node.Storage) + @sizeOf(Node.OptionalIndex)),
550);
551
552var default_draw_buffer: [4096]u8 = undefined;
553
554var debug_start_trace = std.debug.Trace.init;
555
556pub const have_ipc = switch (builtin.os.tag) {
557 .wasi, .freestanding => false,
558 else => true,
559};
560
561const noop_impl = builtin.single_threaded or switch (builtin.os.tag) {
562 .wasi, .freestanding => true,
563 else => false,
564} or switch (builtin.zig_backend) {
565 else => false,
566};
567
568pub const ParentFileError = error{
569 UnsupportedOperation,
570 EnvironmentVariableMissing,
571 UnrecognizedFormat,
572};
573
574pub const Options = struct {
575 /// User-provided buffer with static lifetime.
576 ///
577 /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it
578 /// cannot fit into this buffer which will look bad but not cause any malfunctions.
579 ///
580 /// Must be at least 200 bytes.
581 draw_buffer: []u8 = &default_draw_buffer,
582 /// How many nanoseconds between writing updates to the terminal.
583 refresh_rate_ns: Io.Duration = .fromMilliseconds(80),
584 /// How many nanoseconds to keep the output hidden
585 initial_delay_ns: Io.Duration = .fromMilliseconds(200),
586 /// If provided, causes the progress item to have a denominator.
587 /// 0 means unknown.
588 estimated_total_items: usize = 0,
589 root_name: []const u8 = "",
590 disable_printing: bool = false,
591};
592
593/// Initializes a global Progress instance.
594///
595/// Asserts there is only one global Progress instance.
596///
597/// Call `Node.end` when done.
598///
599/// If an error occurs, `start_failure` will be populated.
600pub fn start(io: Io, options: Options) Node {
601 // Ensure there is only 1 global Progress object.
602 if (global_progress.node_end_index != 0) {
603 debug_start_trace.dump();
604 unreachable;
605 }
606 debug_start_trace.add("first initialized here");
607
608 @memset(&global_progress.node_parents, .unused);
609 @memset(&global_progress.ipc, .{ .locked = false, .valid = false, .generation = 0 });
610 const root_node = Node.init(@fromBackingInt(@intCast(0)), .none, options.root_name, options.estimated_total_items);
611 global_progress.node_end_index = 1;
612
613 assert(options.draw_buffer.len >= 200);
614 global_progress.draw_buffer = options.draw_buffer;
615 global_progress.refresh_rate_ns = @intCast(options.refresh_rate_ns.toNanoseconds());
616 global_progress.initial_delay_ns = @intCast(options.initial_delay_ns.toNanoseconds());
617
618 if (noop_impl) return .none;
619
620 global_progress.io = io;
621
622 if (io.vtable.progressParentFile(io.userdata)) |ipc_file| {
623 global_progress.update_worker = io.concurrent(ipcThreadRun, .{ io, ipc_file }) catch |err| {
624 global_progress.start_failure = .{ .spawn_ipc_worker = err };
625 return .none;
626 };
627 } else |env_err| switch (env_err) {
628 error.EnvironmentVariableMissing => {
629 if (options.disable_printing) return .none;
630 const stderr: Io.File = .stderr();
631 global_progress.terminal = stderr;
632 if (stderr.enableAnsiEscapeCodes(io)) |_| {
633 global_progress.terminal_mode = .ansi_escape_codes;
634 } else |_| if (is_windows) {
635 var get_console_cp = windows.CONSOLE.USER_IO.GET_CP(.Output);
636 // Normally, we would pass `null` to `operate` here as the kernel32
637 // function does not accept a handle, however, if we pass one anyway,
638 // then we will get an error if the handle is not associated with
639 // this process's console, effectively combining an `isTty` check
640 // into the same syscall.
641 switch (get_console_cp.operate(io, stderr) catch |err| switch (err) {
642 error.Canceled => {
643 io.recancel();
644 return .none;
645 },
646 }) {
647 .SUCCESS => global_progress.terminal_mode = .{ .windows_api = .{
648 .code_page = get_console_cp.Data.CodePage,
649 } },
650 .INVALID_HANDLE => {},
651 else => {},
652 }
653 }
654 if (future: switch (global_progress.terminal_mode) {
655 .off => return .none,
656 .ansi_escape_codes => {
657 if (have_sigwinch) {
658 const act: posix.Sigaction = .{
659 .handler = .{ .sigaction = handleSigWinch },
660 .mask = posix.sigemptyset(),
661 .flags = (posix.SA.SIGINFO | posix.SA.RESTART),
662 };
663 posix.sigaction(.WINCH, &act, null);
664 }
665 break :future io.concurrent(updateTask, .{io});
666 },
667 .windows_api => io.concurrent(windowsApiUpdateTask, .{io}),
668 }) |future| {
669 global_progress.update_worker = future;
670 } else |err| {
671 global_progress.start_failure = .{ .spawn_update_worker = err };
672 return .none;
673 }
674 },
675 else => |e| {
676 global_progress.start_failure = .{ .parent_ipc = e };
677 return .none;
678 },
679 }
680
681 return root_node;
682}
683
684pub fn setStatus(new_status: Status) void {
685 if (noop_impl) return;
686 @atomicStore(Status, &global_progress.status, new_status, .monotonic);
687}
688
689/// Returns whether a resize is needed to learn the terminal size.
690fn wait(io: Io, timeout_ns: u64) Io.Cancelable!bool {
691 const timeout: Io.Timeout = .{ .duration = .{
692 .clock = .awake,
693 .raw = .fromNanoseconds(timeout_ns),
694 } };
695 const resize_flag = if (global_progress.redraw_event.waitTimeout(io, timeout)) |_| true else |err| switch (err) {
696 error.Timeout => false,
697 error.Canceled => |e| return e,
698 };
699 global_progress.redraw_event.reset();
700 return resize_flag or (global_progress.cols == 0);
701}
702
703const WorkerError = error{WindowTooSmall} || Io.ConcurrentError || Io.Cancelable ||
704 Io.File.Writer.Error || Io.Operation.FileReadStreaming.Error;
705
706fn updateTask(io: Io) WorkerError!void {
707 // Store this data in the thread so that it does not need to be part of the
708 // linker data of the main executable.
709 var serialized_buffer: Serialized.Buffer = undefined;
710 serialized_buffer.init();
711 defer serialized_buffer.batch.cancel(io);
712
713 // In this function we bypass the wrapper code inside `Io.lockStderr` /
714 // `Io.tryLockStderr` in order to avoid clearing the terminal twice.
715 // We still want to go through the `Io` instance however in case it uses a
716 // task-switching mutex.
717
718 try maybeUpdateSize(io, try wait(io, global_progress.initial_delay_ns));
719 errdefer {
720 const cancel_protection = io.swapCancelProtection(.blocked);
721 defer _ = io.swapCancelProtection(cancel_protection);
722 const stderr = io.vtable.lockStderr(io.userdata, null) catch |err| switch (err) {
723 error.Canceled => unreachable, // blocked
724 };
725 defer io.unlockStderr();
726 clearWrittenWithEscapeCodes(stderr.file_writer) catch {};
727 }
728 while (true) {
729 const buffer, _ = try computeRedraw(io, &serialized_buffer);
730 if (try io.vtable.tryLockStderr(io.userdata, null)) |locked_stderr| {
731 defer io.unlockStderr();
732 global_progress.need_clear = true;
733 locked_stderr.file_writer.interface.writeAll(buffer) catch |err| switch (err) {
734 error.WriteFailed => return locked_stderr.file_writer.err.?,
735 };
736 }
737
738 try maybeUpdateSize(io, try wait(io, global_progress.refresh_rate_ns));
739 }
740}
741
742const WindowsApiError = Io.Cancelable || Io.UnexpectedError;
743
744fn windowsApiWriteMarker(io: Io) WindowsApiError!void {
745 // Write the marker that we will use to find the beginning of the progress when clearing.
746 // Note: This doesn't have to use WriteConsoleW, but doing so avoids dealing with the code page.
747 const terminal = global_progress.terminal;
748 var write_console = windows.CONSOLE.USER_IO.WRITE(.WideCharacter);
749 const buffer = [1]windows.WCHAR{windows_api_start_marker};
750 switch ((try io.operate(.{ .device_io_control = .{
751 .file = terminal,
752 .code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
753 .in = @ptrCast(&write_console.request(null, 1, .{
754 .{ .Size = @sizeOf(@TypeOf(buffer)), .Pointer = &buffer },
755 }, 0, .{})),
756 } })).device_io_control.u.Status) {
757 .SUCCESS => {},
758 .CANCELLED => unreachable,
759 else => |status| return windows.unexpectedStatus(status),
760 }
761}
762
763fn windowsApiUpdateTask(io: Io) WorkerError!void {
764 // Store this data in the thread so that it does not need to be part of the
765 // linker data of the main executable.
766 var serialized_buffer: Serialized.Buffer = undefined;
767 serialized_buffer.init();
768 defer serialized_buffer.batch.cancel(io);
769
770 // In this function we bypass the wrapper code inside `Io.lockStderr` /
771 // `Io.tryLockStderr` in order to avoid clearing the terminal twice.
772 // We still want to go through the `Io` instance however in case it uses a
773 // task-switching mutex.
774
775 try maybeUpdateSize(io, try wait(io, global_progress.initial_delay_ns));
776 errdefer {
777 const cancel_protection = io.swapCancelProtection(.blocked);
778 defer _ = io.swapCancelProtection(cancel_protection);
779 _ = io.vtable.lockStderr(io.userdata, null) catch |err| switch (err) {
780 error.Canceled => unreachable, // blocked
781 };
782 defer io.unlockStderr();
783 clearWrittenWindowsApi(io) catch {};
784 }
785 while (true) {
786 const buffer, const nl_n = try computeRedraw(io, &serialized_buffer);
787 if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| {
788 defer io.unlockStderr();
789 try clearWrittenWindowsApi(io);
790 try windowsApiWriteMarker(io);
791 global_progress.need_clear = true;
792 locked_stderr.file_writer.interface.writeAll(buffer) catch |err| switch (err) {
793 error.WriteFailed => return locked_stderr.file_writer.err.?,
794 };
795 windowsApiMoveToMarker(io, nl_n) catch return;
796 }
797
798 try maybeUpdateSize(io, try wait(io, global_progress.refresh_rate_ns));
799 }
800}
801
802fn ipcThreadRun(io: Io, file: Io.File) WorkerError!void {
803 // Store this data in the thread so that it does not need to be part of the
804 // linker data of the main executable.
805 var serialized_buffer: Serialized.Buffer = undefined;
806 serialized_buffer.init();
807 defer serialized_buffer.batch.cancel(io);
808 var fw = file.writerStreaming(io, &.{});
809
810 _ = try io.sleep(.fromNanoseconds(global_progress.initial_delay_ns), .awake);
811 while (true) {
812 writeIpc(&fw.interface, try serialize(io, &serialized_buffer)) catch |err| switch (err) {
813 error.WriteFailed => return fw.err.?,
814 };
815
816 _ = try io.sleep(.fromNanoseconds(global_progress.refresh_rate_ns), .awake);
817 }
818}
819
820const start_sync = "\x1b[?2026h";
821const up_one_line = "\x1bM";
822const clear = "\x1b[J";
823const save = "\x1b7";
824const restore = "\x1b8";
825const finish_sync = "\x1b[?2026l";
826
827const progress_remove = "\x1b]9;4;0\x1b\\";
828const @"progress_normal {d}" = "\x1b]9;4;1;{d}\x1b\\";
829const @"progress_error {d}" = "\x1b]9;4;2;{d}\x1b\\";
830const progress_pulsing = "\x1b]9;4;3\x1b\\";
831const progress_pulsing_error = "\x1b]9;4;2\x1b\\";
832const progress_normal_100 = "\x1b]9;4;1;100\x1b\\";
833const progress_error_100 = "\x1b]9;4;2;100\x1b\\";
834
835const TreeSymbol = enum {
836 /// ├─
837 tee,
838 /// │
839 line,
840 /// └─
841 langle,
842
843 const Encoding = enum {
844 ansi_escapes,
845 code_page_437,
846 utf8,
847 ascii,
848 };
849
850 /// The escape sequence representation as a string literal
851 fn escapeSeq(symbol: TreeSymbol) *const [9:0]u8 {
852 return switch (symbol) {
853 .tee => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ",
854 .line => "\x1B\x28\x30\x78\x1B\x28\x42 ",
855 .langle => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ",
856 };
857 }
858
859 fn bytes(symbol: TreeSymbol, encoding: Encoding) []const u8 {
860 return switch (encoding) {
861 .ansi_escapes => escapeSeq(symbol),
862 .code_page_437 => switch (symbol) {
863 .tee => "\xC3\xC4 ",
864 .line => "\xB3 ",
865 .langle => "\xC0\xC4 ",
866 },
867 .utf8 => switch (symbol) {
868 .tee => "├─ ",
869 .line => "│ ",
870 .langle => "└─ ",
871 },
872 .ascii => switch (symbol) {
873 .tee => "|- ",
874 .line => "| ",
875 .langle => "+- ",
876 },
877 };
878 }
879
880 fn maxByteLen(symbol: TreeSymbol) usize {
881 var max: usize = 0;
882 inline for (@typeInfo(Encoding).@"enum".field_names) |field_name| {
883 const len = symbol.bytes(@field(Encoding, field_name)).len;
884 max = @max(max, len);
885 }
886 return max;
887 }
888};
889
890fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
891 switch (global_progress.terminal_mode) {
892 .off => unreachable,
893 .ansi_escape_codes => {
894 const bytes = symbol.escapeSeq();
895 buf[start_i..][0..bytes.len].* = bytes.*;
896 return start_i + bytes.len;
897 },
898 .windows_api => |windows_api| {
899 const bytes = switch (windows_api.code_page) {
900 // Code page 437 is the default code page and contains the box drawing symbols
901 437 => symbol.bytes(.code_page_437),
902 // UTF-8
903 65001 => symbol.bytes(.utf8),
904 // Fall back to ASCII approximation
905 else => symbol.bytes(.ascii),
906 };
907 @memcpy(buf[start_i..][0..bytes.len], bytes);
908 return start_i + bytes.len;
909 },
910 }
911}
912
913pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) Io.Writer.Error!void {
914 if (noop_impl or !global_progress.need_clear) return;
915 try file_writer.interface.writeAll(clear ++ progress_remove);
916 global_progress.need_clear = false;
917}
918
919/// U+25BA or â–º
920const windows_api_start_marker = 0x25BA;
921
922fn clearWrittenWindowsApi(io: Io) WindowsApiError!void {
923 // This uses a 'marker' strategy. The idea is:
924 // - Always write a marker (in this case U+25BA or â–º) at the beginning of the progress
925 // - Get the current cursor position (at the end of the progress)
926 // - Subtract the number of lines written to get the expected start of the progress
927 // - Check to see if the first character at the start of the progress is the marker
928 // - If it's not the marker, keep checking the line before until we find it
929 // - Clear the screen from that position down, and set the cursor position to the start
930 //
931 // This strategy works even if there is line wrapping, and can handle the window
932 // being resized/scrolled arbitrarily.
933 //
934 // Notes:
935 // - Ideally, the marker would be a zero-width character, but the Windows console
936 // doesn't seem to support rendering zero-width characters (they show up as a space)
937 // - This same marker idea could technically be done with an attribute instead
938 // (https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes)
939 // but it must be a valid attribute and it actually needs to apply to the first
940 // character in order to be readable via ReadConsoleOutputAttribute. It doesn't seem
941 // like any of the available attributes are invisible/benign.
942 if (!global_progress.need_clear) return;
943 const terminal = global_progress.terminal;
944 const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
945
946 var get_console_info = windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
947 switch (try get_console_info.operate(io, terminal)) {
948 .SUCCESS => {},
949 else => |status| return windows.unexpectedStatus(status),
950 }
951 var fill_spaces = windows.CONSOLE.USER_IO.FILL(
952 .{ .WideCharacter = ' ' },
953 screen_area,
954 get_console_info.Data.dwCursorPosition,
955 );
956 switch (try fill_spaces.operate(io, terminal)) {
957 .SUCCESS => {},
958 else => |status| return windows.unexpectedStatus(status),
959 }
960}
961
962fn windowsApiMoveToMarker(io: Io, nl_n: usize) WindowsApiError!void {
963 const terminal = global_progress.terminal;
964 var get_console_info = windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
965 switch (try get_console_info.operate(io, terminal)) {
966 .SUCCESS => {},
967 else => |status| return windows.unexpectedStatus(status),
968 }
969 const cursor_pos = get_console_info.Data.dwCursorPosition;
970 const expected_y = cursor_pos.Y - @as(i16, @intCast(nl_n));
971 var start_pos: windows.COORD = .{ .X = 0, .Y = expected_y };
972 while (start_pos.Y >= 0) : (start_pos.Y -= 1) {
973 var read_output_char = windows.CONSOLE.USER_IO.READ_OUTPUT_CHARACTER(start_pos, .WideCharacter);
974 var buffer: [1]windows.WCHAR = undefined;
975 switch ((try io.operate(.{ .device_io_control = .{
976 .file = .{
977 .handle = windows.peb().ProcessParameters.ConsoleHandle,
978 .flags = .{ .nonblocking = false },
979 },
980 .code = windows.IOCTL.CONDRV.ISSUE_USER_IO,
981 .in = @ptrCast(&read_output_char.request(terminal, 0, .{}, 1, .{
982 .{ .Size = @sizeOf(@TypeOf(buffer)), .Pointer = &buffer },
983 })),
984 } })).device_io_control.u.Status) {
985 .SUCCESS => {},
986 .CANCELLED => unreachable,
987 else => |status| return windows.unexpectedStatus(status),
988 }
989 if (read_output_char.Data.nLength >= 1 and buffer[0] == windows_api_start_marker) break;
990 } else {
991 // If we couldn't find the marker, then just assume that no lines wrapped
992 start_pos = .{ .X = 0, .Y = expected_y };
993 }
994 var set_cursor_position = windows.CONSOLE.USER_IO.SET_CURSOR_POSITION(start_pos);
995 switch (try set_cursor_position.operate(io, terminal)) {
996 .SUCCESS => {},
997 else => |status| return windows.unexpectedStatus(status),
998 }
999}
1000
1001const Children = struct {
1002 child: Node.OptionalIndex,
1003 sibling: Node.OptionalIndex,
1004};
1005
1006const Serialized = struct {
1007 parents: []Node.Parent,
1008 storage: []Node.Storage,
1009
1010 const Buffer = struct {
1011 parents: [node_storage_buffer_len]Node.Parent,
1012 storage: [node_storage_buffer_len]Node.Storage,
1013
1014 ipc_start: u8,
1015 ipc_end: u8,
1016 ipc_data: [ipc_storage_buffer_len]Ipc.Data,
1017 ipc_buffers: [ipc_storage_buffer_len][max_packet_len]u8,
1018 ipc_vecs: [ipc_storage_buffer_len][1][]u8,
1019 batch_storage: [ipc_storage_buffer_len]Io.Operation.Storage,
1020 batch: Io.Batch,
1021
1022 fn init(buffer: *Buffer) void {
1023 buffer.ipc_start = 0;
1024 buffer.ipc_end = 0;
1025 @memset(&buffer.ipc_data, .unused);
1026 buffer.batch = .init(&buffer.batch_storage);
1027 }
1028 };
1029};
1030
1031fn serialize(io: Io, serialized_buffer: *Serialized.Buffer) !Serialized {
1032 var prev_parents: [node_storage_buffer_len]Node.Parent = undefined;
1033 var prev_storage: [node_storage_buffer_len]Node.Storage = undefined;
1034 {
1035 const ipc_start = serialized_buffer.ipc_start;
1036 const ipc_end = serialized_buffer.ipc_end;
1037 @memcpy(prev_parents[ipc_start..ipc_end], serialized_buffer.parents[ipc_start..ipc_end]);
1038 @memcpy(prev_storage[ipc_start..ipc_end], serialized_buffer.storage[ipc_start..ipc_end]);
1039 }
1040
1041 // Iterate all of the nodes and construct a serializable copy of the state that can be examined
1042 // without atomics. The `@min` call is here because `node_end_index` might briefly exceed the
1043 // node count sometimes.
1044 const end_index = @min(
1045 @atomicLoad(u32, &global_progress.node_end_index, .monotonic),
1046 node_storage_buffer_len,
1047 );
1048 var map: [node_storage_buffer_len]Node.OptionalIndex = undefined;
1049 var serialized_len: u8 = 0;
1050 var maybe_ipc_start: ?u8 = null;
1051 for (
1052 global_progress.node_parents[0..end_index],
1053 global_progress.node_storage[0..end_index],
1054 map[0..end_index],
1055 ) |*parent_ptr, *storage_ptr, *map_entry| {
1056 const parent = @atomicLoad(Node.Parent, parent_ptr, .monotonic);
1057 if (parent == .unused) {
1058 // We might read "mixed" node data in this loop, due to weird atomic things
1059 // or just a node actually being freed while this loop runs. That could cause
1060 // there to be a parent reference to a nonexistent node. Without this assignment,
1061 // this would lead to the map entry containing stale data. By assigning none, the
1062 // child node with the bad parent pointer will be harmlessly omitted from the tree.
1063 //
1064 // Note that there's no concern of potentially creating "looping" data if we read
1065 // "mixed" node data like this, because if a node is (directly or indirectly) its own
1066 // parent, it will just not be printed at all. The general idea here is that performance
1067 // is more important than 100% correct output every frame, given that this API is likely
1068 // to be used in hot paths!
1069 map_entry.* = .none;
1070 continue;
1071 }
1072 const dest_storage = &serialized_buffer.storage[serialized_len];
1073 copyAtomicLoad(&dest_storage.name, &storage_ptr.name);
1074 dest_storage.estimated_total_count = @atomicLoad(u32, &storage_ptr.estimated_total_count, .acquire); // sychronizes with release in `setIpcIndex`
1075 dest_storage.completed_count = @atomicLoad(u32, &storage_ptr.completed_count, .monotonic);
1076
1077 serialized_buffer.parents[serialized_len] = parent;
1078 map_entry.* = @fromBackingInt(@intCast(serialized_len));
1079 if (maybe_ipc_start == null and dest_storage.getIpcIndex() != null) maybe_ipc_start = serialized_len;
1080 serialized_len += 1;
1081 }
1082
1083 // Remap parents to point inside serialized arrays.
1084 for (serialized_buffer.parents[0..serialized_len]) |*parent| {
1085 parent.* = switch (parent.*) {
1086 .unused => unreachable,
1087 .none => .none,
1088 _ => |p| map[@backingInt(p)].toParent(),
1089 };
1090 }
1091
1092 // Fill pipe buffers.
1093 const batch = &serialized_buffer.batch;
1094 batch.awaitConcurrent(io, .{
1095 .duration = .{ .raw = .zero, .clock = .awake },
1096 }) catch |err| switch (err) {
1097 error.Timeout => {},
1098 else => |e| return e,
1099 };
1100 var ready_len: u8 = 0;
1101 while (batch.next()) |operation| switch (operation.index) {
1102 0...ipc_storage_buffer_len - 1 => {
1103 const ipc_data = &serialized_buffer.ipc_data[operation.index];
1104 ipc_data.bytes_read += @intCast(
1105 operation.result.file_read_streaming catch |err| switch (err) {
1106 error.EndOfStream => {
1107 const file = global_progress.ipc_files[operation.index];
1108 const ipc = @atomicRmw(
1109 Ipc,
1110 &global_progress.ipc[operation.index],
1111 .And,
1112 .{
1113 .locked = false,
1114 .valid = true,
1115 .generation = std.math.maxInt(Ipc.Generation),
1116 },
1117 .release,
1118 );
1119 assert(ipc.locked);
1120 if (!ipc.valid) file.close(io);
1121 ipc_data.* = .unused;
1122 continue;
1123 },
1124 else => |e| return e,
1125 },
1126 );
1127 assert(ipc_data.state == .pending);
1128 ipc_data.state = .ready;
1129 ready_len += 1;
1130 },
1131 else => unreachable,
1132 };
1133
1134 // Find nodes which correspond to child processes.
1135 const ipc_start = maybe_ipc_start orelse serialized_len;
1136 serialized_buffer.ipc_start = ipc_start;
1137 for (
1138 serialized_buffer.parents[ipc_start..serialized_len],
1139 serialized_buffer.storage[ipc_start..serialized_len],
1140 ipc_start..,
1141 ) |main_parent, *main_storage, main_index| {
1142 if (main_parent == .unused) continue;
1143 const ipc_index = main_storage.getIpcIndex() orelse continue;
1144 const ipc = &global_progress.ipc[ipc_index.slot];
1145 const ipc_data = &serialized_buffer.ipc_data[ipc_index.slot];
1146 state: switch (ipc_data.state) {
1147 .unused => {
1148 if (@cmpxchgWeak(
1149 Ipc,
1150 ipc,
1151 .{ .locked = false, .valid = true, .generation = ipc_index.generation },
1152 .{ .locked = true, .valid = true, .generation = ipc_index.generation },
1153 .acquire,
1154 .monotonic,
1155 )) |_| continue;
1156
1157 const ipc_vec = &serialized_buffer.ipc_vecs[ipc_index.slot];
1158 ipc_vec.* = .{&serialized_buffer.ipc_buffers[ipc_index.slot]};
1159 batch.addAt(ipc_index.slot, .{ .file_read_streaming = .{
1160 .file = global_progress.ipc_files[ipc_index.slot],
1161 .data = ipc_vec,
1162 } });
1163
1164 ipc_data.* = .{
1165 .state = .pending,
1166 .bytes_read = 0,
1167 .main_index = @intCast(main_index),
1168 .start_index = serialized_len,
1169 .nodes_len = 0,
1170 };
1171 main_storage.completed_count = 0;
1172 main_storage.estimated_total_count = 0;
1173 },
1174 .pending => {
1175 const start_index = ipc_data.start_index;
1176 const nodes_len = @min(ipc_data.nodes_len, node_storage_buffer_len - serialized_len);
1177
1178 main_storage.copyRoot(&prev_storage[ipc_data.main_index]);
1179 @memcpy(
1180 serialized_buffer.storage[serialized_len..][0..nodes_len],
1181 prev_storage[start_index..][0..nodes_len],
1182 );
1183 for (
1184 serialized_buffer.parents[serialized_len..][0..nodes_len],
1185 prev_parents[serialized_len..][0..nodes_len],
1186 ) |*parent, prev_parent| parent.* = switch (prev_parent) {
1187 .none, .unused => .none,
1188 _ => if (@backingInt(prev_parent) == ipc_data.main_index)
1189 @fromBackingInt(@intCast(main_index))
1190 else if (@backingInt(prev_parent) >= start_index and
1191 @backingInt(prev_parent) < start_index + nodes_len)
1192 @fromBackingInt(@intCast(@backingInt(prev_parent) - start_index + serialized_len))
1193 else
1194 .none,
1195 };
1196
1197 ipc_data.main_index = @intCast(main_index);
1198 ipc_data.start_index = serialized_len;
1199 ipc_data.nodes_len = nodes_len;
1200 serialized_len += nodes_len;
1201 },
1202 .ready => {
1203 const ipc_buffer = &serialized_buffer.ipc_buffers[ipc_index.slot];
1204 const packet_start, const packet_end = ipc_data.findLastPacket(ipc_buffer);
1205 const packet_is_empty = packet_end - packet_start <= 1;
1206 if (!packet_is_empty) {
1207 const storage, const parents, const nodes_len = packet_contents: {
1208 var packet_index: usize = packet_start;
1209 const nodes_len: u16 = ipc_buffer[packet_index];
1210 packet_index += 1;
1211 const storage_bytes =
1212 ipc_buffer[packet_index..][0 .. nodes_len * @sizeOf(Node.Storage)];
1213 packet_index += storage_bytes.len;
1214 const parents_bytes =
1215 ipc_buffer[packet_index..][0 .. nodes_len * @sizeOf(Node.Parent)];
1216 packet_index += parents_bytes.len;
1217 assert(packet_index == packet_end);
1218 const storage: []align(1) const Node.Storage = @ptrCast(storage_bytes);
1219 const parents: []align(1) const Node.Parent = @ptrCast(parents_bytes);
1220 const children_nodes_len =
1221 @min(nodes_len - 1, node_storage_buffer_len - serialized_len);
1222 break :packet_contents .{ storage, parents, children_nodes_len };
1223 };
1224
1225 // Mount the root here.
1226 main_storage.copyRoot(&storage[0]);
1227 if (is_big_endian) main_storage.byteSwap();
1228
1229 // Copy the rest of the tree to the end.
1230 const serialized_storage =
1231 serialized_buffer.storage[serialized_len..][0..nodes_len];
1232 @memcpy(serialized_storage, storage[1..][0..nodes_len]);
1233 if (is_big_endian) for (serialized_storage) |*s| s.byteSwap();
1234
1235 // Patch up parent pointers taking into account how the subtree is mounted.
1236 for (
1237 serialized_buffer.parents[serialized_len..][0..nodes_len],
1238 parents[1..][0..nodes_len],
1239 ) |*parent, prev_parent| parent.* = switch (prev_parent) {
1240 // Fix bad data so the rest of the code does not see `unused`.
1241 .none, .unused => .none,
1242 // Root node is being mounted here.
1243 @as(Node.Parent, @fromBackingInt(@intCast(0))) => @fromBackingInt(@intCast(main_index)),
1244 // Other nodes mounted at the end.
1245 // Don't trust child data; if the data is outside the expected range,
1246 // ignore the data. This also handles the case when data was truncated.
1247 _ => if (@backingInt(prev_parent) <= nodes_len)
1248 @fromBackingInt(@intCast(@backingInt(prev_parent) - 1 + serialized_len))
1249 else
1250 .none,
1251 };
1252
1253 ipc_data.main_index = @intCast(main_index);
1254 ipc_data.start_index = serialized_len;
1255 ipc_data.nodes_len = nodes_len;
1256 serialized_len += nodes_len;
1257 }
1258 const ipc_vec = &serialized_buffer.ipc_vecs[ipc_index.slot];
1259 ipc_data.rebase(ipc_buffer, ipc_vec, batch, ipc_index.slot, packet_end);
1260 ready_len -= 1;
1261 if (packet_is_empty) continue :state .pending;
1262 },
1263 }
1264 }
1265 serialized_buffer.ipc_end = serialized_len;
1266
1267 // Ignore data from unused pipes. This ensures that if a child process exists we will
1268 // eventually see `EndOfStream` and close the pipe.
1269 if (ready_len > 0) for (
1270 &serialized_buffer.ipc_data,
1271 &serialized_buffer.ipc_buffers,
1272 &serialized_buffer.ipc_vecs,
1273 0..,
1274 ) |*ipc_data, *ipc_buffer, *ipc_vec, ipc_slot| switch (ipc_data.state) {
1275 .unused, .pending => {},
1276 .ready => {
1277 _, const packet_end = ipc_data.findLastPacket(ipc_buffer);
1278 ipc_data.rebase(ipc_buffer, ipc_vec, batch, @intCast(ipc_slot), packet_end);
1279 ready_len -= 1;
1280 },
1281 };
1282 assert(ready_len == 0);
1283
1284 return .{
1285 .parents = serialized_buffer.parents[0..serialized_len],
1286 .storage = serialized_buffer.storage[0..serialized_len],
1287 };
1288}
1289
1290fn computeRedraw(io: Io, serialized_buffer: *Serialized.Buffer) !struct { []u8, usize } {
1291 if (global_progress.rows == 0 or global_progress.cols == 0) return error.WindowTooSmall;
1292
1293 const serialized = try serialize(io, serialized_buffer);
1294
1295 // Now we can analyze our copy of the graph without atomics, reconstructing
1296 // children lists which do not exist in the canonical data. These are
1297 // needed for tree traversal below.
1298
1299 var children_buffer: [node_storage_buffer_len]Children = undefined;
1300 const children = children_buffer[0..serialized.parents.len];
1301
1302 @memset(children, .{ .child = .none, .sibling = .none });
1303
1304 for (serialized.parents, 0..) |parent, child_index_usize| {
1305 const child_index: Node.Index = @fromBackingInt(@intCast(child_index_usize));
1306 assert(parent != .unused);
1307 const parent_index = parent.unwrap() orelse continue;
1308 const children_node = &children[@backingInt(parent_index)];
1309 if (children_node.child.unwrap()) |existing_child_index| {
1310 const existing_child = &children[@backingInt(existing_child_index)];
1311 children[@backingInt(child_index)].sibling = existing_child.sibling;
1312 existing_child.sibling = child_index.toOptional();
1313 } else {
1314 children_node.child = child_index.toOptional();
1315 }
1316 }
1317
1318 // The strategy is, with every redraw:
1319 // erase to end of screen, write, move cursor to beginning of line, move cursor up N lines
1320 // This keeps the cursor at the beginning so that unlocked stderr writes
1321 // don't get eaten by the clear.
1322
1323 var i: usize = 0;
1324 const buf = global_progress.draw_buffer;
1325
1326 if (global_progress.terminal_mode == .ansi_escape_codes) {
1327 buf[i..][0..start_sync.len].* = start_sync.*;
1328 i += start_sync.len;
1329 }
1330
1331 switch (global_progress.terminal_mode) {
1332 .off => unreachable,
1333 .ansi_escape_codes => {
1334 buf[i..][0..clear.len].* = clear.*;
1335 i += clear.len;
1336 },
1337 .windows_api => {},
1338 }
1339
1340 const root_node_index: Node.Index = @fromBackingInt(@intCast(0));
1341 i, const nl_n = computeNode(buf, i, 0, serialized, children, root_node_index);
1342
1343 if (global_progress.terminal_mode == .ansi_escape_codes) {
1344 {
1345 // Set progress state https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
1346 const root_storage = &serialized.storage[0];
1347 const storage = if (root_storage.name[0] != 0 or children[0].child == .none) root_storage else &serialized.storage[@backingInt(children[0].child)];
1348 const estimated_total = storage.estimated_total_count;
1349 const completed_items = storage.completed_count;
1350 const status = @atomicLoad(Status, &global_progress.status, .monotonic);
1351 switch (status) {
1352 .working => {
1353 if (estimated_total == 0) {
1354 buf[i..][0..progress_pulsing.len].* = progress_pulsing.*;
1355 i += progress_pulsing.len;
1356 } else {
1357 const percent = @as(u64, completed_items) * 100 / estimated_total;
1358 if (std.mem.print(buf[i..], @"progress_normal {d}", .{percent})) |b| {
1359 i += b.len;
1360 } else |_| {}
1361 }
1362 },
1363 .success => {
1364 buf[i..][0..progress_remove.len].* = progress_remove.*;
1365 i += progress_remove.len;
1366 },
1367 .failure => {
1368 buf[i..][0..progress_error_100.len].* = progress_error_100.*;
1369 i += progress_error_100.len;
1370 },
1371 .failure_working => {
1372 if (estimated_total == 0) {
1373 buf[i..][0..progress_pulsing_error.len].* = progress_pulsing_error.*;
1374 i += progress_pulsing_error.len;
1375 } else {
1376 const percent = @as(u64, completed_items) * 100 / estimated_total;
1377 if (std.mem.print(buf[i..], @"progress_error {d}", .{percent})) |b| {
1378 i += b.len;
1379 } else |_| {}
1380 }
1381 },
1382 }
1383 }
1384
1385 if (nl_n > 0) {
1386 buf[i] = '\r';
1387 i += 1;
1388 for (0..nl_n) |_| {
1389 buf[i..][0..up_one_line.len].* = up_one_line.*;
1390 i += up_one_line.len;
1391 }
1392 }
1393
1394 buf[i..][0..finish_sync.len].* = finish_sync.*;
1395 i += finish_sync.len;
1396 }
1397
1398 return .{ buf[0..i], nl_n };
1399}
1400
1401fn computePrefix(
1402 buf: []u8,
1403 start_i: usize,
1404 nl_n: usize,
1405 serialized: Serialized,
1406 children: []const Children,
1407 node_index: Node.Index,
1408) usize {
1409 var i = start_i;
1410 const parent_index = serialized.parents[@backingInt(node_index)].unwrap() orelse return i;
1411 if (serialized.parents[@backingInt(parent_index)] == .none) return i;
1412 if (@backingInt(serialized.parents[@backingInt(parent_index)]) == 0 and
1413 serialized.storage[0].name[0] == 0)
1414 {
1415 return i;
1416 }
1417 i = computePrefix(buf, i, nl_n, serialized, children, parent_index);
1418 if (children[@backingInt(parent_index)].sibling == .none) {
1419 const prefix = " ";
1420 const upper_bound_len = prefix.len + lineUpperBoundLen(nl_n);
1421 if (i + upper_bound_len > buf.len) return buf.len;
1422 buf[i..][0..prefix.len].* = prefix.*;
1423 i += prefix.len;
1424 } else {
1425 const upper_bound_len = TreeSymbol.line.maxByteLen() + lineUpperBoundLen(nl_n);
1426 if (i + upper_bound_len > buf.len) return buf.len;
1427 i = appendTreeSymbol(.line, buf, i);
1428 }
1429 return i;
1430}
1431
1432fn lineUpperBoundLen(nl_n: usize) usize {
1433 // \r\n on Windows, \n otherwise.
1434 const nl_len = if (is_windows) 2 else 1;
1435 return @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) +
1436 "[4294967296/4294967296] ".len + Node.max_name_len + nl_len +
1437 (1 + (nl_n + 1) * up_one_line.len) +
1438 finish_sync.len;
1439}
1440
1441fn computeNode(
1442 buf: []u8,
1443 start_i: usize,
1444 start_nl_n: usize,
1445 serialized: Serialized,
1446 children: []const Children,
1447 node_index: Node.Index,
1448) struct { usize, usize } {
1449 var i = start_i;
1450 var nl_n = start_nl_n;
1451
1452 i = computePrefix(buf, i, nl_n, serialized, children, node_index);
1453
1454 if (i + lineUpperBoundLen(nl_n) > buf.len)
1455 return .{ start_i, start_nl_n };
1456
1457 const storage = &serialized.storage[@backingInt(node_index)];
1458 const estimated_total = storage.estimated_total_count;
1459 const completed_items = storage.completed_count;
1460 const name = if (std.mem.findScalar(u8, &storage.name, 0)) |end| storage.name[0..end] else &storage.name;
1461 const parent = serialized.parents[@backingInt(node_index)];
1462
1463 if (parent != .none) p: {
1464 if (@backingInt(parent) == 0 and serialized.storage[0].name[0] == 0) {
1465 break :p;
1466 }
1467 if (children[@backingInt(node_index)].sibling == .none) {
1468 i = appendTreeSymbol(.langle, buf, i);
1469 } else {
1470 i = appendTreeSymbol(.tee, buf, i);
1471 }
1472 }
1473
1474 const is_empty_root = @backingInt(node_index) == 0 and serialized.storage[0].name[0] == 0;
1475 if (!is_empty_root) {
1476 if (name.len != 0 or estimated_total > 0) {
1477 if (estimated_total > 0) {
1478 if (std.mem.print(buf[i..], "[{d}/{d}] ", .{ completed_items, estimated_total })) |b| {
1479 i += b.len;
1480 } else |_| {}
1481 } else if (completed_items != 0) {
1482 if (std.mem.print(buf[i..], "[{d}] ", .{completed_items})) |b| {
1483 i += b.len;
1484 } else |_| {}
1485 }
1486 if (name.len != 0) {
1487 if (std.mem.print(buf[i..], "{s}", .{name})) |b| {
1488 i += b.len;
1489 } else |_| {}
1490 }
1491 }
1492
1493 i = @min(global_progress.cols + start_i, i);
1494 if (is_windows) {
1495 // \r\n on Windows is necessary for the old console with the
1496 // ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN
1497 // console modes set to behave properly.
1498 buf[i] = '\r';
1499 i += 1;
1500 }
1501 buf[i] = '\n';
1502 i += 1;
1503 nl_n += 1;
1504 }
1505
1506 if (global_progress.withinRowLimit(nl_n)) {
1507 if (children[@backingInt(node_index)].child.unwrap()) |child| {
1508 i, nl_n = computeNode(buf, i, nl_n, serialized, children, child);
1509 }
1510 }
1511
1512 if (global_progress.withinRowLimit(nl_n)) {
1513 if (children[@backingInt(node_index)].sibling.unwrap()) |sibling| {
1514 i, nl_n = computeNode(buf, i, nl_n, serialized, children, sibling);
1515 }
1516 }
1517
1518 return .{ i, nl_n };
1519}
1520
1521fn withinRowLimit(p: *Progress, nl_n: usize) bool {
1522 // The +2 here is so that the PS1 is not scrolled off the top of the terminal.
1523 // one because we keep the cursor on the next line
1524 // one more to account for the PS1
1525 return nl_n + 2 < p.rows;
1526}
1527
1528fn writeIpc(writer: *Io.Writer, serialized: Serialized) Io.Writer.Error!void {
1529 // Byteswap if necessary to ensure little endian over the pipe. This is
1530 // needed because the parent or child process might be running in qemu.
1531 if (is_big_endian) for (serialized.storage) |*s| s.byteSwap();
1532
1533 assert(serialized.parents.len == serialized.storage.len);
1534 const serialized_len: u8 = @intCast(serialized.parents.len);
1535 const header = std.mem.asBytes(&serialized_len);
1536 const storage = std.mem.sliceAsBytes(serialized.storage);
1537 const parents = std.mem.sliceAsBytes(serialized.parents);
1538
1539 var vec = [3][]const u8{ header, storage, parents };
1540 try writer.writeVecAll(&vec);
1541}
1542
1543fn maybeUpdateSize(io: Io, resize_flag: bool) !void {
1544 if (!resize_flag) return;
1545
1546 const file = global_progress.terminal;
1547
1548 if (is_windows) {
1549 var get_console_info = windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
1550 switch (try get_console_info.operate(io, file)) {
1551 .SUCCESS => {
1552 global_progress.rows = @intCast(get_console_info.Data.dwWindowSize.Y);
1553 global_progress.cols = @intCast(get_console_info.Data.dwWindowSize.X);
1554 },
1555 else => {
1556 std.log.debug("failed to determine terminal size; using conservative guess 80x25", .{});
1557 global_progress.rows = 25;
1558 global_progress.cols = 80;
1559 },
1560 }
1561 } else {
1562 var winsize: posix.winsize = .{
1563 .row = 0,
1564 .col = 0,
1565 .xpixel = 0,
1566 .ypixel = 0,
1567 };
1568
1569 const err = (try io.operate(.{ .device_io_control = .{
1570 .file = file,
1571 .code = posix.T.IOCGWINSZ,
1572 .arg = &winsize,
1573 } })).device_io_control;
1574
1575 if (err >= 0) {
1576 global_progress.rows = winsize.row;
1577 global_progress.cols = winsize.col;
1578 } else {
1579 std.log.debug("failed to determine terminal size; using conservative guess 80x25", .{});
1580 global_progress.rows = 25;
1581 global_progress.cols = 80;
1582 }
1583 }
1584}
1585
1586fn handleSigWinch(sig: posix.SIG, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void {
1587 _ = info;
1588 _ = ctx_ptr;
1589 assert(sig == .WINCH);
1590 global_progress.redraw_event.set(global_progress.io);
1591}
1592
1593const have_sigwinch = switch (builtin.os.tag) {
1594 .linux,
1595 .plan9,
1596 .illumos,
1597 .netbsd,
1598 .openbsd,
1599 .haiku,
1600 .driverkit,
1601 .ios,
1602 .maccatalyst,
1603 .macos,
1604 .tvos,
1605 .visionos,
1606 .watchos,
1607 .dragonfly,
1608 .freebsd,
1609 .serenity,
1610 => true,
1611
1612 else => false,
1613};
1614
1615fn copyAtomicStore(dest: []align(@alignOf(usize)) u8, src: []const u8) void {
1616 assert(dest.len == src.len);
1617 const chunked_len = dest.len / @sizeOf(usize);
1618 const dest_chunked: []usize = @as([*]usize, @ptrCast(dest))[0..chunked_len];
1619 const src_chunked: []align(1) const usize = @as([*]align(1) const usize, @ptrCast(src))[0..chunked_len];
1620 for (dest_chunked, src_chunked) |*d, s| {
1621 @atomicStore(usize, d, s, .monotonic);
1622 }
1623 const remainder_start = chunked_len * @sizeOf(usize);
1624 for (dest[remainder_start..], src[remainder_start..]) |*d, s| {
1625 @atomicStore(u8, d, s, .monotonic);
1626 }
1627}
1628
1629fn copyAtomicLoad(
1630 dest: *align(@alignOf(usize)) [Node.max_name_len]u8,
1631 src: *align(@alignOf(usize)) const [Node.max_name_len]u8,
1632) void {
1633 const chunked_len = @divExact(dest.len, @sizeOf(usize));
1634 const dest_chunked: *[chunked_len]usize = @ptrCast(dest);
1635 const src_chunked: *const [chunked_len]usize = @ptrCast(src);
1636 for (dest_chunked, src_chunked) |*d, *s| {
1637 d.* = @atomicLoad(usize, s, .monotonic);
1638 }
1639}