diff --git a/lib/std/build.zig b/lib/std/build.zig index 4d2b5de1f14c..83316dc5ac76 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -978,8 +978,8 @@ pub const Builder = struct { child.cwd = cwd; child.env_map = env_map; - const term = child.spawnAndWait() catch |err| { - log.err("Unable to spawn {s}: {s}", .{ argv[0], @errorName(err) }); + const term = child.spawnAndWait(.{}) catch |err| { + log.err("Unable to spawn {s}: {s}\n", .{ argv[0], @errorName(err) }); return err; }; @@ -1194,7 +1194,7 @@ pub const Builder = struct { child.stderr_behavior = stderr_behavior; child.env_map = self.env_map; - try child.spawn(); + try child.spawn(.{}); const stdout = child.stdout.?.reader().readAllAlloc(self.allocator, max_output_size) catch { return error.ReadFailure; diff --git a/lib/std/build/RunStep.zig b/lib/std/build/RunStep.zig index 3422b83a6d83..eb1383146436 100644 --- a/lib/std/build/RunStep.zig +++ b/lib/std/build/RunStep.zig @@ -224,7 +224,7 @@ pub fn runCommand( if (print) printCmd(cwd, argv); - child.spawn() catch |err| { + child.spawn(.{}) catch |err| { std.debug.print("Unable to spawn {s}: {s}\n", .{ argv[0], @errorName(err) }); return err; }; diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index 7bff9fe0892b..85f7273c3e76 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -1,4 +1,4 @@ -const std = @import("std.zig"); +const std = @import("std"); const builtin = @import("builtin"); const cstr = std.cstr; const unicode = std.unicode; @@ -28,6 +28,21 @@ pub const ChildProcess = struct { stdin: ?File, stdout: ?File, stderr: ?File, + /// Additional streams other than stdin, stdout, stderr + /// * unidirectional input xor output pipe for portability. + /// If non-null (= used) + /// * user must provide function to tell child process file handle + /// (stdin of child process [must be in pipe mode] or environment variables) + /// * user must process pipe handle in the child process + /// - attach the handle ie via `var file = File{.handle=handle};` + /// after parsing the integer. + /// - close it, if not needed anymore + /// - methods like readAll() read until the pipe write end is closed + /// - Non-blocking methods require message separators like ETB, EOT + /// and some more thought about message content/sanitization + /// - It is recommended to use `os.disableFileInheritance()` + /// in the child process to prevent inheritance by grandchildren + extra_streams: ?[]ExtraStream, term: ?(SpawnError!Term), @@ -79,6 +94,10 @@ pub const ChildProcess = struct { /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process. CurrentWorkingDirectoryUnlinked, } || + // POSIX-only. Non-standard streams are used and fcntl failed. + os.FcntlError || + // Non-standard streams are used and writing to stdin of child process failed. + os.WriteError || os.ExecveError || os.SetIdError || os.ChangeCurDirError || @@ -100,6 +119,22 @@ pub const ChildProcess = struct { Close, }; + pub const PipeDirection = enum { + parent_to_child, + child_to_parent, + }; + + /// ExtraStream (= pipe) is unidirected (in->out) for portability. + pub const ExtraStream = struct { + /// parent_to_child => parent writes into pipe.output + /// child_to_parent => parent reads from pipe.input + direction: PipeDirection, + /// pipe input end for reading + input: ?File, + /// pipe output end for writing + output: ?File, + }; + /// First argument in argv is the executable. pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess { return .{ @@ -117,6 +152,7 @@ pub const ChildProcess = struct { .stdin = null, .stdout = null, .stderr = null, + .extra_streams = null, .stdin_behavior = StdIo.Inherit, .stdout_behavior = StdIo.Inherit, .stderr_behavior = StdIo.Inherit, @@ -130,25 +166,38 @@ pub const ChildProcess = struct { self.gid = user_info.gid; } + const ExPipeInfoProto = switch (builtin.zig_backend) { + // workaround until we replace stage1 with stage2 + .stage1 => ?fn (self: *ChildProcess) ChildProcess.SpawnError!void, + else => ?*const fn (self: *ChildProcess) ChildProcess.SpawnError!void, + }; + + pub const SpawnOptions = struct { + /// Use this field to execute custom code in parent process before the clone call. + /// For example, the child process must be told about non-standard pipes this way. + /// See field of ChildProcess 'extra_streams' for more information. + pipe_info_fn: ExPipeInfoProto = null, + }; + /// On success must call `kill` or `wait`. - pub fn spawn(self: *ChildProcess) SpawnError!void { + pub fn spawn(self: *ChildProcess, opts: SpawnOptions) SpawnError!void { if (!std.process.can_spawn) { @compileError("the target operating system cannot spawn processes"); } if (comptime builtin.target.isDarwin()) { - return self.spawnMacos(); + return self.spawnMacos(opts); } if (builtin.os.tag == .windows) { - return self.spawnWindows(); + return self.spawnWindows(opts); } else { - return self.spawnPosix(); + return self.spawnPosix(opts); } } - pub fn spawnAndWait(self: *ChildProcess) SpawnError!Term { - try self.spawn(); + pub fn spawnAndWait(self: *ChildProcess, opts: SpawnOptions) SpawnError!Term { + try self.spawn(opts); return self.wait(); } @@ -373,6 +422,7 @@ pub const ChildProcess = struct { /// 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. + /// Using extra pipes is not supported. pub fn exec(args: struct { allocator: mem.Allocator, argv: []const []const u8, @@ -391,7 +441,7 @@ pub const ChildProcess = struct { child.env_map = args.env_map; child.expand_arg0 = args.expand_arg0; - try child.spawn(); + try child.spawn(.{}); if (builtin.os.tag == .haiku) { const stdout_in = child.stdout.?.reader(); @@ -481,6 +531,8 @@ pub const ChildProcess = struct { self.term = self.cleanupAfterWait(status); } + /// Close file handles of parent process to child process, + /// if they were not closed in the meantime fn cleanupStreams(self: *ChildProcess) void { if (self.stdin) |*stdin| { stdin.close(); @@ -494,6 +546,24 @@ pub const ChildProcess = struct { stderr.close(); self.stderr = null; } + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra| { + switch (extra.direction) { + .parent_to_child => { + if (extra.output) |*outpipe| { + outpipe.close(); + extra.output = null; + } + }, + .child_to_parent => { + if (extra.input) |*inpipe| { + inpipe.close(); + extra.input = null; + } + }, + } + } + } } fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term { @@ -547,7 +617,7 @@ pub const ChildProcess = struct { Term{ .Unknown = status }; } - fn spawnMacos(self: *ChildProcess) SpawnError!void { + fn spawnMacos(self: *ChildProcess, opts: SpawnOptions) 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); @@ -558,6 +628,22 @@ pub const ChildProcess = struct { 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); + 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; + } + 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) { @@ -575,6 +661,26 @@ pub const ChildProcess = struct { undefined; defer if (any_ignore) os.close(dev_null_fd); + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra, i| { + std.debug.assert(extra.input == null); + std.debug.assert(extra.output == null); + + const tmp_pipe = os.pipe() catch |err| { + for (extra_streams[0..i]) |*extra_fail| { + extra_fail.input.?.close(); + extra_fail.output.?.close(); + extra_fail.input = null; + extra_fail.output = null; + } + return err; + }; + + extra.input = File{ .handle = tmp_pipe[0] }; + extra.output = File{ .handle = tmp_pipe[1] }; + } + } + var attr = try os.posix_spawn.Attr.init(); defer attr.deinit(); var flags: u16 = os.darwin.POSIX_SPAWN_SETSIGDEF | os.darwin.POSIX_SPAWN_SETSIGMASK; @@ -593,6 +699,15 @@ pub const ChildProcess = struct { 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.extra_streams) |extra_streams| { + for (extra_streams) |*extra| { + switch (extra.direction) { + .parent_to_child => try actions.close(extra.output.?.handle), + .child_to_parent => try actions.close(extra.input.?.handle), + } + } + } + if (self.cwd_dir) |cwd| { try actions.fchdir(cwd.fd); } else if (self.cwd) |cwd| { @@ -611,24 +726,14 @@ pub const ChildProcess = struct { 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; + // user must communicate extra pipes to child process either via + // environment variables or stdin + if (self.extra_streams != null) { + std.debug.assert(opts.pipe_info_fn != null); + try opts.pipe_info_fn.?(self); } + const pid = try os.posix_spawn.spawnp(self.argv[0], actions, attr, argv_buf, envp); self.pid = pid; self.term = null; @@ -641,6 +746,29 @@ pub const ChildProcess = struct { if (self.stderr_behavior == StdIo.Pipe) { os.close(stderr_pipe[1]); } + + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra| { + switch (extra.direction) { + .parent_to_child => { + extra.input.?.close(); // parent does not read + extra.input = null; + std.debug.assert(extra.output != null); + var fcntl_flags = try os.fcntl(extra.output.?.handle, os.F.GETFD, 0); + std.debug.assert((fcntl_flags & os.FD_CLOEXEC) == 0); + _ = try os.fcntl(extra.output.?.handle, os.F.SETFD, os.FD_CLOEXEC); + }, + .child_to_parent => { + extra.output.?.close(); // parent does not write + extra.output = null; + std.debug.assert(extra.input != null); + var fcntl_flags = try os.fcntl(extra.input.?.handle, os.F.GETFD, 0); + std.debug.assert((fcntl_flags & os.FD_CLOEXEC) == 0); + _ = try os.fcntl(extra.input.?.handle, os.F.SETFD, os.FD_CLOEXEC); + }, + } + } + } } fn setUpChildIoPosixSpawn( @@ -662,7 +790,7 @@ pub const ChildProcess = struct { } } - fn spawnPosix(self: *ChildProcess) SpawnError!void { + fn spawnPosix(self: *ChildProcess, opts: SpawnOptions) 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) { @@ -679,6 +807,22 @@ pub const ChildProcess = struct { destroyPipe(stderr_pipe); }; + 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; + } + 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) { @@ -698,6 +842,26 @@ pub const ChildProcess = struct { if (any_ignore) os.close(dev_null_fd); } + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra, i| { + std.debug.assert(extra.input == null); + std.debug.assert(extra.output == null); + + const tmp_pipe = os.pipe() catch |err| { + for (extra_streams[0..i]) |*extra_fail| { + extra_fail.input.?.close(); + extra_fail.output.?.close(); + extra_fail.input = null; + extra_fail.output = null; + } + return err; + }; + + extra.input = File{ .handle = tmp_pipe[0] }; + extra.output = File{ .handle = tmp_pipe[1] }; + } + } + var arena_allocator = std.heap.ArenaAllocator.init(self.allocator); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -730,6 +894,13 @@ pub const ChildProcess = struct { } }; + // user must communicate extra pipes to child process either via + // environment variables or stdin + if (self.extra_streams != null) { + std.debug.assert(opts.pipe_info_fn != null); + try opts.pipe_info_fn.?(self); + } + // 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: { @@ -764,6 +935,23 @@ pub const ChildProcess = struct { os.close(stderr_pipe[1]); } + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra| { + switch (extra.direction) { + .parent_to_child => { + extra.output.?.close(); // child does not write + extra.output = null; + std.debug.assert(extra.input != null); + }, + .child_to_parent => { + extra.input.?.close(); // child does not read + extra.input = null; + std.debug.assert(extra.output != null); + }, + } + } + } + if (self.cwd_dir) |cwd| { os.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err); } else if (self.cwd) |cwd| { @@ -787,22 +975,6 @@ pub const ChildProcess = struct { // 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; @@ -816,9 +988,34 @@ pub const ChildProcess = struct { if (self.stderr_behavior == StdIo.Pipe) { os.close(stderr_pipe[1]); } + + // close unused pipe end in parent and set CLOEXEC + // to prevent non-standard streams to leak into children + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra| { + switch (extra.direction) { + .parent_to_child => { + extra.input.?.close(); // parent does not read + extra.input = null; + std.debug.assert(extra.output != null); + var fcntl_flags = try os.fcntl(extra.output.?.handle, os.F.GETFD, 0); + std.debug.assert((fcntl_flags & os.FD_CLOEXEC) == 0); + _ = try os.fcntl(extra.output.?.handle, os.F.SETFD, os.FD_CLOEXEC); + }, + .child_to_parent => { + extra.output.?.close(); // parent does not write + extra.output = null; + std.debug.assert(extra.input != null); + var fcntl_flags = try os.fcntl(extra.input.?.handle, os.F.GETFD, 0); + std.debug.assert((fcntl_flags & os.FD_CLOEXEC) == 0); + _ = try os.fcntl(extra.input.?.handle, os.F.SETFD, os.FD_CLOEXEC); + }, + } + } + } } - fn spawnWindows(self: *ChildProcess) SpawnError!void { + fn spawnWindows(self: *ChildProcess, opts: SpawnOptions) SpawnError!void { const saAttr = windows.SECURITY_ATTRIBUTES{ .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), .bInheritHandle = windows.TRUE, @@ -876,7 +1073,12 @@ pub const ChildProcess = struct { 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); + try windowsMakeAsyncPipe( + &g_hChildStd_OUT_Rd, + &g_hChildStd_OUT_Wr, + &saAttr, + PipeDirection.child_to_parent, + ); }, StdIo.Ignore => { g_hChildStd_OUT_Wr = nul_handle; @@ -896,7 +1098,12 @@ pub const ChildProcess = struct { 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); + try windowsMakeAsyncPipe( + &g_hChildStd_ERR_Rd, + &g_hChildStd_ERR_Wr, + &saAttr, + ChildProcess.PipeDirection.child_to_parent, + ); }, StdIo.Ignore => { g_hChildStd_ERR_Wr = nul_handle; @@ -912,6 +1119,54 @@ pub const ChildProcess = struct { windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); }; + 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; + } + // TODO simplify to operate on file handles + errdefer { + self.stdin = null; + self.stdout = null; + self.stderr = null; + } + + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra, i| { + std.debug.assert(extra.input == null); + std.debug.assert(extra.output == null); + + var g_hChildExtra_Stream_Rd: ?windows.HANDLE = null; + var g_hChildExtra_Stream_Wr: ?windows.HANDLE = null; + windowsMakeAsyncPipe( + &g_hChildExtra_Stream_Rd, + &g_hChildExtra_Stream_Wr, + &saAttr, + extra.direction, + ) catch |err| { + for (extra_streams[0..i]) |*extra_fail| { + extra_fail.input.?.close(); + extra_fail.output.?.close(); + extra_fail.input = null; + extra_fail.output = null; + } + return err; + }; + extra.input = File{ .handle = g_hChildExtra_Stream_Rd.? }; + extra.output = File{ .handle = g_hChildExtra_Stream_Wr.? }; + } + } + const cmd_line = try windowsCreateCommandLine(self.allocator, self.argv); defer self.allocator.free(cmd_line); @@ -989,6 +1244,13 @@ pub const ChildProcess = struct { const cmd_line_w = try unicode.utf8ToUtf16LeWithNull(self.allocator, cmd_line); defer self.allocator.free(cmd_line_w); + // user must communicate extra pipes to child process either via + // environment variables or stdin + if (self.extra_streams != null) { + std.debug.assert(opts.pipe_info_fn != null); + try opts.pipe_info_fn.?(self); + } + exec: { const PATH: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; const PATHEXT: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; @@ -1082,6 +1344,23 @@ pub const ChildProcess = struct { if (self.stdout_behavior == StdIo.Pipe) { os.close(g_hChildStd_OUT_Wr.?); } + + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra| { + switch (extra.direction) { + .parent_to_child => { + extra.input.?.close(); // reading end + extra.input = null; + try windows.SetHandleInformation(extra.output.?.handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT); + }, + .child_to_parent => { + extra.output.?.close(); // writing end + extra.output = null; + try windows.SetHandleInformation(extra.input.?.handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT); + }, + } + } + } } fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { @@ -1324,23 +1603,20 @@ fn windowsCreateProcessPathExt( } 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. + // See https://stackoverflow.com/a/4207169/9306292 + // One can manually write in unicode even if one doesn't compile in unicode + // (-DUNICODE). + // Thus CREATE_UNICODE_ENVIRONMENT, according to how one constructed the + // environment block, is necessary, since CreateProcessA and CreateProcessW may + // work with either Ansi or Unicode. + // * The environment variables can still be inherited from parent process, + // if set to NULL + // * The OS can for an unspecified environment block not figure out, + // if it is Unicode or ANSI. + // * Applications may break without specification of the environment variable + // due to inability of Windows to check (+translate) the character encodings. + // TODO This breaks applications only compatible with Ansi. + // - Clarify, if we want to add complexity to support those. return windows.CreateProcessW( app_name, cmd_line, @@ -1465,7 +1741,13 @@ fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const w var pipe_name_counter = std.atomic.Atomic(u32).init(1); -fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { +// direction defines which side of the handle is inherited by ChildProcess +fn windowsMakeAsyncPipe( + rd: *?windows.HANDLE, + wr: *?windows.HANDLE, + sattr: *const windows.SECURITY_ATTRIBUTES, + direction: ChildProcess.PipeDirection, +) !void { var tmp_bufw: [128]u16 = undefined; // Anonymous pipes are built upon Named pipes. @@ -1520,8 +1802,10 @@ fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *cons } errdefer os.close(write_handle); - try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); - + switch (direction) { + .child_to_parent => try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT), + .parent_to_child => try windows.SetHandleInformation(write_handle, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT), + } rd.* = read_handle; wr.* = write_handle; } diff --git a/lib/std/os.zig b/lib/std/os.zig index b0884cef05d9..b14a74e0a025 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -4840,6 +4840,16 @@ pub fn lseek_CUR_get(fd: fd_t) SeekError!u64 { } } +const UnsetFileInheritanceError = FcntlError || windows.SetHandleInformationError; + +pub inline fn disableFileInheritance(file_handle: fd_t) UnsetFileInheritanceError!void { + if (builtin.os.tag == .windows) { + try windows.SetHandleInformation(file_handle, windows.HANDLE_FLAG_INHERIT, 0); + } else { + _ = try fcntl(file_handle, F.SETFD, FD_CLOEXEC); + } +} + pub const FcntlError = error{ PermissionDenied, FileBusy, diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index ea09631719af..d56a1e953d7e 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -225,6 +225,16 @@ pub fn DeviceIoControl( } } +pub const GetHandleInformationError = error{Unexpected}; + +pub fn GetHandleInformation(h: HANDLE, flags: *DWORD) GetHandleInformationError!void { + if (kernel32.GetHandleInformation(h, flags) == 0) { + switch (kernel32.GetLastError()) { + else => |err| return unexpectedError(err), + } + } +} + pub fn GetOverlappedResult(h: HANDLE, overlapped: *OVERLAPPED, wait: bool) !DWORD { var bytes: DWORD = undefined; if (kernel32.GetOverlappedResult(h, overlapped, &bytes, @boolToInt(wait)) == 0) { diff --git a/lib/std/os/windows/kernel32.zig b/lib/std/os/windows/kernel32.zig index e1cb7f333a28..75b95217f799 100644 --- a/lib/std/os/windows/kernel32.zig +++ b/lib/std/os/windows/kernel32.zig @@ -218,6 +218,8 @@ pub extern "kernel32" fn GetFullPathNameW( lpFilePart: ?*?[*:0]u16, ) callconv(@import("std").os.windows.WINAPI) u32; +pub extern "kernel32" fn GetHandleInformation(hObject: HANDLE, dwFlags: *DWORD) callconv(WINAPI) BOOL; + pub extern "kernel32" fn GetOverlappedResult(hFile: HANDLE, lpOverlapped: *OVERLAPPED, lpNumberOfBytesTransferred: *DWORD, bWait: BOOL) callconv(WINAPI) BOOL; pub extern "kernel32" fn GetProcessHeap() callconv(WINAPI) ?HANDLE; diff --git a/src/Compilation.zig b/src/Compilation.zig index b385fa5f72ba..6a9b1120d5b3 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -4052,7 +4052,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P child.stdout_behavior = .Inherit; child.stderr_behavior = .Inherit; - const term = child.spawnAndWait() catch |err| { + const term = child.spawnAndWait(.{}) catch |err| { return comp.failCObj(c_object, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); }; switch (term) { @@ -4070,7 +4070,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P child.stdout_behavior = .Ignore; child.stderr_behavior = .Pipe; - try child.spawn(); + try child.spawn(.{}); const stderr_reader = child.stderr.?.reader(); diff --git a/src/link/Coff/lld.zig b/src/link/Coff/lld.zig index 46b013054289..27342abc6536 100644 --- a/src/link/Coff/lld.zig +++ b/src/link/Coff/lld.zig @@ -505,7 +505,7 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod child.stdout_behavior = .Inherit; child.stderr_behavior = .Inherit; - const term = child.spawnAndWait() catch |err| { + const term = child.spawnAndWait(.{}) catch |err| { log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); return error.UnableToSpawnSelf; }; @@ -522,7 +522,7 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod child.stdout_behavior = .Ignore; child.stderr_behavior = .Pipe; - try child.spawn(); + try child.spawn(.{}); const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024); diff --git a/src/link/Elf.zig b/src/link/Elf.zig index 30c88fe9b200..ff4958255edc 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -1870,7 +1870,7 @@ fn linkWithLLD(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node) !v child.stdout_behavior = .Inherit; child.stderr_behavior = .Inherit; - const term = child.spawnAndWait() catch |err| { + const term = child.spawnAndWait(.{}) catch |err| { log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); return error.UnableToSpawnSelf; }; @@ -1887,7 +1887,7 @@ fn linkWithLLD(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node) !v child.stdout_behavior = .Ignore; child.stderr_behavior = .Pipe; - try child.spawn(); + try child.spawn(.{}); const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024); diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 346c92ebbef1..b85bedb425db 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -3548,7 +3548,7 @@ fn linkWithLLD(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) ! child.stdout_behavior = .Inherit; child.stderr_behavior = .Inherit; - const term = child.spawnAndWait() catch |err| { + const term = child.spawnAndWait(.{}) catch |err| { log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); return error.UnableToSpawnWasm; }; @@ -3565,7 +3565,7 @@ fn linkWithLLD(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) ! child.stdout_behavior = .Ignore; child.stderr_behavior = .Pipe; - try child.spawn(); + try child.spawn(.{}); const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024); diff --git a/src/main.zig b/src/main.zig index ec0eb74e9338..d8dabc8ed2f5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3376,7 +3376,7 @@ fn runOrTest( comp_destroyed.* = true; } - const term = child.spawnAndWait() catch |err| { + const term = child.spawnAndWait(.{}) catch |err| { try warnAboutForeignBinaries(arena, arg_mode, target_info, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed with '{s}':\n{s}", .{ @errorName(err), cmd }); @@ -4059,7 +4059,7 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi child.stdout_behavior = .Inherit; child.stderr_behavior = .Inherit; - const term = try child.spawnAndWait(); + const term = try child.spawnAndWait(.{}); switch (term) { .Exited => |code| { if (code == 0) return cleanExit(); diff --git a/src/mingw.zig b/src/mingw.zig index a4f2f0cf9165..4d9b03a720f5 100644 --- a/src/mingw.zig +++ b/src/mingw.zig @@ -376,7 +376,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { child.stdout_behavior = .Pipe; child.stderr_behavior = .Pipe; - try child.spawn(); + try child.spawn(.{}); const stderr_reader = child.stderr.?.reader(); diff --git a/test/standalone.zig b/test/standalone.zig index f2d497e6eaef..7b4e15ecf6cf 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -61,6 +61,7 @@ pub fn addCases(cases: *tests.StandaloneContext) void { (builtin.os.tag != .windows or builtin.cpu.arch != .aarch64)) { cases.addBuildFile("test/standalone/load_dynamic_library/build.zig", .{}); + cases.addBuildFile("test/standalone/childprocess_extrapipe/build.zig", .{}); } if (builtin.os.tag == .windows) { diff --git a/test/standalone/childprocess_extrapipe/build.zig b/test/standalone/childprocess_extrapipe/build.zig new file mode 100644 index 000000000000..6a18f3f6d169 --- /dev/null +++ b/test/standalone/childprocess_extrapipe/build.zig @@ -0,0 +1,16 @@ +const Builder = @import("std").build.Builder; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const child = b.addExecutable("child", "child.zig"); + child.setBuildMode(mode); + + const parent = b.addExecutable("parent", "parent.zig"); + parent.setBuildMode(mode); + const run_cmd = parent.run(); + run_cmd.addArtifactArg(child); + + const test_step = b.step("test", "Test it"); + test_step.dependOn(&run_cmd.step); +} diff --git a/test/standalone/childprocess_extrapipe/child.zig b/test/standalone/childprocess_extrapipe/child.zig new file mode 100644 index 000000000000..24eaf25ae623 --- /dev/null +++ b/test/standalone/childprocess_extrapipe/child.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const windows = std.os.windows; +pub fn main() !void { + var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; + defer std.debug.assert(!general_purpose_allocator.deinit()); + const gpa = general_purpose_allocator.allocator(); + const stdin = std.io.getStdIn(); + const stdin_reader = stdin.reader(); + const stdin_cont = try stdin_reader.readUntilDelimiterAlloc(gpa, '\n', 2_000); + defer gpa.free(stdin_cont); + var file_handle = file_handle: { + if (builtin.target.os.tag == .windows) { + var handle_int = try std.fmt.parseInt(usize, stdin_cont, 10); + break :file_handle @intToPtr(windows.HANDLE, handle_int); + } else { + break :file_handle try std.fmt.parseInt(std.os.fd_t, stdin_cont, 10); + } + }; + if (builtin.target.os.tag == .windows) { + // windows.HANDLE_FLAG_INHERIT is enabled + var handle_flags: windows.DWORD = undefined; + try windows.GetHandleInformation(file_handle, &handle_flags); + try std.testing.expect(handle_flags & windows.HANDLE_FLAG_INHERIT != 0); + } else { + // FD_CLOEXEC is not set + var fcntl_flags = try std.os.fcntl(file_handle, std.os.F.GETFD, 0); + try std.testing.expect((fcntl_flags & std.os.FD_CLOEXEC) == 0); + } + try std.os.disableFileInheritance(file_handle); + var extra_stream_in = std.fs.File{ .handle = file_handle }; + defer extra_stream_in.close(); + const extra_str_in_rd = extra_stream_in.reader(); + const all_extra_str_in = try extra_str_in_rd.readUntilDelimiterAlloc(gpa, '\x17', 20_000); + defer gpa.free(all_extra_str_in); + try std.testing.expectEqualSlices(u8, all_extra_str_in, "test123"); +} diff --git a/test/standalone/childprocess_extrapipe/parent.zig b/test/standalone/childprocess_extrapipe/parent.zig new file mode 100644 index 000000000000..df5c2c99567d --- /dev/null +++ b/test/standalone/childprocess_extrapipe/parent.zig @@ -0,0 +1,82 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const ChildProcess = std.ChildProcess; +const math = std.math; +const windows = std.os.windows; +const os = std.os; +const testing = std.testing; + +fn testPipeInfo(self: *ChildProcess) ChildProcess.SpawnError!void { + const windowsPtrDigits: usize = std.math.log10(math.maxInt(usize)); + const otherPtrDigits: usize = std.math.log10(math.maxInt(u32)) + 1; // +1 for sign + if (self.extra_streams) |extra_streams| { + for (extra_streams) |*extra| { + const size = comptime size: { + if (builtin.target.os.tag == .windows) { + break :size windowsPtrDigits; + } else { + break :size otherPtrDigits; + } + }; + var buf = comptime [_]u8{0} ** size; + var s_chpipe_h: []u8 = undefined; + std.debug.assert(extra.direction == .parent_to_child); + const handle = handle: { + if (builtin.target.os.tag == .windows) { + // handle is *anyopaque and there is no other way to cast + break :handle @ptrToInt(extra.*.input.?.handle); + } else { + break :handle extra.*.input.?.handle; + } + }; + s_chpipe_h = std.fmt.bufPrint( + buf[0..], + "{d}", + .{handle}, + ) catch unreachable; + try self.stdin.?.writer().writeAll(s_chpipe_h); + try self.stdin.?.writer().writeAll("\n"); + } + } +} + +pub fn main() !void { + var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; + defer if (gpa_state.deinit()) @panic("found memory leaks"); + const gpa = gpa_state.allocator(); + + var it = try std.process.argsWithAllocator(gpa); + defer it.deinit(); + _ = it.next() orelse unreachable; // skip binary name + const child_path = it.next() orelse unreachable; + + var child_process = ChildProcess.init( + &.{child_path}, + gpa, + ); + child_process.stdin_behavior = .Pipe; + var extra_streams = [_]ChildProcess.ExtraStream{ + .{ + .direction = .parent_to_child, + .input = null, + .output = null, + }, + }; + child_process.extra_streams = &extra_streams; + + try child_process.spawn(.{ .pipe_info_fn = testPipeInfo }); + try std.testing.expect(child_process.extra_streams.?[0].input == null); + if (builtin.target.os.tag == .windows) { + var handle_flags: windows.DWORD = undefined; + try windows.GetHandleInformation(child_process.extra_streams.?[0].output.?.handle, &handle_flags); + std.debug.assert(handle_flags & windows.HANDLE_FLAG_INHERIT != 0); + } else { + const fcntl_flags = try os.fcntl(child_process.extra_streams.?[0].output.?.handle, os.F.GETFD, 0); + try std.testing.expect((fcntl_flags & os.FD_CLOEXEC) != 0); + } + + const extra_str_wr = child_process.extra_streams.?[0].output.?.writer(); + try extra_str_wr.writeAll("test123\x17"); // ETB = \x17 + const ret_val = try child_process.wait(); + try testing.expectEqual(ret_val, .{ .Exited = 0 }); +} diff --git a/test/tests.zig b/test/tests.zig index d884a41599f5..96005ca5776e 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -887,7 +887,7 @@ pub const StackTracesContext = struct { if (b.verbose) { printInvocation(args.items); } - child.spawn() catch |err| debug.panic("Unable to spawn {s}: {s}\n", .{ full_exe_path, @errorName(err) }); + child.spawn(.{}) catch |err| debug.panic("Unable to spawn {s}: {s}\n", .{ full_exe_path, @errorName(err) }); const stdout = child.stdout.?.reader().readAllAlloc(b.allocator, max_stdout_size) catch unreachable; defer b.allocator.free(stdout);