| 1 | //! This API is non-allocating, non-fallible, thread-safe, and lock-free. |
| 2 | const Progress = @This(); |
| 3 | |
| 4 | const builtin = @import("builtin"); |
| 5 | const is_big_endian = builtin.cpu.arch.endian() == .big; |
| 6 | const is_windows = builtin.os.tag == .windows; |
| 7 | |
| 8 | const std = @import("std"); |
| 9 | const Io = std.Io; |
| 10 | const windows = std.os.windows; |
| 11 | const testing = std.testing; |
| 12 | const assert = std.debug.assert; |
| 13 | const posix = std.posix; |
| 14 | const Writer = Io.Writer; |
| 15 | |
| 16 | /// Currently this API only supports this value being set to stderr, which |
| 17 | /// happens automatically inside `start`. |
| 18 | terminal: Io.File, |
| 19 | |
| 20 | io: Io, |
| 21 | |
| 22 | terminal_mode: TerminalMode, |
| 23 | |
| 24 | update_worker: ?Io.Future(WorkerError!void), |
| 25 | |
| 26 | /// Atomically set by SIGWINCH as well as the root done() function. |
| 27 | redraw_event: Io.Event, |
| 28 | need_clear: bool, |
| 29 | status: Status, |
| 30 | |
| 31 | refresh_rate_ns: u64, |
| 32 | initial_delay_ns: u64, |
| 33 | |
| 34 | rows: u16, |
| 35 | cols: u16, |
| 36 | |
| 37 | /// Accessed only by the update thread. |
| 38 | draw_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. |
| 43 | node_parents: [node_storage_buffer_len]Node.Parent, |
| 44 | node_storage: [node_storage_buffer_len]Node.Storage, |
| 45 | node_freelist_next: [node_storage_buffer_len]Node.OptionalIndex, |
| 46 | node_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. |
| 50 | node_end_index: u32, |
| 51 | |
| 52 | ipc_next: Ipc.SlotAtomic, |
| 53 | ipc: [ipc_storage_buffer_len]Ipc, |
| 54 | ipc_files: [ipc_storage_buffer_len]Io.File, |
| 55 | |
| 56 | start_failure: StartFailure, |
| 57 | |
| 58 | pub 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 | |
| 74 | const 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 | |
| 81 | pub 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 | |
| 155 | pub 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`. |
| 170 | pub 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 | |
| 507 | var 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 | |
| 534 | pub 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. |
| 542 | const node_storage_buffer_len = ipc_storage_buffer_len - 1; |
| 543 | |
| 544 | /// Power of two to avoid wasted `ipc_next` increments. |
| 545 | const ipc_storage_buffer_len = 128; |
| 546 | |
| 547 | pub const max_packet_len = std.math.ceilPowerOfTwoAssert( |
| 548 | usize, |
| 549 | 1 + node_storage_buffer_len * (@sizeOf(Node.Storage) + @sizeOf(Node.OptionalIndex)), |
| 550 | ); |
| 551 | |
| 552 | var default_draw_buffer: [4096]u8 = undefined; |
| 553 | |
| 554 | var debug_start_trace = std.debug.Trace.init; |
| 555 | |
| 556 | pub const have_ipc = switch (builtin.os.tag) { |
| 557 | .wasi, .freestanding => false, |
| 558 | else => true, |
| 559 | }; |
| 560 | |
| 561 | const 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 | |
| 568 | pub const ParentFileError = error{ |
| 569 | UnsupportedOperation, |
| 570 | EnvironmentVariableMissing, |
| 571 | UnrecognizedFormat, |
| 572 | }; |
| 573 | |
| 574 | pub 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. |
| 600 | pub 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 | |
| 684 | pub 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. |
| 690 | fn 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 | |
| 703 | const WorkerError = error{WindowTooSmall} || Io.ConcurrentError || Io.Cancelable || |
| 704 | Io.File.Writer.Error || Io.Operation.FileReadStreaming.Error; |
| 705 | |
| 706 | fn 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 | |
| 742 | const WindowsApiError = Io.Cancelable || Io.UnexpectedError; |
| 743 | |
| 744 | fn 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 | |
| 763 | fn 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 | |
| 802 | fn 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 | |
| 820 | const start_sync = "\x1b[?2026h"; |
| 821 | const up_one_line = "\x1bM"; |
| 822 | const clear = "\x1b[J"; |
| 823 | const save = "\x1b7"; |
| 824 | const restore = "\x1b8"; |
| 825 | const finish_sync = "\x1b[?2026l"; |
| 826 | |
| 827 | const progress_remove = "\x1b]9;4;0\x1b\\"; |
| 828 | const @"progress_normal {d}" = "\x1b]9;4;1;{d}\x1b\\"; |
| 829 | const @"progress_error {d}" = "\x1b]9;4;2;{d}\x1b\\"; |
| 830 | const progress_pulsing = "\x1b]9;4;3\x1b\\"; |
| 831 | const progress_pulsing_error = "\x1b]9;4;2\x1b\\"; |
| 832 | const progress_normal_100 = "\x1b]9;4;1;100\x1b\\"; |
| 833 | const progress_error_100 = "\x1b]9;4;2;100\x1b\\"; |
| 834 | |
| 835 | const 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 | |
| 890 | fn 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 | |
| 913 | pub 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 â–º |
| 920 | const windows_api_start_marker = 0x25BA; |
| 921 | |
| 922 | fn 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 | |
| 962 | fn 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 | |
| 1001 | const Children = struct { |
| 1002 | child: Node.OptionalIndex, |
| 1003 | sibling: Node.OptionalIndex, |
| 1004 | }; |
| 1005 | |
| 1006 | const 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 | |
| 1031 | fn 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 | |
| 1290 | fn 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 | |
| 1401 | fn 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 | |
| 1432 | fn 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 | |
| 1441 | fn 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 | |
| 1521 | fn 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 | |
| 1528 | fn 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 | |
| 1543 | fn 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 | |
| 1586 | fn 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 | |
| 1593 | const 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 | |
| 1615 | fn 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 | |
| 1629 | fn 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 | } |