authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-28 17:43:42-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-30 12:10:02-08:00
logfe15534cd5464f2ba81b68aa00b6e89b39180bef
tree8665b22f5f4e6d130229427075eb331bd048188e
parent0fd8263e970fd716a47d023d877ce8544b33b2f2

std.process.run: use Io.File.MultiReader

and delete the special-cased function

2 files changed, 30 insertions(+), 93 deletions(-)

lib/std/process.zig+30-23
......@@ -453,16 +453,16 @@ pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child {
453453 return io.vtable.processSpawnPath(io.userdata, dir, options);
454454}
455455
456pub const RunError = SpawnError || Child.CollectOutputError;
456pub const RunError = SpawnError || error{
457 StreamTooLong,
458} || Io.ConcurrentError || Allocator.Error || Io.File.Reader.Error || Io.Timeout.Error;
457459
458460pub const RunOptions = struct {
459461 argv: []const []const u8,
460462 stderr_limit: Io.Limit = .unlimited,
461463 stdout_limit: Io.Limit = .unlimited,
462 /// How many bytes to initially allocate for stderr.
463 stderr_reserve_amount: usize = 1,
464 /// How many bytes to initially allocate for stdout.
465 stdout_reserve_amount: usize = 1,
464 /// How many bytes to initially allocate for stderr and stdout.
465 reserve_amount: usize = 64,
466466
467467 /// Set to change the current working directory when spawning the child process.
468468 cwd: ?[]const u8 = null,
......@@ -516,29 +516,36 @@ pub fn run(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult {
516516 });
517517 defer child.kill(io);
518518
519 var stdout: std.ArrayList(u8) = .empty;
520 defer stdout.deinit(gpa);
521 var stderr: std.ArrayList(u8) = .empty;
522 defer stderr.deinit(gpa);
523
524 try stdout.ensureUnusedCapacity(gpa, options.stdout_reserve_amount);
525 try stderr.ensureUnusedCapacity(gpa, options.stderr_reserve_amount);
526
527 try child.collectOutput(io, .{
528 .allocator = gpa,
529 .stdout = &stdout,
530 .stderr = &stderr,
531 .stdout_limit = options.stdout_limit,
532 .stderr_limit = options.stderr_limit,
533 .timeout = options.timeout,
534 });
519 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
520 var multi_reader: Io.File.MultiReader = undefined;
521 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
522 defer multi_reader.deinit();
523
524 const stdout_reader = multi_reader.reader(0);
525 const stderr_reader = multi_reader.reader(1);
526
527 while (multi_reader.fill(options.reserve_amount, options.timeout)) |_| {
528 if (options.stdout_limit.toInt()) |limit| {
529 if (stdout_reader.buffered().len > limit)
530 return error.StreamTooLong;
531 }
532 if (options.stderr_limit.toInt()) |limit| {
533 if (stderr_reader.buffered().len > limit)
534 return error.StreamTooLong;
535 }
536 } else |err| switch (err) {
537 error.EndOfStream => {},
538 else => |e| return e,
539 }
540
541 try multi_reader.checkAnyError();
535542
536543 const term = try child.wait(io);
537544
538 const stdout_slice = try stdout.toOwnedSlice(gpa);
545 const stdout_slice = try multi_reader.toOwnedSlice(0);
539546 errdefer gpa.free(stdout_slice);
540547
541 const stderr_slice = try stderr.toOwnedSlice(gpa);
548 const stderr_slice = try multi_reader.toOwnedSlice(1);
542549 errdefer gpa.free(stderr_slice);
543550
544551 return .{
lib/std/process/Child.zig-70
......@@ -124,73 +124,3 @@ pub fn wait(child: *Child, io: Io) WaitError!Term {
124124 assert(child.id != null);
125125 return io.vtable.childWait(io.userdata, child);
126126}
127
128pub const CollectOutputError = error{
129 StreamTooLong,
130} || Io.ConcurrentError || Allocator.Error || Io.File.Reader.Error || Io.Timeout.Error;
131
132pub const CollectOutputOptions = struct {
133 stdout: *std.ArrayList(u8),
134 stderr: *std.ArrayList(u8),
135 /// Used for `stdout` and `stderr`. If not provided, only the existing
136 /// capacity will be used.
137 allocator: ?Allocator = null,
138 stdout_limit: Io.Limit = .unlimited,
139 stderr_limit: Io.Limit = .unlimited,
140 timeout: Io.Timeout = .none,
141};
142
143/// Collect the output from the process's stdout and stderr. Will return once
144/// all output has been collected. This does not mean that the process has
145/// ended. `wait` should still be called to wait for and clean up the process.
146///
147/// The process must have been started with stdout and stderr set to
148/// `process.SpawnOptions.StdIo.pipe`.
149pub fn collectOutput(child: *const Child, io: Io, options: CollectOutputOptions) CollectOutputError!void {
150 const files: [2]Io.File = .{ child.stdout.?, child.stderr.? };
151 const lists: [2]*std.ArrayList(u8) = .{ options.stdout, options.stderr };
152 const limits: [2]Io.Limit = .{ options.stdout_limit, options.stderr_limit };
153 var reads: [2]Io.Operation = undefined;
154 var vecs: [2][1][]u8 = undefined;
155 var ring: [2]u32 = undefined;
156 var batch: Io.Batch = .init(&reads, &ring);
157 defer {
158 batch.cancel(io);
159 while (batch.next()) |op| {
160 lists[op].items.len += reads[op].file_read_streaming.status.result catch continue;
161 }
162 }
163 var remaining: usize = 0;
164 for (0.., &reads, &lists, &files, &vecs) |op, *read, list, file, *vec| {
165 if (options.allocator) |gpa| try list.ensureUnusedCapacity(gpa, 1);
166 const cap = list.unusedCapacitySlice();
167 if (cap.len == 0) return error.StreamTooLong;
168 vec[0] = cap;
169 read.* = .{ .file_read_streaming = .{
170 .file = file,
171 .data = vec,
172 } };
173 batch.add(op);
174 remaining += 1;
175 }
176 while (remaining > 0) {
177 try batch.wait(io, options.timeout);
178 while (batch.next()) |op| {
179 const n = reads[op].file_read_streaming.status.result catch |err| switch (err) {
180 error.EndOfStream => {
181 remaining -= 1;
182 continue;
183 },
184 else => |e| return e,
185 };
186 lists[op].items.len += n;
187 if (lists[op].items.len > @intFromEnum(limits[op])) return error.StreamTooLong;
188 if (options.allocator) |gpa| try lists[op].ensureUnusedCapacity(gpa, 1);
189 const cap = lists[op].unusedCapacitySlice();
190 if (cap.len == 0) return error.StreamTooLong;
191 vecs[op][0] = cap;
192 reads[op].file_read_streaming.status = .{ .unstarted = {} };
193 batch.add(op);
194 }
195 }
196}