aboutsummaryrefslogtreecommitdiff
path: root/lib/std/child_process.zig
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2023-03-03 12:08:18 -0700
committerAndrew Kelley <andrew@ziglang.org>2023-03-03 12:08:18 -0700
commitdf4cfc2ecf498bf4615ccbaa93438849322bbd18 (patch)
treea71611e86cacd8e021190cc4755574c514acb5c7 /lib/std/child_process.zig
parent72443fb88cfddad8a58868c150eaf5818826cb21 (diff)
parent75ff34db9e93056482233f8476a06f78b4a2f3c2 (diff)
downloadzig-df4cfc2ecf498bf4615ccbaa93438849322bbd18.tar.gz
zig-df4cfc2ecf498bf4615ccbaa93438849322bbd18.zip
Merge remote-tracking branch 'origin/master' into llvm16
Diffstat (limited to 'lib/std/child_process.zig')
-rw-r--r--lib/std/child_process.zig266
1 files changed, 63 insertions, 203 deletions
diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig
index c3bd53b880..dba92ab998 100644
--- a/lib/std/child_process.zig
+++ b/lib/std/child_process.zig
@@ -19,8 +19,15 @@ const maxInt = std.math.maxInt;
const assert = std.debug.assert;
pub const ChildProcess = struct {
- pid: if (builtin.os.tag == .windows) void else i32,
- handle: if (builtin.os.tag == .windows) windows.HANDLE else void,
+ pub const Id = switch (builtin.os.tag) {
+ .windows => windows.HANDLE,
+ else => os.pid_t,
+ };
+
+ /// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`.
+ /// On Windows this is the hProcess.
+ /// On POSIX this is the pid.
+ id: Id,
thread_handle: if (builtin.os.tag == .windows) windows.HANDLE else void,
allocator: mem.Allocator,
@@ -105,8 +112,7 @@ pub const ChildProcess = struct {
return .{
.allocator = allocator,
.argv = argv,
- .pid = undefined,
- .handle = undefined,
+ .id = undefined,
.thread_handle = undefined,
.err_pipe = null,
.term = null,
@@ -131,6 +137,7 @@ pub const ChildProcess = struct {
}
/// On success must call `kill` or `wait`.
+ /// After spawning the `id` is available.
pub fn spawn(self: *ChildProcess) SpawnError!void {
if (!std.process.can_spawn) {
@compileError("the target operating system cannot spawn processes");
@@ -167,7 +174,7 @@ pub const ChildProcess = struct {
return term;
}
- try windows.TerminateProcess(self.handle, exit_code);
+ try windows.TerminateProcess(self.id, exit_code);
try self.waitUnwrappedWindows();
return self.term.?;
}
@@ -177,18 +184,21 @@ pub const ChildProcess = struct {
self.cleanupStreams();
return term;
}
- try os.kill(self.pid, os.SIG.TERM);
+ try os.kill(self.id, os.SIG.TERM);
try self.waitUnwrapped();
return self.term.?;
}
/// Blocks until child process terminates and then cleans up all resources.
pub fn wait(self: *ChildProcess) !Term {
- if (builtin.os.tag == .windows) {
- return self.waitWindows();
- } else {
- return self.waitPosix();
- }
+ const term = if (builtin.os.tag == .windows)
+ try self.waitWindows()
+ else
+ try self.waitPosix();
+
+ self.id = undefined;
+
+ return term;
}
pub const ExecResult = struct {
@@ -197,6 +207,19 @@ pub const ChildProcess = struct {
stderr: []u8,
};
+ fn fifoToOwnedArrayList(fifo: *std.io.PollFifo) std.ArrayList(u8) {
+ if (fifo.head > 0) {
+ std.mem.copy(u8, fifo.buf[0..fifo.count], fifo.buf[fifo.head .. fifo.head + fifo.count]);
+ }
+ const result = std.ArrayList(u8){
+ .items = fifo.buf[0..fifo.count],
+ .capacity = fifo.buf.len,
+ .allocator = fifo.allocator,
+ };
+ fifo.* = std.io.PollFifo.init(fifo.allocator);
+ return result;
+ }
+
/// Collect the output from the process's stdout and stderr. Will return once all output
/// has been collected. This does not mean that the process has ended. `wait` should still
/// be called to wait for and clean up the process.
@@ -210,196 +233,33 @@ pub const ChildProcess = struct {
) !void {
debug.assert(child.stdout_behavior == .Pipe);
debug.assert(child.stderr_behavior == .Pipe);
- if (builtin.os.tag == .haiku) {
- const stdout_in = child.stdout.?.reader();
- const stderr_in = child.stderr.?.reader();
-
- try stdout_in.readAllArrayList(stdout, max_output_bytes);
- try stderr_in.readAllArrayList(stderr, max_output_bytes);
- } else if (builtin.os.tag == .windows) {
- try collectOutputWindows(child, stdout, stderr, max_output_bytes);
- } else {
- try collectOutputPosix(child, stdout, stderr, max_output_bytes);
- }
- }
- fn collectOutputPosix(
- child: ChildProcess,
- stdout: *std.ArrayList(u8),
- stderr: *std.ArrayList(u8),
- max_output_bytes: usize,
- ) !void {
- var poll_fds = [_]os.pollfd{
- .{ .fd = child.stdout.?.handle, .events = os.POLL.IN, .revents = undefined },
- .{ .fd = child.stderr.?.handle, .events = os.POLL.IN, .revents = undefined },
- };
-
- var dead_fds: usize = 0;
- // We ask for ensureTotalCapacity with this much extra space. This has more of an
- // effect on small reads because once the reads start to get larger the amount
- // of space an ArrayList will allocate grows exponentially.
- const bump_amt = 512;
-
- const err_mask = os.POLL.ERR | os.POLL.NVAL | os.POLL.HUP;
-
- while (dead_fds < poll_fds.len) {
- const events = try os.poll(&poll_fds, std.math.maxInt(i32));
- if (events == 0) continue;
-
- var remove_stdout = false;
- var remove_stderr = false;
- // Try reading whatever is available before checking the error
- // conditions.
- // It's still possible to read after a POLL.HUP is received, always
- // check if there's some data waiting to be read first.
- if (poll_fds[0].revents & os.POLL.IN != 0) {
- // stdout is ready.
- const new_capacity = std.math.min(stdout.items.len + bump_amt, max_output_bytes);
- try stdout.ensureTotalCapacity(new_capacity);
- const buf = stdout.unusedCapacitySlice();
- if (buf.len == 0) return error.StdoutStreamTooLong;
- const nread = try os.read(poll_fds[0].fd, buf);
- stdout.items.len += nread;
-
- // Remove the fd when the EOF condition is met.
- remove_stdout = nread == 0;
- } else {
- remove_stdout = poll_fds[0].revents & err_mask != 0;
- }
+ // we could make this work with multiple allocators but YAGNI
+ if (stdout.allocator.ptr != stderr.allocator.ptr or
+ stdout.allocator.vtable != stderr.allocator.vtable)
+ @panic("ChildProcess.collectOutput only supports 1 allocator");
- if (poll_fds[1].revents & os.POLL.IN != 0) {
- // stderr is ready.
- const new_capacity = std.math.min(stderr.items.len + bump_amt, max_output_bytes);
- try stderr.ensureTotalCapacity(new_capacity);
- const buf = stderr.unusedCapacitySlice();
- if (buf.len == 0) return error.StderrStreamTooLong;
- const nread = try os.read(poll_fds[1].fd, buf);
- stderr.items.len += nread;
-
- // Remove the fd when the EOF condition is met.
- remove_stderr = nread == 0;
- } else {
- remove_stderr = poll_fds[1].revents & err_mask != 0;
- }
+ var poller = std.io.poll(stdout.allocator, enum { stdout, stderr }, .{
+ .stdout = child.stdout.?,
+ .stderr = child.stderr.?,
+ });
+ defer poller.deinit();
- // Exclude the fds that signaled an error.
- if (remove_stdout) {
- poll_fds[0].fd = -1;
- dead_fds += 1;
- }
- if (remove_stderr) {
- poll_fds[1].fd = -1;
- dead_fds += 1;
- }
+ while (try poller.poll()) {
+ if (poller.fifo(.stdout).count > max_output_bytes)
+ return error.StdoutStreamTooLong;
+ if (poller.fifo(.stderr).count > max_output_bytes)
+ return error.StderrStreamTooLong;
}
- }
- const WindowsAsyncReadResult = enum {
- pending,
- closed,
- full,
- };
-
- fn windowsAsyncRead(
- handle: windows.HANDLE,
- overlapped: *windows.OVERLAPPED,
- buf: *std.ArrayList(u8),
- bump_amt: usize,
- max_output_bytes: usize,
- ) !WindowsAsyncReadResult {
- while (true) {
- const new_capacity = std.math.min(buf.items.len + bump_amt, max_output_bytes);
- try buf.ensureTotalCapacity(new_capacity);
- const next_buf = buf.unusedCapacitySlice();
- if (next_buf.len == 0) return .full;
- var read_bytes: u32 = undefined;
- const read_result = windows.kernel32.ReadFile(handle, next_buf.ptr, math.cast(u32, next_buf.len) orelse maxInt(u32), &read_bytes, overlapped);
- if (read_result == 0) return switch (windows.kernel32.GetLastError()) {
- .IO_PENDING => .pending,
- .BROKEN_PIPE => .closed,
- else => |err| windows.unexpectedError(err),
- };
- buf.items.len += read_bytes;
- }
+ stdout.* = fifoToOwnedArrayList(poller.fifo(.stdout));
+ stderr.* = fifoToOwnedArrayList(poller.fifo(.stderr));
}
- fn collectOutputWindows(child: ChildProcess, stdout: *std.ArrayList(u8), stderr: *std.ArrayList(u8), max_output_bytes: usize) !void {
- const bump_amt = 512;
- const outs = [_]*std.ArrayList(u8){
- stdout,
- stderr,
- };
- const handles = [_]windows.HANDLE{
- child.stdout.?.handle,
- child.stderr.?.handle,
- };
-
- var overlapped = [_]windows.OVERLAPPED{
- mem.zeroes(windows.OVERLAPPED),
- mem.zeroes(windows.OVERLAPPED),
- };
-
- var wait_objects: [2]windows.HANDLE = undefined;
- var wait_object_count: u2 = 0;
-
- // we need to cancel all pending IO before returning so our OVERLAPPED values don't go out of scope
- defer for (wait_objects[0..wait_object_count]) |o| {
- _ = windows.kernel32.CancelIo(o);
- };
-
- // Windows Async IO requires an initial call to ReadFile before waiting on the handle
- for ([_]u1{ 0, 1 }) |i| {
- switch (try windowsAsyncRead(handles[i], &overlapped[i], outs[i], bump_amt, max_output_bytes)) {
- .pending => {
- wait_objects[wait_object_count] = handles[i];
- wait_object_count += 1;
- },
- .closed => {}, // don't add to the wait_objects list
- .full => return if (i == 0) error.StdoutStreamTooLong else error.StderrStreamTooLong,
- }
- }
-
- while (wait_object_count > 0) {
- const status = windows.kernel32.WaitForMultipleObjects(wait_object_count, &wait_objects, 0, windows.INFINITE);
- if (status == windows.WAIT_FAILED) {
- switch (windows.kernel32.GetLastError()) {
- else => |err| return windows.unexpectedError(err),
- }
- }
- if (status < windows.WAIT_OBJECT_0 or status > windows.WAIT_OBJECT_0 + wait_object_count - 1)
- unreachable;
-
- const wait_idx = status - windows.WAIT_OBJECT_0;
-
- // this extra `i` index is needed to map the wait handle back to the stdout or stderr
- // values since the wait_idx can change which handle it corresponds with
- const i: u1 = if (wait_objects[wait_idx] == handles[0]) 0 else 1;
-
- // remove completed event from the wait list
- wait_object_count -= 1;
- if (wait_idx == 0)
- wait_objects[0] = wait_objects[1];
-
- var read_bytes: u32 = undefined;
- if (windows.kernel32.GetOverlappedResult(handles[i], &overlapped[i], &read_bytes, 0) == 0) {
- switch (windows.kernel32.GetLastError()) {
- .BROKEN_PIPE => continue,
- else => |err| return windows.unexpectedError(err),
- }
- }
-
- outs[i].items.len += read_bytes;
-
- switch (try windowsAsyncRead(handles[i], &overlapped[i], outs[i], bump_amt, max_output_bytes)) {
- .pending => {
- wait_objects[wait_object_count] = handles[i];
- wait_object_count += 1;
- },
- .closed => {}, // don't add to the wait_objects list
- .full => return if (i == 0) error.StdoutStreamTooLong else error.StderrStreamTooLong,
- }
- }
- }
+ pub const ExecError = os.GetCwdError || os.ReadError || SpawnError || os.PollError || error{
+ StdoutStreamTooLong,
+ StderrStreamTooLong,
+ };
/// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
/// If it succeeds, the caller owns result.stdout and result.stderr memory.
@@ -411,7 +271,7 @@ pub const ChildProcess = struct {
env_map: ?*const EnvMap = null,
max_output_bytes: usize = 50 * 1024,
expand_arg0: Arg0Expand = .no_expand,
- }) !ExecResult {
+ }) ExecError!ExecResult {
var child = ChildProcess.init(args.argv, args.allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Pipe;
@@ -459,18 +319,18 @@ pub const ChildProcess = struct {
}
fn waitUnwrappedWindows(self: *ChildProcess) !void {
- const result = windows.WaitForSingleObjectEx(self.handle, windows.INFINITE, false);
+ const result = windows.WaitForSingleObjectEx(self.id, windows.INFINITE, false);
self.term = @as(SpawnError!Term, x: {
var exit_code: windows.DWORD = undefined;
- if (windows.kernel32.GetExitCodeProcess(self.handle, &exit_code) == 0) {
+ if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) {
break :x Term{ .Unknown = 0 };
} else {
break :x Term{ .Exited = @truncate(u8, exit_code) };
}
});
- os.close(self.handle);
+ os.close(self.id);
os.close(self.thread_handle);
self.cleanupStreams();
return result;
@@ -478,9 +338,9 @@ pub const ChildProcess = struct {
fn waitUnwrapped(self: *ChildProcess) !void {
const res: os.WaitPidResult = if (comptime builtin.target.isDarwin())
- try os.posix_spawn.waitpid(self.pid, 0)
+ try os.posix_spawn.waitpid(self.id, 0)
else
- os.waitpid(self.pid, 0);
+ os.waitpid(self.id, 0);
const status = res.status;
self.cleanupStreams();
self.handleWaitResult(status);
@@ -638,7 +498,7 @@ pub const ChildProcess = struct {
self.stderr = null;
}
- self.pid = pid;
+ self.id = pid;
self.term = null;
if (self.stdin_behavior == StdIo.Pipe) {
@@ -812,7 +672,7 @@ pub const ChildProcess = struct {
self.stderr = null;
}
- self.pid = pid;
+ self.id = pid;
self.err_pipe = err_pipe;
self.term = null;
@@ -1078,7 +938,7 @@ pub const ChildProcess = struct {
self.stderr = null;
}
- self.handle = piProcInfo.hProcess;
+ self.id = piProcInfo.hProcess;
self.thread_handle = piProcInfo.hThread;
self.term = null;