mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
When linking libc and compiling natively, Zig tries to integrate with the system C compiler. However, this caused Zig to fail when no system C compiler is installed, despite the fact that Zig is perfectly capable of compiling & linking libc without one. This commit makes Zig fall back to using its own ability to provide libc in the case that no C compiler is installed. For glibc, it means sometimes getting the warning "zig cannot build new glibc version abc, providing instead xyz". Ideally, Zig would do some more validation about the system libraries being linked against, and report an error in case it could not provide the exact correct libc version of the system libraries (or that the system libraries themselves conflict with each other), however, I think it is fair to call that a separate enhancement.
1326 lines
49 KiB
Zig
1326 lines
49 KiB
Zig
const std = @import("std.zig");
|
|
const builtin = @import("builtin");
|
|
const cstr = std.cstr;
|
|
const unicode = std.unicode;
|
|
const io = std.io;
|
|
const fs = std.fs;
|
|
const os = std.os;
|
|
const process = std.process;
|
|
const File = std.fs.File;
|
|
const windows = os.windows;
|
|
const linux = os.linux;
|
|
const mem = std.mem;
|
|
const math = std.math;
|
|
const debug = std.debug;
|
|
const EnvMap = process.EnvMap;
|
|
const Os = std.builtin.Os;
|
|
const TailQueue = std.TailQueue;
|
|
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,
|
|
thread_handle: if (builtin.os.tag == .windows) windows.HANDLE else void,
|
|
|
|
allocator: mem.Allocator,
|
|
|
|
stdin: ?File,
|
|
stdout: ?File,
|
|
stderr: ?File,
|
|
|
|
term: ?(SpawnError!Term),
|
|
|
|
argv: []const []const u8,
|
|
|
|
/// Leave as null to use the current env map using the supplied allocator.
|
|
env_map: ?*const EnvMap,
|
|
|
|
stdin_behavior: StdIo,
|
|
stdout_behavior: StdIo,
|
|
stderr_behavior: StdIo,
|
|
|
|
/// Set to change the user id when spawning the child process.
|
|
uid: if (builtin.os.tag == .windows or builtin.os.tag == .wasi) void else ?os.uid_t,
|
|
|
|
/// Set to change the group id when spawning the child process.
|
|
gid: if (builtin.os.tag == .windows or builtin.os.tag == .wasi) void else ?os.gid_t,
|
|
|
|
/// Set to change the current working directory when spawning the child process.
|
|
cwd: ?[]const u8,
|
|
/// Set to change the current working directory when spawning the child process.
|
|
/// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190
|
|
/// Once that is done, `cwd` will be deprecated in favor of this field.
|
|
cwd_dir: ?fs.Dir = null,
|
|
|
|
err_pipe: ?if (builtin.os.tag == .windows) void else [2]os.fd_t,
|
|
|
|
expand_arg0: Arg0Expand,
|
|
|
|
/// Darwin-only. Disable ASLR for the child process.
|
|
disable_aslr: bool = false,
|
|
|
|
pub const Arg0Expand = os.Arg0Expand;
|
|
|
|
pub const SpawnError = error{
|
|
OutOfMemory,
|
|
|
|
/// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV.
|
|
NoDevice,
|
|
|
|
/// Windows-only. One of:
|
|
/// * `cwd` was provided and it could not be re-encoded into UTF16LE, or
|
|
/// * The `PATH` or `PATHEXT` environment variable contained invalid UTF-8.
|
|
InvalidUtf8,
|
|
|
|
/// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process.
|
|
CurrentWorkingDirectoryUnlinked,
|
|
} ||
|
|
os.ExecveError ||
|
|
os.SetIdError ||
|
|
os.ChangeCurDirError ||
|
|
windows.CreateProcessError ||
|
|
windows.WaitForSingleObjectError ||
|
|
os.posix_spawn.Error;
|
|
|
|
pub const Term = union(enum) {
|
|
Exited: u8,
|
|
Signal: u32,
|
|
Stopped: u32,
|
|
Unknown: u32,
|
|
};
|
|
|
|
pub const StdIo = enum {
|
|
Inherit,
|
|
Ignore,
|
|
Pipe,
|
|
Close,
|
|
};
|
|
|
|
/// First argument in argv is the executable.
|
|
pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
|
|
return .{
|
|
.allocator = allocator,
|
|
.argv = argv,
|
|
.pid = undefined,
|
|
.handle = undefined,
|
|
.thread_handle = undefined,
|
|
.err_pipe = null,
|
|
.term = null,
|
|
.env_map = null,
|
|
.cwd = null,
|
|
.uid = if (builtin.os.tag == .windows or builtin.os.tag == .wasi) {} else null,
|
|
.gid = if (builtin.os.tag == .windows or builtin.os.tag == .wasi) {} else null,
|
|
.stdin = null,
|
|
.stdout = null,
|
|
.stderr = null,
|
|
.stdin_behavior = StdIo.Inherit,
|
|
.stdout_behavior = StdIo.Inherit,
|
|
.stderr_behavior = StdIo.Inherit,
|
|
.expand_arg0 = .no_expand,
|
|
};
|
|
}
|
|
|
|
pub fn setUserName(self: *ChildProcess, name: []const u8) !void {
|
|
const user_info = try os.getUserInfo(name);
|
|
self.uid = user_info.uid;
|
|
self.gid = user_info.gid;
|
|
}
|
|
|
|
/// On success must call `kill` or `wait`.
|
|
pub fn spawn(self: *ChildProcess) SpawnError!void {
|
|
if (!std.process.can_spawn) {
|
|
@compileError("the target operating system cannot spawn processes");
|
|
}
|
|
|
|
if (comptime builtin.target.isDarwin()) {
|
|
return self.spawnMacos();
|
|
}
|
|
|
|
if (builtin.os.tag == .windows) {
|
|
return self.spawnWindows();
|
|
} else {
|
|
return self.spawnPosix();
|
|
}
|
|
}
|
|
|
|
pub fn spawnAndWait(self: *ChildProcess) SpawnError!Term {
|
|
try self.spawn();
|
|
return self.wait();
|
|
}
|
|
|
|
/// Forcibly terminates child process and then cleans up all resources.
|
|
pub fn kill(self: *ChildProcess) !Term {
|
|
if (builtin.os.tag == .windows) {
|
|
return self.killWindows(1);
|
|
} else {
|
|
return self.killPosix();
|
|
}
|
|
}
|
|
|
|
pub fn killWindows(self: *ChildProcess, exit_code: windows.UINT) !Term {
|
|
if (self.term) |term| {
|
|
self.cleanupStreams();
|
|
return term;
|
|
}
|
|
|
|
try windows.TerminateProcess(self.handle, exit_code);
|
|
try self.waitUnwrappedWindows();
|
|
return self.term.?;
|
|
}
|
|
|
|
pub fn killPosix(self: *ChildProcess) !Term {
|
|
if (self.term) |term| {
|
|
self.cleanupStreams();
|
|
return term;
|
|
}
|
|
try os.kill(self.pid, 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();
|
|
}
|
|
}
|
|
|
|
pub const ExecResult = struct {
|
|
term: Term,
|
|
stdout: []u8,
|
|
stderr: []u8,
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
fn collectOutputWindows(child: ChildProcess, outs: [2]*std.ArrayList(u8), max_output_bytes: usize) !void {
|
|
const bump_amt = 512;
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
pub fn exec(args: struct {
|
|
allocator: mem.Allocator,
|
|
argv: []const []const u8,
|
|
cwd: ?[]const u8 = null,
|
|
cwd_dir: ?fs.Dir = null,
|
|
env_map: ?*const EnvMap = null,
|
|
max_output_bytes: usize = 50 * 1024,
|
|
expand_arg0: Arg0Expand = .no_expand,
|
|
}) !ExecResult {
|
|
var child = ChildProcess.init(args.argv, args.allocator);
|
|
child.stdin_behavior = .Ignore;
|
|
child.stdout_behavior = .Pipe;
|
|
child.stderr_behavior = .Pipe;
|
|
child.cwd = args.cwd;
|
|
child.cwd_dir = args.cwd_dir;
|
|
child.env_map = args.env_map;
|
|
child.expand_arg0 = args.expand_arg0;
|
|
|
|
try child.spawn();
|
|
|
|
if (builtin.os.tag == .haiku) {
|
|
const stdout_in = child.stdout.?.reader();
|
|
const stderr_in = child.stderr.?.reader();
|
|
|
|
const stdout = try stdout_in.readAllAlloc(args.allocator, args.max_output_bytes);
|
|
errdefer args.allocator.free(stdout);
|
|
const stderr = try stderr_in.readAllAlloc(args.allocator, args.max_output_bytes);
|
|
errdefer args.allocator.free(stderr);
|
|
|
|
return ExecResult{
|
|
.term = try child.wait(),
|
|
.stdout = stdout,
|
|
.stderr = stderr,
|
|
};
|
|
}
|
|
|
|
var stdout = std.ArrayList(u8).init(args.allocator);
|
|
var stderr = std.ArrayList(u8).init(args.allocator);
|
|
errdefer {
|
|
stdout.deinit();
|
|
stderr.deinit();
|
|
}
|
|
|
|
if (builtin.os.tag == .windows) {
|
|
try collectOutputWindows(child, [_]*std.ArrayList(u8){ &stdout, &stderr }, args.max_output_bytes);
|
|
} else {
|
|
try collectOutputPosix(child, &stdout, &stderr, args.max_output_bytes);
|
|
}
|
|
|
|
return ExecResult{
|
|
.term = try child.wait(),
|
|
.stdout = stdout.toOwnedSlice(),
|
|
.stderr = stderr.toOwnedSlice(),
|
|
};
|
|
}
|
|
|
|
fn waitWindows(self: *ChildProcess) !Term {
|
|
if (self.term) |term| {
|
|
self.cleanupStreams();
|
|
return term;
|
|
}
|
|
|
|
try self.waitUnwrappedWindows();
|
|
return self.term.?;
|
|
}
|
|
|
|
fn waitPosix(self: *ChildProcess) !Term {
|
|
if (self.term) |term| {
|
|
self.cleanupStreams();
|
|
return term;
|
|
}
|
|
|
|
try self.waitUnwrapped();
|
|
return self.term.?;
|
|
}
|
|
|
|
fn waitUnwrappedWindows(self: *ChildProcess) !void {
|
|
const result = windows.WaitForSingleObjectEx(self.handle, windows.INFINITE, false);
|
|
|
|
self.term = @as(SpawnError!Term, x: {
|
|
var exit_code: windows.DWORD = undefined;
|
|
if (windows.kernel32.GetExitCodeProcess(self.handle, &exit_code) == 0) {
|
|
break :x Term{ .Unknown = 0 };
|
|
} else {
|
|
break :x Term{ .Exited = @truncate(u8, exit_code) };
|
|
}
|
|
});
|
|
|
|
os.close(self.handle);
|
|
os.close(self.thread_handle);
|
|
self.cleanupStreams();
|
|
return result;
|
|
}
|
|
|
|
fn waitUnwrapped(self: *ChildProcess) !void {
|
|
const res: os.WaitPidResult = if (comptime builtin.target.isDarwin())
|
|
try os.posix_spawn.waitpid(self.pid, 0)
|
|
else
|
|
os.waitpid(self.pid, 0);
|
|
const status = res.status;
|
|
self.cleanupStreams();
|
|
self.handleWaitResult(status);
|
|
}
|
|
|
|
fn handleWaitResult(self: *ChildProcess, status: u32) void {
|
|
self.term = self.cleanupAfterWait(status);
|
|
}
|
|
|
|
fn cleanupStreams(self: *ChildProcess) void {
|
|
if (self.stdin) |*stdin| {
|
|
stdin.close();
|
|
self.stdin = null;
|
|
}
|
|
if (self.stdout) |*stdout| {
|
|
stdout.close();
|
|
self.stdout = null;
|
|
}
|
|
if (self.stderr) |*stderr| {
|
|
stderr.close();
|
|
self.stderr = null;
|
|
}
|
|
}
|
|
|
|
fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term {
|
|
if (self.err_pipe) |err_pipe| {
|
|
defer destroyPipe(err_pipe);
|
|
|
|
if (builtin.os.tag == .linux) {
|
|
var fd = [1]std.os.pollfd{std.os.pollfd{
|
|
.fd = err_pipe[0],
|
|
.events = std.os.POLL.IN,
|
|
.revents = undefined,
|
|
}};
|
|
|
|
// Check if the eventfd buffer stores a non-zero value by polling
|
|
// it, that's the error code returned by the child process.
|
|
_ = std.os.poll(&fd, 0) catch unreachable;
|
|
|
|
// According to eventfd(2) the descriptor is readable if the counter
|
|
// has a value greater than 0
|
|
if ((fd[0].revents & std.os.POLL.IN) != 0) {
|
|
const err_int = try readIntFd(err_pipe[0]);
|
|
return @errSetCast(SpawnError, @intToError(err_int));
|
|
}
|
|
} else {
|
|
// Write maxInt(ErrInt) to the write end of the err_pipe. This is after
|
|
// waitpid, so this write is guaranteed to be after the child
|
|
// pid potentially wrote an error. This way we can do a blocking
|
|
// read on the error pipe and either get maxInt(ErrInt) (no error) or
|
|
// an error code.
|
|
try writeIntFd(err_pipe[1], maxInt(ErrInt));
|
|
const err_int = try readIntFd(err_pipe[0]);
|
|
// Here we potentially return the fork child's error from the parent
|
|
// pid.
|
|
if (err_int != maxInt(ErrInt)) {
|
|
return @errSetCast(SpawnError, @intToError(err_int));
|
|
}
|
|
}
|
|
}
|
|
|
|
return statusToTerm(status);
|
|
}
|
|
|
|
fn statusToTerm(status: u32) Term {
|
|
return if (os.W.IFEXITED(status))
|
|
Term{ .Exited = os.W.EXITSTATUS(status) }
|
|
else if (os.W.IFSIGNALED(status))
|
|
Term{ .Signal = os.W.TERMSIG(status) }
|
|
else if (os.W.IFSTOPPED(status))
|
|
Term{ .Stopped = os.W.STOPSIG(status) }
|
|
else
|
|
Term{ .Unknown = status };
|
|
}
|
|
|
|
fn spawnMacos(self: *ChildProcess) SpawnError!void {
|
|
const pipe_flags = if (io.is_async) os.O.NONBLOCK else 0;
|
|
const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined;
|
|
errdefer if (self.stdin_behavior == StdIo.Pipe) destroyPipe(stdin_pipe);
|
|
|
|
const stdout_pipe = if (self.stdout_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined;
|
|
errdefer if (self.stdout_behavior == StdIo.Pipe) destroyPipe(stdout_pipe);
|
|
|
|
const stderr_pipe = if (self.stderr_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined;
|
|
errdefer if (self.stderr_behavior == StdIo.Pipe) destroyPipe(stderr_pipe);
|
|
|
|
const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore);
|
|
const dev_null_fd = if (any_ignore)
|
|
os.openZ("/dev/null", os.O.RDWR, 0) catch |err| switch (err) {
|
|
error.PathAlreadyExists => unreachable,
|
|
error.NoSpaceLeft => unreachable,
|
|
error.FileTooBig => unreachable,
|
|
error.DeviceBusy => unreachable,
|
|
error.FileLocksNotSupported => unreachable,
|
|
error.BadPathName => unreachable, // Windows-only
|
|
error.InvalidHandle => unreachable, // WASI-only
|
|
error.WouldBlock => unreachable,
|
|
else => |e| return e,
|
|
}
|
|
else
|
|
undefined;
|
|
defer if (any_ignore) os.close(dev_null_fd);
|
|
|
|
var attr = try os.posix_spawn.Attr.init();
|
|
defer attr.deinit();
|
|
var flags: u16 = os.darwin.POSIX_SPAWN_SETSIGDEF | os.darwin.POSIX_SPAWN_SETSIGMASK;
|
|
if (self.disable_aslr) {
|
|
flags |= os.darwin._POSIX_SPAWN_DISABLE_ASLR;
|
|
}
|
|
try attr.set(flags);
|
|
|
|
var actions = try os.posix_spawn.Actions.init();
|
|
defer actions.deinit();
|
|
|
|
try setUpChildIoPosixSpawn(self.stdin_behavior, &actions, stdin_pipe, os.STDIN_FILENO, dev_null_fd);
|
|
try setUpChildIoPosixSpawn(self.stdout_behavior, &actions, stdout_pipe, os.STDOUT_FILENO, dev_null_fd);
|
|
try setUpChildIoPosixSpawn(self.stderr_behavior, &actions, stderr_pipe, os.STDERR_FILENO, dev_null_fd);
|
|
|
|
if (self.cwd_dir) |cwd| {
|
|
try actions.fchdir(cwd.fd);
|
|
} else if (self.cwd) |cwd| {
|
|
try actions.chdir(cwd);
|
|
}
|
|
|
|
var arena_allocator = std.heap.ArenaAllocator.init(self.allocator);
|
|
defer arena_allocator.deinit();
|
|
const arena = arena_allocator.allocator();
|
|
|
|
const argv_buf = try arena.allocSentinel(?[*:0]u8, self.argv.len, null);
|
|
for (self.argv) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
|
|
|
|
const envp = if (self.env_map) |env_map| m: {
|
|
const envp_buf = try createNullDelimitedEnvMap(arena, env_map);
|
|
break :m envp_buf.ptr;
|
|
} else std.c.environ;
|
|
|
|
const pid = try os.posix_spawn.spawnp(self.argv[0], actions, attr, argv_buf, envp);
|
|
|
|
if (self.stdin_behavior == StdIo.Pipe) {
|
|
self.stdin = File{ .handle = stdin_pipe[1] };
|
|
} else {
|
|
self.stdin = null;
|
|
}
|
|
if (self.stdout_behavior == StdIo.Pipe) {
|
|
self.stdout = File{ .handle = stdout_pipe[0] };
|
|
} else {
|
|
self.stdout = null;
|
|
}
|
|
if (self.stderr_behavior == StdIo.Pipe) {
|
|
self.stderr = File{ .handle = stderr_pipe[0] };
|
|
} else {
|
|
self.stderr = null;
|
|
}
|
|
|
|
self.pid = pid;
|
|
self.term = null;
|
|
|
|
if (self.stdin_behavior == StdIo.Pipe) {
|
|
os.close(stdin_pipe[0]);
|
|
}
|
|
if (self.stdout_behavior == StdIo.Pipe) {
|
|
os.close(stdout_pipe[1]);
|
|
}
|
|
if (self.stderr_behavior == StdIo.Pipe) {
|
|
os.close(stderr_pipe[1]);
|
|
}
|
|
}
|
|
|
|
fn setUpChildIoPosixSpawn(
|
|
stdio: StdIo,
|
|
actions: *os.posix_spawn.Actions,
|
|
pipe_fd: [2]i32,
|
|
std_fileno: i32,
|
|
dev_null_fd: i32,
|
|
) !void {
|
|
switch (stdio) {
|
|
.Pipe => {
|
|
const idx: usize = if (std_fileno == 0) 0 else 1;
|
|
try actions.dup2(pipe_fd[idx], std_fileno);
|
|
try actions.close(pipe_fd[1 - idx]);
|
|
},
|
|
.Close => try actions.close(std_fileno),
|
|
.Inherit => {},
|
|
.Ignore => try actions.dup2(dev_null_fd, std_fileno),
|
|
}
|
|
}
|
|
|
|
fn spawnPosix(self: *ChildProcess) SpawnError!void {
|
|
const pipe_flags = if (io.is_async) os.O.NONBLOCK else 0;
|
|
const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined;
|
|
errdefer if (self.stdin_behavior == StdIo.Pipe) {
|
|
destroyPipe(stdin_pipe);
|
|
};
|
|
|
|
const stdout_pipe = if (self.stdout_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined;
|
|
errdefer if (self.stdout_behavior == StdIo.Pipe) {
|
|
destroyPipe(stdout_pipe);
|
|
};
|
|
|
|
const stderr_pipe = if (self.stderr_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined;
|
|
errdefer if (self.stderr_behavior == StdIo.Pipe) {
|
|
destroyPipe(stderr_pipe);
|
|
};
|
|
|
|
const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore);
|
|
const dev_null_fd = if (any_ignore)
|
|
os.openZ("/dev/null", os.O.RDWR, 0) catch |err| switch (err) {
|
|
error.PathAlreadyExists => unreachable,
|
|
error.NoSpaceLeft => unreachable,
|
|
error.FileTooBig => unreachable,
|
|
error.DeviceBusy => unreachable,
|
|
error.FileLocksNotSupported => unreachable,
|
|
error.BadPathName => unreachable, // Windows-only
|
|
error.InvalidHandle => unreachable, // WASI-only
|
|
error.WouldBlock => unreachable,
|
|
else => |e| return e,
|
|
}
|
|
else
|
|
undefined;
|
|
defer {
|
|
if (any_ignore) os.close(dev_null_fd);
|
|
}
|
|
|
|
var arena_allocator = std.heap.ArenaAllocator.init(self.allocator);
|
|
defer arena_allocator.deinit();
|
|
const arena = arena_allocator.allocator();
|
|
|
|
// The POSIX standard does not allow malloc() between fork() and execve(),
|
|
// and `self.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]u8, self.argv.len, null);
|
|
for (self.argv) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr;
|
|
|
|
const envp = m: {
|
|
if (self.env_map) |env_map| {
|
|
const envp_buf = try createNullDelimitedEnvMap(arena, env_map);
|
|
break :m envp_buf.ptr;
|
|
} else if (builtin.link_libc) {
|
|
break :m std.c.environ;
|
|
} else if (builtin.output_mode == .Exe) {
|
|
// Then we have Zig start code and this works.
|
|
// TODO type-safety for null-termination of `os.environ`.
|
|
break :m @ptrCast([*:null]?[*:0]u8, os.environ.ptr);
|
|
} else {
|
|
// TODO come up with a solution for this.
|
|
@compileError("missing std lib enhancement: ChildProcess implementation has no way to collect the environment variables to forward to the child process");
|
|
}
|
|
};
|
|
|
|
// This pipe is used to communicate errors between the time of fork
|
|
// and execve from the child process to the parent process.
|
|
const err_pipe = blk: {
|
|
if (builtin.os.tag == .linux) {
|
|
const fd = try os.eventfd(0, linux.EFD.CLOEXEC);
|
|
// There's no distinction between the readable and the writeable
|
|
// end with eventfd
|
|
break :blk [2]os.fd_t{ fd, fd };
|
|
} else {
|
|
break :blk try os.pipe2(os.O.CLOEXEC);
|
|
}
|
|
};
|
|
errdefer destroyPipe(err_pipe);
|
|
|
|
const pid_result = try os.fork();
|
|
if (pid_result == 0) {
|
|
// we are the child
|
|
setUpChildIo(self.stdin_behavior, stdin_pipe[0], os.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
|
|
setUpChildIo(self.stdout_behavior, stdout_pipe[1], os.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
|
|
setUpChildIo(self.stderr_behavior, stderr_pipe[1], os.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
|
|
|
|
if (self.stdin_behavior == .Pipe) {
|
|
os.close(stdin_pipe[0]);
|
|
os.close(stdin_pipe[1]);
|
|
}
|
|
if (self.stdout_behavior == .Pipe) {
|
|
os.close(stdout_pipe[0]);
|
|
os.close(stdout_pipe[1]);
|
|
}
|
|
if (self.stderr_behavior == .Pipe) {
|
|
os.close(stderr_pipe[0]);
|
|
os.close(stderr_pipe[1]);
|
|
}
|
|
|
|
if (self.cwd_dir) |cwd| {
|
|
os.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err);
|
|
} else if (self.cwd) |cwd| {
|
|
os.chdir(cwd) catch |err| forkChildErrReport(err_pipe[1], err);
|
|
}
|
|
|
|
if (self.gid) |gid| {
|
|
os.setregid(gid, gid) catch |err| forkChildErrReport(err_pipe[1], err);
|
|
}
|
|
|
|
if (self.uid) |uid| {
|
|
os.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err);
|
|
}
|
|
|
|
const err = switch (self.expand_arg0) {
|
|
.expand => os.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp),
|
|
.no_expand => os.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp),
|
|
};
|
|
forkChildErrReport(err_pipe[1], err);
|
|
}
|
|
|
|
// we are the parent
|
|
const pid = @intCast(i32, pid_result);
|
|
if (self.stdin_behavior == StdIo.Pipe) {
|
|
self.stdin = File{ .handle = stdin_pipe[1] };
|
|
} else {
|
|
self.stdin = null;
|
|
}
|
|
if (self.stdout_behavior == StdIo.Pipe) {
|
|
self.stdout = File{ .handle = stdout_pipe[0] };
|
|
} else {
|
|
self.stdout = null;
|
|
}
|
|
if (self.stderr_behavior == StdIo.Pipe) {
|
|
self.stderr = File{ .handle = stderr_pipe[0] };
|
|
} else {
|
|
self.stderr = null;
|
|
}
|
|
|
|
self.pid = pid;
|
|
self.err_pipe = err_pipe;
|
|
self.term = null;
|
|
|
|
if (self.stdin_behavior == StdIo.Pipe) {
|
|
os.close(stdin_pipe[0]);
|
|
}
|
|
if (self.stdout_behavior == StdIo.Pipe) {
|
|
os.close(stdout_pipe[1]);
|
|
}
|
|
if (self.stderr_behavior == StdIo.Pipe) {
|
|
os.close(stderr_pipe[1]);
|
|
}
|
|
}
|
|
|
|
fn spawnWindows(self: *ChildProcess) SpawnError!void {
|
|
const saAttr = windows.SECURITY_ATTRIBUTES{
|
|
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
|
|
.bInheritHandle = windows.TRUE,
|
|
.lpSecurityDescriptor = null,
|
|
};
|
|
|
|
const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore);
|
|
|
|
const nul_handle = if (any_ignore)
|
|
// "\Device\Null" or "\??\NUL"
|
|
windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{
|
|
.access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE,
|
|
.share_access = windows.FILE_SHARE_READ,
|
|
.creation = windows.OPEN_EXISTING,
|
|
.io_mode = .blocking,
|
|
}) catch |err| switch (err) {
|
|
error.PathAlreadyExists => unreachable, // not possible for "NUL"
|
|
error.PipeBusy => unreachable, // not possible for "NUL"
|
|
error.FileNotFound => unreachable, // not possible for "NUL"
|
|
error.AccessDenied => unreachable, // not possible for "NUL"
|
|
error.NameTooLong => unreachable, // not possible for "NUL"
|
|
error.WouldBlock => unreachable, // not possible for "NUL"
|
|
else => |e| return e,
|
|
}
|
|
else
|
|
undefined;
|
|
defer {
|
|
if (any_ignore) os.close(nul_handle);
|
|
}
|
|
if (any_ignore) {
|
|
try windows.SetHandleInformation(nul_handle, windows.HANDLE_FLAG_INHERIT, 0);
|
|
}
|
|
|
|
var g_hChildStd_IN_Rd: ?windows.HANDLE = null;
|
|
var g_hChildStd_IN_Wr: ?windows.HANDLE = null;
|
|
switch (self.stdin_behavior) {
|
|
StdIo.Pipe => {
|
|
try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr);
|
|
},
|
|
StdIo.Ignore => {
|
|
g_hChildStd_IN_Rd = nul_handle;
|
|
},
|
|
StdIo.Inherit => {
|
|
g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null;
|
|
},
|
|
StdIo.Close => {
|
|
g_hChildStd_IN_Rd = null;
|
|
},
|
|
}
|
|
errdefer if (self.stdin_behavior == StdIo.Pipe) {
|
|
windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
|
|
};
|
|
|
|
var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
|
|
var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
|
|
switch (self.stdout_behavior) {
|
|
StdIo.Pipe => {
|
|
try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
|
|
},
|
|
StdIo.Ignore => {
|
|
g_hChildStd_OUT_Wr = nul_handle;
|
|
},
|
|
StdIo.Inherit => {
|
|
g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null;
|
|
},
|
|
StdIo.Close => {
|
|
g_hChildStd_OUT_Wr = null;
|
|
},
|
|
}
|
|
errdefer if (self.stdin_behavior == StdIo.Pipe) {
|
|
windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr);
|
|
};
|
|
|
|
var g_hChildStd_ERR_Rd: ?windows.HANDLE = null;
|
|
var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
|
|
switch (self.stderr_behavior) {
|
|
StdIo.Pipe => {
|
|
try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
|
|
},
|
|
StdIo.Ignore => {
|
|
g_hChildStd_ERR_Wr = nul_handle;
|
|
},
|
|
StdIo.Inherit => {
|
|
g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null;
|
|
},
|
|
StdIo.Close => {
|
|
g_hChildStd_ERR_Wr = null;
|
|
},
|
|
}
|
|
errdefer if (self.stdin_behavior == StdIo.Pipe) {
|
|
windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr);
|
|
};
|
|
|
|
const cmd_line = try windowsCreateCommandLine(self.allocator, self.argv);
|
|
defer self.allocator.free(cmd_line);
|
|
|
|
var siStartInfo = windows.STARTUPINFOW{
|
|
.cb = @sizeOf(windows.STARTUPINFOW),
|
|
.hStdError = g_hChildStd_ERR_Wr,
|
|
.hStdOutput = g_hChildStd_OUT_Wr,
|
|
.hStdInput = g_hChildStd_IN_Rd,
|
|
.dwFlags = windows.STARTF_USESTDHANDLES,
|
|
|
|
.lpReserved = null,
|
|
.lpDesktop = null,
|
|
.lpTitle = null,
|
|
.dwX = 0,
|
|
.dwY = 0,
|
|
.dwXSize = 0,
|
|
.dwYSize = 0,
|
|
.dwXCountChars = 0,
|
|
.dwYCountChars = 0,
|
|
.dwFillAttribute = 0,
|
|
.wShowWindow = 0,
|
|
.cbReserved2 = 0,
|
|
.lpReserved2 = null,
|
|
};
|
|
var piProcInfo: windows.PROCESS_INFORMATION = undefined;
|
|
|
|
const cwd_w = if (self.cwd) |cwd| try unicode.utf8ToUtf16LeWithNull(self.allocator, cwd) else null;
|
|
defer if (cwd_w) |cwd| self.allocator.free(cwd);
|
|
const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null;
|
|
|
|
const maybe_envp_buf = if (self.env_map) |env_map| try createWindowsEnvBlock(self.allocator, env_map) else null;
|
|
defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf);
|
|
const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null;
|
|
|
|
// the cwd set in ChildProcess is in effect when choosing the executable path
|
|
// to match posix semantics
|
|
const app_path = x: {
|
|
if (self.cwd) |cwd| {
|
|
const resolved = try fs.path.resolve(self.allocator, &[_][]const u8{ cwd, self.argv[0] });
|
|
defer self.allocator.free(resolved);
|
|
break :x try cstr.addNullByte(self.allocator, resolved);
|
|
} else {
|
|
break :x try cstr.addNullByte(self.allocator, self.argv[0]);
|
|
}
|
|
};
|
|
defer self.allocator.free(app_path);
|
|
|
|
const app_path_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, app_path);
|
|
defer self.allocator.free(app_path_w);
|
|
|
|
const cmd_line_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, cmd_line);
|
|
defer self.allocator.free(cmd_line_w);
|
|
|
|
windowsCreateProcess(app_path_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| {
|
|
if (no_path_err != error.FileNotFound) return no_path_err;
|
|
|
|
var free_path = true;
|
|
const PATH = process.getEnvVarOwned(self.allocator, "PATH") catch |err| switch (err) {
|
|
error.EnvironmentVariableNotFound => blk: {
|
|
free_path = false;
|
|
break :blk "";
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
defer if (free_path) self.allocator.free(PATH);
|
|
|
|
var free_path_ext = true;
|
|
const PATHEXT = process.getEnvVarOwned(self.allocator, "PATHEXT") catch |err| switch (err) {
|
|
error.EnvironmentVariableNotFound => blk: {
|
|
free_path_ext = false;
|
|
break :blk "";
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
defer if (free_path_ext) self.allocator.free(PATHEXT);
|
|
|
|
const app_name = self.argv[0];
|
|
|
|
var it = mem.tokenize(u8, PATH, ";");
|
|
retry: while (it.next()) |search_path| {
|
|
const path_no_ext = try fs.path.join(self.allocator, &[_][]const u8{ search_path, app_name });
|
|
defer self.allocator.free(path_no_ext);
|
|
|
|
var ext_it = mem.tokenize(u8, PATHEXT, ";");
|
|
while (ext_it.next()) |app_ext| {
|
|
const joined_path = try mem.concat(self.allocator, u8, &[_][]const u8{ path_no_ext, app_ext });
|
|
defer self.allocator.free(joined_path);
|
|
|
|
const joined_path_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, joined_path);
|
|
defer self.allocator.free(joined_path_w);
|
|
|
|
if (windowsCreateProcess(joined_path_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) |_| {
|
|
break :retry;
|
|
} else |err| switch (err) {
|
|
error.FileNotFound => continue,
|
|
error.AccessDenied => continue,
|
|
else => return err,
|
|
}
|
|
}
|
|
} else {
|
|
return no_path_err; // return the original error
|
|
}
|
|
};
|
|
|
|
if (g_hChildStd_IN_Wr) |h| {
|
|
self.stdin = File{ .handle = h };
|
|
} else {
|
|
self.stdin = null;
|
|
}
|
|
if (g_hChildStd_OUT_Rd) |h| {
|
|
self.stdout = File{ .handle = h };
|
|
} else {
|
|
self.stdout = null;
|
|
}
|
|
if (g_hChildStd_ERR_Rd) |h| {
|
|
self.stderr = File{ .handle = h };
|
|
} else {
|
|
self.stderr = null;
|
|
}
|
|
|
|
self.handle = piProcInfo.hProcess;
|
|
self.thread_handle = piProcInfo.hThread;
|
|
self.term = null;
|
|
|
|
if (self.stdin_behavior == StdIo.Pipe) {
|
|
os.close(g_hChildStd_IN_Rd.?);
|
|
}
|
|
if (self.stderr_behavior == StdIo.Pipe) {
|
|
os.close(g_hChildStd_ERR_Wr.?);
|
|
}
|
|
if (self.stdout_behavior == StdIo.Pipe) {
|
|
os.close(g_hChildStd_OUT_Wr.?);
|
|
}
|
|
}
|
|
|
|
fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void {
|
|
switch (stdio) {
|
|
.Pipe => try os.dup2(pipe_fd, std_fileno),
|
|
.Close => os.close(std_fileno),
|
|
.Inherit => {},
|
|
.Ignore => try os.dup2(dev_null_fd, std_fileno),
|
|
}
|
|
}
|
|
};
|
|
|
|
fn windowsCreateProcess(app_name: [*:0]u16, cmd_line: [*:0]u16, envp_ptr: ?[*]u16, cwd_ptr: ?[*:0]u16, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION) !void {
|
|
// TODO the docs for environment pointer say:
|
|
// > A pointer to the environment block for the new process. If this parameter
|
|
// > is NULL, the new process uses the environment of the calling process.
|
|
// > ...
|
|
// > An environment block can contain either Unicode or ANSI characters. If
|
|
// > the environment block pointed to by lpEnvironment contains Unicode
|
|
// > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
|
|
// > If this parameter is NULL and the environment block of the parent process
|
|
// > contains Unicode characters, you must also ensure that dwCreationFlags
|
|
// > includes CREATE_UNICODE_ENVIRONMENT.
|
|
// This seems to imply that we have to somehow know whether our process parent passed
|
|
// CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
|
|
// Since we do not know this information that would imply that we must not pass NULL
|
|
// for the parameter.
|
|
// However this would imply that programs compiled with -DUNICODE could not pass
|
|
// environment variables to programs that were not, which seems unlikely.
|
|
// More investigation is needed.
|
|
return windows.CreateProcessW(
|
|
app_name,
|
|
cmd_line,
|
|
null,
|
|
null,
|
|
windows.TRUE,
|
|
windows.CREATE_UNICODE_ENVIRONMENT,
|
|
@ptrCast(?*anyopaque, envp_ptr),
|
|
cwd_ptr,
|
|
lpStartupInfo,
|
|
lpProcessInformation,
|
|
);
|
|
}
|
|
|
|
/// Caller must dealloc.
|
|
fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
defer buf.deinit();
|
|
|
|
for (argv) |arg, arg_i| {
|
|
if (arg_i != 0) try buf.append(' ');
|
|
if (mem.indexOfAny(u8, arg, " \t\n\"") == null) {
|
|
try buf.appendSlice(arg);
|
|
continue;
|
|
}
|
|
try buf.append('"');
|
|
var backslash_count: usize = 0;
|
|
for (arg) |byte| {
|
|
switch (byte) {
|
|
'\\' => backslash_count += 1,
|
|
'"' => {
|
|
try buf.appendNTimes('\\', backslash_count * 2 + 1);
|
|
try buf.append('"');
|
|
backslash_count = 0;
|
|
},
|
|
else => {
|
|
try buf.appendNTimes('\\', backslash_count);
|
|
try buf.append(byte);
|
|
backslash_count = 0;
|
|
},
|
|
}
|
|
}
|
|
try buf.appendNTimes('\\', backslash_count * 2);
|
|
try buf.append('"');
|
|
}
|
|
|
|
return buf.toOwnedSliceSentinel(0);
|
|
}
|
|
|
|
fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void {
|
|
if (rd) |h| os.close(h);
|
|
if (wr) |h| os.close(h);
|
|
}
|
|
|
|
fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
|
|
var rd_h: windows.HANDLE = undefined;
|
|
var wr_h: windows.HANDLE = undefined;
|
|
try windows.CreatePipe(&rd_h, &wr_h, sattr);
|
|
errdefer windowsDestroyPipe(rd_h, wr_h);
|
|
try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0);
|
|
rd.* = rd_h;
|
|
wr.* = wr_h;
|
|
}
|
|
|
|
var pipe_name_counter = std.atomic.Atomic(u32).init(1);
|
|
|
|
fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void {
|
|
var tmp_bufw: [128]u16 = undefined;
|
|
|
|
// Anonymous pipes are built upon Named pipes.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe
|
|
// Asynchronous (overlapped) read and write operations are not supported by anonymous pipes.
|
|
// https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations
|
|
const pipe_path = blk: {
|
|
var tmp_buf: [128]u8 = undefined;
|
|
// Forge a random path for the pipe.
|
|
const pipe_path = std.fmt.bufPrintZ(
|
|
&tmp_buf,
|
|
"\\\\.\\pipe\\zig-childprocess-{d}-{d}",
|
|
.{ windows.kernel32.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .Monotonic) },
|
|
) catch unreachable;
|
|
const len = std.unicode.utf8ToUtf16Le(&tmp_bufw, pipe_path) catch unreachable;
|
|
tmp_bufw[len] = 0;
|
|
break :blk tmp_bufw[0..len :0];
|
|
};
|
|
|
|
// Create the read handle that can be used with overlapped IO ops.
|
|
const read_handle = windows.kernel32.CreateNamedPipeW(
|
|
pipe_path.ptr,
|
|
windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED,
|
|
windows.PIPE_TYPE_BYTE,
|
|
1,
|
|
4096,
|
|
4096,
|
|
0,
|
|
sattr,
|
|
);
|
|
if (read_handle == windows.INVALID_HANDLE_VALUE) {
|
|
switch (windows.kernel32.GetLastError()) {
|
|
else => |err| return windows.unexpectedError(err),
|
|
}
|
|
}
|
|
errdefer os.close(read_handle);
|
|
|
|
var sattr_copy = sattr.*;
|
|
const write_handle = windows.kernel32.CreateFileW(
|
|
pipe_path.ptr,
|
|
windows.GENERIC_WRITE,
|
|
0,
|
|
&sattr_copy,
|
|
windows.OPEN_EXISTING,
|
|
windows.FILE_ATTRIBUTE_NORMAL,
|
|
null,
|
|
);
|
|
if (write_handle == windows.INVALID_HANDLE_VALUE) {
|
|
switch (windows.kernel32.GetLastError()) {
|
|
else => |err| return windows.unexpectedError(err),
|
|
}
|
|
}
|
|
errdefer os.close(write_handle);
|
|
|
|
try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0);
|
|
|
|
rd.* = read_handle;
|
|
wr.* = write_handle;
|
|
}
|
|
|
|
fn destroyPipe(pipe: [2]os.fd_t) void {
|
|
os.close(pipe[0]);
|
|
if (pipe[0] != pipe[1]) os.close(pipe[1]);
|
|
}
|
|
|
|
// Child of fork calls this to report an error to the fork parent.
|
|
// Then the child exits.
|
|
fn forkChildErrReport(fd: i32, err: ChildProcess.SpawnError) noreturn {
|
|
writeIntFd(fd, @as(ErrInt, @errorToInt(err))) catch {};
|
|
// If we're linking libc, some naughty applications may have registered atexit handlers
|
|
// which we really do not want to run in the fork child. I caught LLVM doing this and
|
|
// it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne,
|
|
// "Why'd you have to go and make things so complicated?"
|
|
if (builtin.link_libc) {
|
|
// The _exit(2) function does nothing but make the exit syscall, unlike exit(3)
|
|
std.c._exit(1);
|
|
}
|
|
os.exit(1);
|
|
}
|
|
|
|
const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8);
|
|
|
|
fn writeIntFd(fd: i32, value: ErrInt) !void {
|
|
const file = File{
|
|
.handle = fd,
|
|
.capable_io_mode = .blocking,
|
|
.intended_io_mode = .blocking,
|
|
};
|
|
file.writer().writeIntNative(u64, @intCast(u64, value)) catch return error.SystemResources;
|
|
}
|
|
|
|
fn readIntFd(fd: i32) !ErrInt {
|
|
const file = File{
|
|
.handle = fd,
|
|
.capable_io_mode = .blocking,
|
|
.intended_io_mode = .blocking,
|
|
};
|
|
return @intCast(ErrInt, file.reader().readIntNative(u64) catch return error.SystemResources);
|
|
}
|
|
|
|
/// Caller must free result.
|
|
pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 {
|
|
// count bytes needed
|
|
const max_chars_needed = x: {
|
|
var max_chars_needed: usize = 4; // 4 for the final 4 null bytes
|
|
var it = env_map.iterator();
|
|
while (it.next()) |pair| {
|
|
// +1 for '='
|
|
// +1 for null byte
|
|
max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2;
|
|
}
|
|
break :x max_chars_needed;
|
|
};
|
|
const result = try allocator.alloc(u16, max_chars_needed);
|
|
errdefer allocator.free(result);
|
|
|
|
var it = env_map.iterator();
|
|
var i: usize = 0;
|
|
while (it.next()) |pair| {
|
|
i += try unicode.utf8ToUtf16Le(result[i..], pair.key_ptr.*);
|
|
result[i] = '=';
|
|
i += 1;
|
|
i += try unicode.utf8ToUtf16Le(result[i..], pair.value_ptr.*);
|
|
result[i] = 0;
|
|
i += 1;
|
|
}
|
|
result[i] = 0;
|
|
i += 1;
|
|
result[i] = 0;
|
|
i += 1;
|
|
result[i] = 0;
|
|
i += 1;
|
|
result[i] = 0;
|
|
i += 1;
|
|
return allocator.shrink(result, i);
|
|
}
|
|
|
|
pub fn createNullDelimitedEnvMap(arena: mem.Allocator, env_map: *const EnvMap) ![:null]?[*:0]u8 {
|
|
const envp_count = env_map.count();
|
|
const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null);
|
|
{
|
|
var it = env_map.iterator();
|
|
var i: usize = 0;
|
|
while (it.next()) |pair| : (i += 1) {
|
|
const env_buf = try arena.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.len + 1, 0);
|
|
mem.copy(u8, env_buf, pair.key_ptr.*);
|
|
env_buf[pair.key_ptr.len] = '=';
|
|
mem.copy(u8, env_buf[pair.key_ptr.len + 1 ..], pair.value_ptr.*);
|
|
envp_buf[i] = env_buf.ptr;
|
|
}
|
|
assert(i == envp_count);
|
|
}
|
|
return envp_buf;
|
|
}
|
|
|
|
test "createNullDelimitedEnvMap" {
|
|
const testing = std.testing;
|
|
const allocator = testing.allocator;
|
|
var envmap = EnvMap.init(allocator);
|
|
defer envmap.deinit();
|
|
|
|
try envmap.put("HOME", "/home/ifreund");
|
|
try envmap.put("WAYLAND_DISPLAY", "wayland-1");
|
|
try envmap.put("DISPLAY", ":1");
|
|
try envmap.put("DEBUGINFOD_URLS", " ");
|
|
try envmap.put("XCURSOR_SIZE", "24");
|
|
|
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
defer arena.deinit();
|
|
const environ = try createNullDelimitedEnvMap(arena.allocator(), &envmap);
|
|
|
|
try testing.expectEqual(@as(usize, 5), environ.len);
|
|
|
|
inline for (.{
|
|
"HOME=/home/ifreund",
|
|
"WAYLAND_DISPLAY=wayland-1",
|
|
"DISPLAY=:1",
|
|
"DEBUGINFOD_URLS= ",
|
|
"XCURSOR_SIZE=24",
|
|
}) |target| {
|
|
for (environ) |variable| {
|
|
if (mem.eql(u8, mem.span(variable orelse continue), target)) break;
|
|
} else {
|
|
try testing.expect(false); // Environment variable not found
|
|
}
|
|
}
|
|
}
|