feature. See also
. The project being documented here (as the example) is the Zig library itself.
Threaded.spawnPosix
fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Spawned
File
Code
fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Spawned {
// we must initially set CLOEXEC to avoid a race condition. If another thread
// is racing to spawn a different child process, we don't want it to inherit
// these FDs in any scenario; that would mean that, for instance, calls to
// `poll` from the parent would not report the child's stdout as closing when
// expected, since the other child may retain a reference to the write end of
// the pipe. So, we create the pipes with CLOEXEC initially. After fork, we
// need to do something in the new child to make sure we preserve the reference
// we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it
// turns out, we `dup2` everything anyway, so there's no need!
const pipe_flags: posix.O = .{ .CLOEXEC = true };
const stdin_pipe = if (options.stdin == .pipe) try pipe2(pipe_flags) else undefined;
errdefer if (options.stdin == .pipe) {
destroyPipe(stdin_pipe);
};
const stdout_pipe = if (options.stdout == .pipe) try pipe2(pipe_flags) else undefined;
errdefer if (options.stdout == .pipe) {
destroyPipe(stdout_pipe);
};
const stderr_pipe = if (options.stderr == .pipe) try pipe2(pipe_flags) else undefined;
errdefer if (options.stderr == .pipe) {
destroyPipe(stderr_pipe);
};
const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore);
const dev_null_fd = if (any_ignore) try getDevNullFd(t) else undefined;
const prog_pipe: [2]posix.fd_t = if (options.progress_node.index != .none) pipe: {
const pipe = try pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
switch (native_os) {
.linux => _ = posix.system.fcntl(pipe[0], posix.F.SETPIPE_SZ, @as(u32, std.Progress.max_packet_len * 2)),
else => {},
}
break :pipe pipe;
} else .{ -1, -1 };
errdefer destroyPipe(prog_pipe);
var arena_allocator = std.heap.ArenaAllocator.init(t.allocator);
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
// and this allocator may be a libc allocator.
// I have personally observed the child process deadlocking when it tries
// to call malloc() due to a heap allocation between fork() and execve(),
// in musl v1.1.24.
// Additionally, we want to reduce the number of possible ways things
// can fail between fork() and execve().
// Therefore, we do all the allocation for the execve() before the fork().
// This means we must do the null-termination of argv and env vars here.
const argv_buf = try arena.allocSentinel(?[*:0]const u8, options.argv.len, null);
for (options.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeSentinel(u8, arg, 0)).ptr;
const prog_fileno = 3;
comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno);
const env_block = env_block: {
const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno;
if (options.environ_map) |environ_map| break :env_block try environ_map.createPosixBlock(arena, .{
.zig_progress_fd = prog_fd,
});
break :env_block try t.environ.process_environ.createPosixBlock(arena, .{
.zig_progress_fd = prog_fd,
});
};
// It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.
const err_pipe = try pipe2(.{ .CLOEXEC = true });
errdefer destroyPipe(err_pipe);
t.scanEnviron();
const PATH = t.environ.string.PATH orelse default_PATH;
const pid_result: posix.pid_t = fork: {
const rc = posix.system.fork();
switch (posix.errno(rc)) {
.SUCCESS => break :fork @intCast(rc),
.AGAIN => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOSYS => return error.OperationUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
};
if (pid_result == 0) {
defer comptime unreachable;
if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked;
const ep1 = err_pipe[1];
setUpChildIo(options.stdin, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkBail(ep1, err);
setUpChildIo(options.stdout, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkBail(ep1, err);
setUpChildIo(options.stderr, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkBail(ep1, err);
switch (options.cwd) {
.inherit => {},
.dir => |cwd| {
fchdir(cwd.handle) catch |err| forkBail(ep1, err);
},
.path => |cwd| {
chdir(cwd) catch |err| forkBail(ep1, err);
},
}
// equal to prog_fileno and be clobbered by this dup2 call.
if (prog_pipe[1] != -1) dup2(prog_pipe[1], prog_fileno) catch |err| forkBail(ep1, err);
if (options.gid) |gid| {
switch (posix.errno(posix.system.setregid(gid, gid))) {
.SUCCESS => {},
.AGAIN => forkBail(ep1, error.ResourceLimitReached),
.INVAL => forkBail(ep1, error.InvalidUserId),
.PERM => forkBail(ep1, error.PermissionDenied),
else => forkBail(ep1, error.Unexpected),
}
}
if (options.uid) |uid| {
switch (posix.errno(posix.system.setreuid(uid, uid))) {
.SUCCESS => {},
.AGAIN => forkBail(ep1, error.ResourceLimitReached),
.INVAL => forkBail(ep1, error.InvalidUserId),
.PERM => forkBail(ep1, error.PermissionDenied),
else => forkBail(ep1, error.Unexpected),
}
}
if (options.pgid) |pid| {
switch (posix.errno(posix.system.setpgid(0, pid))) {
.SUCCESS => {},
.ACCES => forkBail(ep1, error.ProcessAlreadyExec),
.INVAL => forkBail(ep1, error.InvalidProcessGroupId),
.PERM => forkBail(ep1, error.PermissionDenied),
else => forkBail(ep1, error.Unexpected),
}
}
if (options.start_suspended) {
switch (posix.errno(posix.system.kill(0, .STOP))) {
.SUCCESS => {},
.PERM => forkBail(ep1, error.PermissionDenied),
else => forkBail(ep1, error.Unexpected),
}
}
const err = posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, env_block, PATH);
forkBail(ep1, err);
}
const pid: posix.pid_t = @intCast(pid_result);
errdefer comptime unreachable;
closeFd(err_pipe[1]);
if (options.stdin == .pipe) closeFd(stdin_pipe[0]);
if (options.stdout == .pipe) closeFd(stdout_pipe[1]);
if (options.stderr == .pipe) closeFd(stderr_pipe[1]);
if (prog_pipe[1] != -1) closeFd(prog_pipe[1]);
options.progress_node.setIpcFile(t, .{ .handle = prog_pipe[0], .flags = .{ .nonblocking = true } });
return .{
.pid = pid,
.err_fd = err_pipe[0],
.stdin = switch (options.stdin) {
.pipe => .{ .handle = stdin_pipe[1], .flags = .{ .nonblocking = false } },
else => null,
},
.stdout = switch (options.stdout) {
.pipe => .{ .handle = stdout_pipe[0], .flags = .{ .nonblocking = false } },
else => null,
},
.stderr = switch (options.stderr) {
.pipe => .{ .handle = stderr_pipe[0], .flags = .{ .nonblocking = false } },
else => null,
},
};
}