From f47824f24d1b20c276a797df8cf99066c08203b6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 May 2024 11:25:41 -0700 Subject: [PATCH] std: restructure child process namespace --- CMakeLists.txt | 208 +-- lib/compiler/aro/aro/Driver.zig | 2 +- lib/compiler/std-docs.zig | 4 +- lib/std/Build.zig | 6 +- lib/std/Build/Step.zig | 6 +- lib/std/child_process.zig | 1880 --------------------- lib/std/process.zig | 135 +- lib/std/process/Child.zig | 1795 ++++++++++++++++++++ lib/std/std.zig | 2 - lib/std/zig/LibCInstallation.zig | 4 +- src/Compilation.zig | 6 +- src/link.zig | 4 +- src/link/Coff.zig | 10 +- src/link/Wasm.zig | 2 +- src/main.zig | 12 +- test/src/Cases.zig | 2 +- test/standalone/child_process/main.zig | 2 +- test/standalone/windows_bat_args/fuzz.zig | 2 +- test/standalone/windows_bat_args/test.zig | 2 +- test/standalone/windows_spawn/main.zig | 2 +- tools/docgen.zig | 2 +- tools/generate_linux_syscalls.zig | 4 +- tools/update_clang_options.zig | 2 +- tools/update_cpu_features.zig | 2 +- 24 files changed, 2048 insertions(+), 2048 deletions(-) delete mode 100644 lib/std/child_process.zig create mode 100644 lib/std/process/Child.zig diff --git a/CMakeLists.txt b/CMakeLists.txt index 2daff370fb..22051f1902 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,100 +193,6 @@ set(ZIG_CPP_SOURCES # Needed because we use cmake, not the zig build system, to build zig2.o. set(ZIG_STAGE2_SOURCES "${ZIG_CONFIG_ZIG_OUT}" - lib/std/array_hash_map.zig - lib/std/array_list.zig - lib/std/ascii.zig - lib/std/atomic.zig - lib/std/base64.zig - lib/std/BitStack.zig - lib/std/buf_map.zig - lib/std/Build.zig - lib/std/Build/Cache.zig - lib/std/Build/Cache/DepTokenizer.zig - lib/std/builtin.zig - lib/std/c.zig - lib/std/c/linux.zig - lib/std/child_process.zig - lib/std/coff.zig - lib/std/static_string_map.zig - lib/std/crypto.zig - lib/std/crypto/blake3.zig - lib/std/crypto/siphash.zig - lib/std/debug.zig - lib/std/dwarf.zig - lib/std/dwarf/AT.zig - lib/std/dwarf/ATE.zig - lib/std/dwarf/FORM.zig - lib/std/dwarf/LANG.zig - lib/std/dwarf/OP.zig - lib/std/dwarf/TAG.zig - lib/std/elf.zig - lib/std/fifo.zig - lib/std/fmt.zig - lib/std/fmt/format_float.zig - lib/std/fmt/parse_float.zig - lib/std/fs.zig - lib/std/fs/AtomicFile.zig - lib/std/fs/Dir.zig - lib/std/fs/File.zig - lib/std/fs/get_app_data_dir.zig - lib/std/fs/path.zig - lib/std/hash.zig - lib/std/hash/auto_hash.zig - lib/std/hash/wyhash.zig - lib/std/hash_map.zig - lib/std/heap.zig - lib/std/heap/arena_allocator.zig - lib/std/io.zig - lib/std/io/buffered_atomic_file.zig - lib/std/io/buffered_writer.zig - lib/std/io/change_detection_stream.zig - lib/std/io/counting_reader.zig - lib/std/io/counting_writer.zig - lib/std/io/find_byte_writer.zig - lib/std/io/fixed_buffer_stream.zig - lib/std/io/limited_reader.zig - lib/std/io/Reader.zig - lib/std/io/seekable_stream.zig - lib/std/io/Writer.zig - lib/std/json.zig - lib/std/json/stringify.zig - lib/std/leb128.zig - lib/std/linked_list.zig - lib/std/log.zig - lib/std/macho.zig - lib/std/math.zig - lib/std/math/big.zig - lib/std/math/big/int.zig - lib/std/math/float.zig - lib/std/math/frexp.zig - lib/std/math/isinf.zig - lib/std/math/isnan.zig - lib/std/math/log.zig - lib/std/math/log10.zig - lib/std/math/log2.zig - lib/std/math/signbit.zig - lib/std/math/sqrt.zig - lib/std/mem.zig - lib/std/mem/Allocator.zig - lib/std/meta.zig - lib/std/meta/trailer_flags.zig - lib/std/multi_array_list.zig - lib/std/os.zig - lib/std/os/linux.zig - lib/std/os/linux/x86_64.zig - lib/std/os/linux.zig - lib/std/os/linux/IoUring.zig - lib/std/os/linux/io_uring_sqe.zig - lib/std/os/linux/x86_64.zig - lib/std/os/windows.zig - lib/std/os/windows/ntstatus.zig - lib/std/os/windows/win32error.zig - lib/std/Progress.zig - lib/std/pdb.zig - lib/std/process.zig - lib/std/Random.zig - lib/std/sort.zig lib/compiler_rt.zig lib/compiler_rt/absv.zig lib/compiler_rt/absvdi2.zig @@ -363,7 +269,7 @@ set(ZIG_STAGE2_SOURCES lib/compiler_rt/fixxfdi.zig lib/compiler_rt/fixxfsi.zig lib/compiler_rt/fixxfti.zig - lib/compiler_rt/int_from_float.zig + lib/compiler_rt/float_from_int.zig lib/compiler_rt/floatdidf.zig lib/compiler_rt/floatdihf.zig lib/compiler_rt/floatdisf.zig @@ -404,7 +310,7 @@ set(ZIG_STAGE2_SOURCES lib/compiler_rt/getf2.zig lib/compiler_rt/gexf2.zig lib/compiler_rt/int.zig - lib/compiler_rt/float_from_int.zig + lib/compiler_rt/int_from_float.zig lib/compiler_rt/log.zig lib/compiler_rt/log10.zig lib/compiler_rt/log2.zig @@ -417,7 +323,11 @@ set(ZIG_STAGE2_SOURCES lib/compiler_rt/multf3.zig lib/compiler_rt/mulxf3.zig lib/compiler_rt/negXi2.zig + lib/compiler_rt/negdf2.zig + lib/compiler_rt/negsf2.zig + lib/compiler_rt/negtf2.zig lib/compiler_rt/negv.zig + lib/compiler_rt/negxf2.zig lib/compiler_rt/os_version_check.zig lib/compiler_rt/parity.zig lib/compiler_rt/popcount.zig @@ -430,15 +340,11 @@ set(ZIG_STAGE2_SOURCES lib/compiler_rt/sincos.zig lib/compiler_rt/sqrt.zig lib/compiler_rt/stack_probe.zig + lib/compiler_rt/subdf3.zig lib/compiler_rt/subo.zig lib/compiler_rt/subsf3.zig - lib/compiler_rt/subdf3.zig lib/compiler_rt/subtf3.zig lib/compiler_rt/subxf3.zig - lib/compiler_rt/negsf2.zig - lib/compiler_rt/negdf2.zig - lib/compiler_rt/negtf2.zig - lib/compiler_rt/negxf2.zig lib/compiler_rt/tan.zig lib/compiler_rt/trig.zig lib/compiler_rt/trunc.zig @@ -461,8 +367,12 @@ set(ZIG_STAGE2_SOURCES lib/compiler_rt/unorddf2.zig lib/compiler_rt/unordsf2.zig lib/compiler_rt/unordtf2.zig - lib/std/start.zig - lib/std/std.zig + lib/std/BitStack.zig + lib/std/Build.zig + lib/std/Build/Cache.zig + lib/std/Build/Cache/DepTokenizer.zig + lib/std/Progress.zig + lib/std/Random.zig lib/std/Target.zig lib/std/Target/Query.zig lib/std/Target/aarch64.zig @@ -476,8 +386,8 @@ set(ZIG_STAGE2_SOURCES lib/std/Target/nvptx.zig lib/std/Target/powerpc.zig lib/std/Target/riscv.zig - lib/std/Target/sparc.zig lib/std/Target/s390x.zig + lib/std/Target/sparc.zig lib/std/Target/wasm.zig lib/std/Target/x86.zig lib/std/Thread.zig @@ -486,6 +396,96 @@ set(ZIG_STAGE2_SOURCES lib/std/Thread/Pool.zig lib/std/Thread/ResetEvent.zig lib/std/Thread/WaitGroup.zig + lib/std/array_hash_map.zig + lib/std/array_list.zig + lib/std/ascii.zig + lib/std/atomic.zig + lib/std/base64.zig + lib/std/buf_map.zig + lib/std/builtin.zig + lib/std/c.zig + lib/std/c/linux.zig + lib/std/coff.zig + lib/std/crypto.zig + lib/std/crypto/blake3.zig + lib/std/crypto/siphash.zig + lib/std/debug.zig + lib/std/dwarf.zig + lib/std/dwarf/AT.zig + lib/std/dwarf/ATE.zig + lib/std/dwarf/FORM.zig + lib/std/dwarf/LANG.zig + lib/std/dwarf/OP.zig + lib/std/dwarf/TAG.zig + lib/std/elf.zig + lib/std/fifo.zig + lib/std/fmt.zig + lib/std/fmt/format_float.zig + lib/std/fmt/parse_float.zig + lib/std/fs.zig + lib/std/fs/AtomicFile.zig + lib/std/fs/Dir.zig + lib/std/fs/File.zig + lib/std/fs/get_app_data_dir.zig + lib/std/fs/path.zig + lib/std/hash.zig + lib/std/hash/auto_hash.zig + lib/std/hash/wyhash.zig + lib/std/hash_map.zig + lib/std/heap.zig + lib/std/heap/arena_allocator.zig + lib/std/io.zig + lib/std/io/Reader.zig + lib/std/io/Writer.zig + lib/std/io/buffered_atomic_file.zig + lib/std/io/buffered_writer.zig + lib/std/io/change_detection_stream.zig + lib/std/io/counting_reader.zig + lib/std/io/counting_writer.zig + lib/std/io/find_byte_writer.zig + lib/std/io/fixed_buffer_stream.zig + lib/std/io/limited_reader.zig + lib/std/io/seekable_stream.zig + lib/std/json.zig + lib/std/json/stringify.zig + lib/std/leb128.zig + lib/std/linked_list.zig + lib/std/log.zig + lib/std/macho.zig + lib/std/math.zig + lib/std/math/big.zig + lib/std/math/big/int.zig + lib/std/math/float.zig + lib/std/math/frexp.zig + lib/std/math/isinf.zig + lib/std/math/isnan.zig + lib/std/math/log.zig + lib/std/math/log10.zig + lib/std/math/log2.zig + lib/std/math/signbit.zig + lib/std/math/sqrt.zig + lib/std/mem.zig + lib/std/mem/Allocator.zig + lib/std/meta.zig + lib/std/meta/trailer_flags.zig + lib/std/multi_array_list.zig + lib/std/os.zig + lib/std/os/linux.zig + lib/std/os/linux.zig + lib/std/os/linux/IoUring.zig + lib/std/os/linux/io_uring_sqe.zig + lib/std/os/linux/x86_64.zig + lib/std/os/linux/x86_64.zig + lib/std/os/windows.zig + lib/std/os/windows/ntstatus.zig + lib/std/os/windows/win32error.zig + lib/std/pdb.zig + lib/std/process.zig + lib/std/process/Child.zig + lib/std/sort.zig + lib/std/start.zig + lib/std/static_string_map.zig + lib/std/std.zig lib/std/time.zig lib/std/treap.zig lib/std/unicode.zig diff --git a/lib/compiler/aro/aro/Driver.zig b/lib/compiler/aro/aro/Driver.zig index f3d5d5a988..66b1563e49 100644 --- a/lib/compiler/aro/aro/Driver.zig +++ b/lib/compiler/aro/aro/Driver.zig @@ -803,7 +803,7 @@ pub fn invokeLinker(d: *Driver, tc: *Toolchain, comptime fast_exit: bool) !void return d.fatal("unable to dump linker args: {s}", .{errorDescription(er)}); }; } - var child = std.ChildProcess.init(argv.items, d.comp.gpa); + var child = std.process.Child.init(argv.items, d.comp.gpa); // TODO handle better child.stdin_behavior = .Inherit; child.stdout_behavior = .Inherit; diff --git a/lib/compiler/std-docs.zig b/lib/compiler/std-docs.zig index 4642b4317f..eaabcfa936 100644 --- a/lib/compiler/std-docs.zig +++ b/lib/compiler/std-docs.zig @@ -302,7 +302,7 @@ fn buildWasmBinary( "--listen=-", }); - var child = std.ChildProcess.init(argv.items, gpa); + var child = std.process.Child.init(argv.items, gpa); child.stdin_behavior = .Pipe; child.stdout_behavior = .Pipe; child.stderr_behavior = .Pipe; @@ -437,7 +437,7 @@ fn openBrowserTabThread(gpa: Allocator, url: []const u8) !void { .macos => "open", else => "xdg-open", }; - var child = std.ChildProcess.init(&.{ main_exe, url }, gpa); + var child = std.process.Child.init(&.{ main_exe, url }, gpa); child.stdin_behavior = .Ignore; child.stdout_behavior = .Ignore; child.stderr_behavior = .Ignore; diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 6c8ba75730..a2e8a7c564 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -178,7 +178,7 @@ pub const RunError = error{ ExitCodeFailure, ProcessTerminated, ExecNotSupported, -} || std.ChildProcess.SpawnError; +} || std.process.Child.SpawnError; pub const PkgConfigError = error{ PkgConfigCrashed, @@ -1719,7 +1719,7 @@ pub fn runAllowFail( b: *Build, argv: []const []const u8, out_code: *u8, - stderr_behavior: std.ChildProcess.StdIo, + stderr_behavior: std.process.Child.StdIo, ) RunError![]u8 { assert(argv.len != 0); @@ -1727,7 +1727,7 @@ pub fn runAllowFail( return error.ExecNotSupported; const max_output_size = 400 * 1024; - var child = std.ChildProcess.init(argv, b.allocator); + var child = std.process.Child.init(argv, b.allocator); child.stdin_behavior = .Ignore; child.stdout_behavior = .Pipe; child.stderr_behavior = stderr_behavior; diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index b6fcd4ac7a..01bea6c0ce 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -275,7 +275,7 @@ pub fn evalChildProcess(s: *Step, argv: []const []const u8) !void { try handleChildProcUnsupported(s, null, argv); try handleVerbose(s.owner, null, argv); - const result = std.ChildProcess.run(.{ + const result = std.process.Child.run(.{ .allocator = arena, .argv = argv, }) catch |err| return s.fail("unable to spawn {s}: {s}", .{ argv[0], @errorName(err) }); @@ -313,7 +313,7 @@ pub fn evalZigProcess( try handleChildProcUnsupported(s, null, argv); try handleVerbose(s.owner, null, argv); - var child = std.ChildProcess.init(argv, arena); + var child = std.process.Child.init(argv, arena); child.env_map = &b.graph.env_map; child.stdin_behavior = .Pipe; child.stdout_behavior = .Pipe; @@ -480,7 +480,7 @@ pub inline fn handleChildProcUnsupported( pub fn handleChildProcessTerm( s: *Step, - term: std.ChildProcess.Term, + term: std.process.Child.Term, opt_cwd: ?[]const u8, argv: []const []const u8, ) error{ MakeFailed, OutOfMemory }!void { diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig deleted file mode 100644 index 3c1b01d3b8..0000000000 --- a/lib/std/child_process.zig +++ /dev/null @@ -1,1880 +0,0 @@ -const std = @import("std.zig"); -const builtin = @import("builtin"); -const unicode = std.unicode; -const io = std.io; -const fs = std.fs; -const process = std.process; -const File = std.fs.File; -const windows = std.os.windows; -const linux = std.os.linux; -const posix = std.posix; -const mem = std.mem; -const math = std.math; -const debug = std.debug; -const EnvMap = process.EnvMap; -const maxInt = std.math.maxInt; -const assert = std.debug.assert; -const native_os = builtin.os.tag; - -pub const ChildProcess = struct { - pub const Id = switch (native_os) { - .windows => windows.HANDLE, - .wasi => void, - else => posix.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 (native_os == .windows) windows.HANDLE else void, - - allocator: mem.Allocator, - - /// The writing end of the child process's standard input pipe. - /// Usage requires `stdin_behavior == StdIo.Pipe`. - /// Available after calling `spawn()`. - stdin: ?File, - - /// The reading end of the child process's standard output pipe. - /// Usage requires `stdout_behavior == StdIo.Pipe`. - /// Available after calling `spawn()`. - stdout: ?File, - - /// The reading end of the child process's standard error pipe. - /// Usage requires `stderr_behavior == StdIo.Pipe`. - /// Available after calling `spawn()`. - stderr: ?File, - - /// Terminated state of the child process. - /// Available after calling `wait()`. - 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 (native_os == .windows or native_os == .wasi) void else ?posix.uid_t, - - /// Set to change the group id when spawning the child process. - gid: if (native_os == .windows or native_os == .wasi) void else ?posix.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 (native_os == .windows) void else [2]posix.fd_t, - - expand_arg0: Arg0Expand, - - /// Darwin-only. Disable ASLR for the child process. - disable_aslr: bool = false, - - /// Darwin-only. Start child process in suspended state as if SIGSTOP was sent. - start_suspended: bool = false, - - /// Set to true to obtain rusage information for the child process. - /// Depending on the target platform and implementation status, the - /// requested statistics may or may not be available. If they are - /// available, then the `resource_usage_statistics` field will be populated - /// after calling `wait`. - /// On Linux and Darwin, this obtains rusage statistics from wait4(). - request_resource_usage_statistics: bool = false, - - /// This is available after calling wait if - /// `request_resource_usage_statistics` was set to `true` before calling - /// `spawn`. - resource_usage_statistics: ResourceUsageStatistics = .{}, - - pub const ResourceUsageStatistics = struct { - rusage: @TypeOf(rusage_init) = rusage_init, - - /// Returns the peak resident set size of the child process, in bytes, - /// if available. - pub inline fn getMaxRss(rus: ResourceUsageStatistics) ?usize { - switch (native_os) { - .linux => { - if (rus.rusage) |ru| { - return @as(usize, @intCast(ru.maxrss)) * 1024; - } else { - return null; - } - }, - .windows => { - if (rus.rusage) |ru| { - return ru.PeakWorkingSetSize; - } else { - return null; - } - }, - .macos, .ios => { - if (rus.rusage) |ru| { - // Darwin oddly reports in bytes instead of kilobytes. - return @as(usize, @intCast(ru.maxrss)); - } else { - return null; - } - }, - else => return null, - } - } - - const rusage_init = switch (native_os) { - .linux, .macos, .ios => @as(?posix.rusage, null), - .windows => @as(?windows.VM_COUNTERS, null), - else => {}, - }; - }; - - pub const Arg0Expand = posix.Arg0Expand; - - pub const SpawnError = error{ - OutOfMemory, - - /// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV. - NoDevice, - - /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8. - /// https://simonsapin.github.io/wtf-8/ - InvalidWtf8, - - /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process. - CurrentWorkingDirectoryUnlinked, - - /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed - /// within arguments when executing a `.bat`/`.cmd` script. - /// - NUL/LF signifiies end of arguments, so anything afterwards - /// would be lost after execution. - /// - CR is stripped by `cmd.exe`, so any CR codepoints - /// would be lost after execution. - InvalidBatchScriptArg, - } || - posix.ExecveError || - posix.SetIdError || - posix.ChangeCurDirError || - windows.CreateProcessError || - windows.GetProcessMemoryInfoError || - windows.WaitForSingleObjectError; - - pub const Term = union(enum) { - Exited: u8, - Signal: u32, - Stopped: u32, - Unknown: u32, - }; - - /// Behavior of the child process's standard input, output, and error - /// streams. - pub const StdIo = enum { - /// Inherit the stream from the parent process. - Inherit, - - /// Pass a null stream to the child process. - /// This is /dev/null on POSIX and NUL on Windows. - Ignore, - - /// Create a pipe for the stream. - /// The corresponding field (`stdout`, `stderr`, or `stdin`) - /// will be assigned a `File` object that can be used - /// to read from or write to the pipe. - Pipe, - - /// Close the stream after the child process spawns. - Close, - }; - - /// First argument in argv is the executable. - pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess { - return .{ - .allocator = allocator, - .argv = argv, - .id = undefined, - .thread_handle = undefined, - .err_pipe = null, - .term = null, - .env_map = null, - .cwd = null, - .uid = if (native_os == .windows or native_os == .wasi) {} else null, - .gid = if (native_os == .windows or native_os == .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 std.process.getUserInfo(name); - self.uid = user_info.uid; - self.gid = user_info.gid; - } - - /// 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"); - } - - if (native_os == .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 (native_os == .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; - } - - windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) { - error.PermissionDenied => { - // Usually when TerminateProcess triggers a ACCESS_DENIED error, it - // indicates that the process has already exited, but there may be - // some rare edge cases where our process handle no longer has the - // PROCESS_TERMINATE access right, so let's do another check to make - // sure the process is really no longer running: - windows.WaitForSingleObjectEx(self.id, 0, false) catch return err; - return error.AlreadyTerminated; - }, - else => return err, - }; - try self.waitUnwrappedWindows(); - return self.term.?; - } - - pub fn killPosix(self: *ChildProcess) !Term { - if (self.term) |term| { - self.cleanupStreams(); - return term; - } - posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { - error.ProcessNotFound => return error.AlreadyTerminated, - else => return err, - }; - try self.waitUnwrapped(); - return self.term.?; - } - - /// Blocks until child process terminates and then cleans up all resources. - pub fn wait(self: *ChildProcess) !Term { - const term = if (native_os == .windows) - try self.waitWindows() - else - try self.waitPosix(); - - self.id = undefined; - - return term; - } - - pub const RunResult = struct { - term: Term, - stdout: []u8, - stderr: []u8, - }; - - fn fifoToOwnedArrayList(fifo: *std.io.PollFifo) std.ArrayList(u8) { - if (fifo.head > 0) { - @memcpy(fifo.buf[0..fifo.count], fifo.buf[fifo.head..][0..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. - /// - /// The process must be started with stdout_behavior and stderr_behavior == .Pipe - pub fn collectOutput( - child: ChildProcess, - stdout: *std.ArrayList(u8), - stderr: *std.ArrayList(u8), - max_output_bytes: usize, - ) !void { - debug.assert(child.stdout_behavior == .Pipe); - debug.assert(child.stderr_behavior == .Pipe); - - // we could make this work with multiple allocators but YAGNI - if (stdout.allocator.ptr != stderr.allocator.ptr or - stdout.allocator.vtable != stderr.allocator.vtable) - { - unreachable; // ChildProcess.collectOutput only supports 1 allocator - } - - var poller = std.io.poll(stdout.allocator, enum { stdout, stderr }, .{ - .stdout = child.stdout.?, - .stderr = child.stderr.?, - }); - defer poller.deinit(); - - 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; - } - - stdout.* = fifoToOwnedArrayList(poller.fifo(.stdout)); - stderr.* = fifoToOwnedArrayList(poller.fifo(.stderr)); - } - - pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.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. - pub fn run(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, - }) RunError!RunResult { - 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; - - var stdout = std.ArrayList(u8).init(args.allocator); - var stderr = std.ArrayList(u8).init(args.allocator); - errdefer { - stdout.deinit(); - stderr.deinit(); - } - - try child.spawn(); - try child.collectOutput(&stdout, &stderr, args.max_output_bytes); - - return RunResult{ - .term = try child.wait(), - .stdout = try stdout.toOwnedSlice(), - .stderr = try 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.id, windows.INFINITE, false); - - self.term = @as(SpawnError!Term, x: { - var exit_code: windows.DWORD = undefined; - if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) { - break :x Term{ .Unknown = 0 }; - } else { - break :x Term{ .Exited = @as(u8, @truncate(exit_code)) }; - } - }); - - if (self.request_resource_usage_statistics) { - self.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(self.id); - } - - posix.close(self.id); - posix.close(self.thread_handle); - self.cleanupStreams(); - return result; - } - - fn waitUnwrapped(self: *ChildProcess) !void { - const res: posix.WaitPidResult = res: { - if (self.request_resource_usage_statistics) { - switch (native_os) { - .linux, .macos, .ios => { - var ru: posix.rusage = undefined; - const res = posix.wait4(self.id, 0, &ru); - self.resource_usage_statistics.rusage = ru; - break :res res; - }, - else => {}, - } - } - - break :res posix.waitpid(self.id, 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 (native_os == .linux) { - var fd = [1]posix.pollfd{posix.pollfd{ - .fd = err_pipe[0], - .events = posix.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. - _ = posix.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 & posix.POLL.IN) != 0) { - const err_int = try readIntFd(err_pipe[0]); - return @as(SpawnError, @errorCast(@errorFromInt(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 @as(SpawnError, @errorCast(@errorFromInt(err_int))); - } - } - } - - return statusToTerm(status); - } - - fn statusToTerm(status: u32) Term { - return if (posix.W.IFEXITED(status)) - Term{ .Exited = posix.W.EXITSTATUS(status) } - else if (posix.W.IFSIGNALED(status)) - Term{ .Signal = posix.W.TERMSIG(status) } - else if (posix.W.IFSTOPPED(status)) - Term{ .Stopped = posix.W.STOPSIG(status) } - else - Term{ .Unknown = status }; - } - - fn spawnPosix(self: *ChildProcess) SpawnError!void { - const pipe_flags: posix.O = .{}; - const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try posix.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 posix.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 posix.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) - posix.openZ("/dev/null", .{ .ACCMODE = .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.WouldBlock => unreachable, - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - } - else - undefined; - defer { - if (any_ignore) posix.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]const u8, self.argv.len, null); - for (self.argv, 0..) |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 @as([*:null]const ?[*:0]const u8, @ptrCast(std.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 (native_os == .linux) { - const fd = try posix.eventfd(0, linux.EFD.CLOEXEC); - // There's no distinction between the readable and the writeable - // end with eventfd - break :blk [2]posix.fd_t{ fd, fd }; - } else { - break :blk try posix.pipe2(.{ .CLOEXEC = true }); - } - }; - errdefer destroyPipe(err_pipe); - - const pid_result = try posix.fork(); - if (pid_result == 0) { - // we are the child - setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); - setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); - setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); - - if (self.stdin_behavior == .Pipe) { - posix.close(stdin_pipe[0]); - posix.close(stdin_pipe[1]); - } - if (self.stdout_behavior == .Pipe) { - posix.close(stdout_pipe[0]); - posix.close(stdout_pipe[1]); - } - if (self.stderr_behavior == .Pipe) { - posix.close(stderr_pipe[0]); - posix.close(stderr_pipe[1]); - } - - if (self.cwd_dir) |cwd| { - posix.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err); - } else if (self.cwd) |cwd| { - posix.chdir(cwd) catch |err| forkChildErrReport(err_pipe[1], err); - } - - if (self.gid) |gid| { - posix.setregid(gid, gid) catch |err| forkChildErrReport(err_pipe[1], err); - } - - if (self.uid) |uid| { - posix.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err); - } - - const err = switch (self.expand_arg0) { - .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), - .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), - }; - forkChildErrReport(err_pipe[1], err); - } - - // we are the parent - const pid: i32 = @intCast(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.id = pid; - self.err_pipe = err_pipe; - self.term = null; - - if (self.stdin_behavior == StdIo.Pipe) { - posix.close(stdin_pipe[0]); - } - if (self.stdout_behavior == StdIo.Pipe) { - posix.close(stdout_pipe[1]); - } - if (self.stderr_behavior == StdIo.Pipe) { - posix.close(stderr_pipe[1]); - } - } - - fn spawnWindows(self: *ChildProcess) SpawnError!void { - var 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.GENERIC_WRITE | windows.SYNCHRONIZE, - .share_access = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, - .sa = &saAttr, - .creation = windows.OPEN_EXISTING, - }) catch |err| switch (err) { - error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" - error.PipeBusy => return error.Unexpected, // not possible for "NUL" - error.FileNotFound => return error.Unexpected, // not possible for "NUL" - error.AccessDenied => return error.Unexpected, // not possible for "NUL" - error.NameTooLong => return error.Unexpected, // not possible for "NUL" - error.WouldBlock => return error.Unexpected, // not possible for "NUL" - error.NetworkNotFound => return error.Unexpected, // not possible for "NUL" - error.AntivirusInterference => return error.Unexpected, // not possible for "NUL" - else => |e| return e, - } - else - undefined; - defer { - if (any_ignore) posix.close(nul_handle); - } - - 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.stdout_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.stderr_behavior == StdIo.Pipe) { - windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); - }; - - 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.wtf8ToWtf16LeAllocZ(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; - - const app_name_wtf8 = self.argv[0]; - const app_name_is_absolute = fs.path.isAbsolute(app_name_wtf8); - - // the cwd set in ChildProcess is in effect when choosing the executable path - // to match posix semantics - var cwd_path_w_needs_free = false; - const cwd_path_w = x: { - // If the app name is absolute, then we need to use its dirname as the cwd - if (app_name_is_absolute) { - cwd_path_w_needs_free = true; - const dir = fs.path.dirname(app_name_wtf8).?; - break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, dir); - } else if (self.cwd) |cwd| { - cwd_path_w_needs_free = true; - break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd); - } else { - break :x &[_:0]u16{}; // empty for cwd - } - }; - defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); - - // If the app name has more than just a filename, then we need to separate that - // into the basename and dirname and use the dirname as an addition to the cwd - // path. This is because NtQueryDirectoryFile cannot accept FileName params with - // path separators. - const app_basename_wtf8 = fs.path.basename(app_name_wtf8); - // If the app name is absolute, then the cwd will already have the app's dirname in it, - // so only populate app_dirname if app name is a relative path with > 0 path separators. - const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_wtf8) else null; - const app_dirname_w: ?[:0]u16 = x: { - if (maybe_app_dirname_wtf8) |app_dirname_wtf8| { - break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_dirname_wtf8); - } - break :x null; - }; - defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); - - const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8); - defer self.allocator.free(app_name_w); - - run: { - const PATH: [:0]const u16 = std.process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; - const PATHEXT: [:0]const u16 = std.process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; - - // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules - // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously - // constructed arguments. - // - // We'll need to wait until we're actually trying to run the command to know for sure - // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually - // serializing the command line until we determine how it should be serialized. - var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv); - defer cmd_line_cache.deinit(); - - var app_buf = std.ArrayListUnmanaged(u16){}; - defer app_buf.deinit(self.allocator); - - try app_buf.appendSlice(self.allocator, app_name_w); - - var dir_buf = std.ArrayListUnmanaged(u16){}; - defer dir_buf.deinit(self.allocator); - - if (cwd_path_w.len > 0) { - try dir_buf.appendSlice(self.allocator, cwd_path_w); - } - if (app_dirname_w) |app_dir| { - if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); - try dir_buf.appendSlice(self.allocator, app_dir); - } - if (dir_buf.items.len > 0) { - // Need to normalize the path, openDirW can't handle things like double backslashes - const normalized_len = windows.normalizePath(u16, dir_buf.items) catch return error.BadPathName; - dir_buf.shrinkRetainingCapacity(normalized_len); - } - - windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| { - const original_err = switch (no_path_err) { - // argv[0] contains unsupported characters that will never resolve to a valid exe. - error.InvalidArg0 => return error.FileNotFound, - error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, - error.UnrecoverableInvalidExe => return error.InvalidExe, - else => |e| return e, - }; - - // If the app name had path separators, that disallows PATH searching, - // and there's no need to search the PATH if the app name is absolute. - // We still search the path if the cwd is absolute because of the - // "cwd set in ChildProcess is in effect when choosing the executable path - // to match posix semantics" behavior--we don't want to skip searching - // the PATH just because we were trying to set the cwd of the child process. - if (app_dirname_w != null or app_name_is_absolute) { - return original_err; - } - - var it = mem.tokenizeScalar(u16, PATH, ';'); - while (it.next()) |search_path| { - dir_buf.clearRetainingCapacity(); - try dir_buf.appendSlice(self.allocator, search_path); - // Need to normalize the path, some PATH values can contain things like double - // backslashes which openDirW can't handle - const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue; - dir_buf.shrinkRetainingCapacity(normalized_len); - - if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) { - break :run; - } else |err| switch (err) { - // argv[0] contains unsupported characters that will never resolve to a valid exe. - error.InvalidArg0 => return error.FileNotFound, - error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, - error.UnrecoverableInvalidExe => return error.InvalidExe, - else => |e| return e, - } - } else { - return original_err; - } - }; - } - - 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.id = piProcInfo.hProcess; - self.thread_handle = piProcInfo.hThread; - self.term = null; - - if (self.stdin_behavior == StdIo.Pipe) { - posix.close(g_hChildStd_IN_Rd.?); - } - if (self.stderr_behavior == StdIo.Pipe) { - posix.close(g_hChildStd_ERR_Wr.?); - } - if (self.stdout_behavior == StdIo.Pipe) { - posix.close(g_hChildStd_OUT_Wr.?); - } - } - - fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { - switch (stdio) { - .Pipe => try posix.dup2(pipe_fd, std_fileno), - .Close => posix.close(std_fileno), - .Inherit => {}, - .Ignore => try posix.dup2(dev_null_fd, std_fileno), - } - } -}; - -/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. -/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. -/// Note: `app_buf` should not contain any leading path separators. -/// Note: If the dir is the cwd, dir_buf should be empty (len = 0). -fn windowsCreateProcessPathExt( - allocator: mem.Allocator, - dir_buf: *std.ArrayListUnmanaged(u16), - app_buf: *std.ArrayListUnmanaged(u16), - pathext: [:0]const u16, - cmd_line_cache: *WindowsCommandLineCache, - envp_ptr: ?[*]u16, - cwd_ptr: ?[*:0]u16, - lpStartupInfo: *windows.STARTUPINFOW, - lpProcessInformation: *windows.PROCESS_INFORMATION, -) !void { - const app_name_len = app_buf.items.len; - const dir_path_len = dir_buf.items.len; - - if (app_name_len == 0) return error.FileNotFound; - - defer app_buf.shrinkRetainingCapacity(app_name_len); - defer dir_buf.shrinkRetainingCapacity(dir_path_len); - - // The name of the game here is to avoid CreateProcessW calls at all costs, - // and only ever try calling it when we have a real candidate for execution. - // Secondarily, we want to minimize the number of syscalls used when checking - // for each PATHEXT-appended version of the app name. - // - // An overview of the technique used: - // - Open the search directory for iteration (either cwd or a path from PATH) - // - Use NtQueryDirectoryFile with a wildcard filename of `*` to - // check if anything that could possibly match either the unappended version - // of the app name or any of the versions with a PATHEXT value appended exists. - // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early - // without needing to use PATHEXT at all. - // - // This allows us to use a sequence - // for any directory that doesn't contain any possible matches, instead of having - // to use a separate look up for each individual filename combination (unappended + - // each PATHEXT appended). For directories where the wildcard *does* match something, - // we iterate the matches and take note of any that are either the unappended version, - // or a version with a supported PATHEXT appended. We then try calling CreateProcessW - // with the found versions in the appropriate order. - - var dir = dir: { - // needs to be null-terminated - try dir_buf.append(allocator, 0); - defer dir_buf.shrinkRetainingCapacity(dir_path_len); - const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); - break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch - return error.FileNotFound; - }; - defer dir.close(); - - // Add wildcard and null-terminator - try app_buf.append(allocator, '*'); - try app_buf.append(allocator, 0); - const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; - - // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries - // returned per NtQueryDirectoryFile call. - var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined; - const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); - if (file_information_buf.len < file_info_maximum_single_entry_size) { - @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); - } - var io_status: windows.IO_STATUS_BLOCK = undefined; - - const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; - var pathext_seen = [_]bool{false} ** num_supported_pathext; - var any_pathext_seen = false; - var unappended_exists = false; - - // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions - // of the app_name we should try to spawn. - // Note: This is necessary because the order of the files returned is filesystem-dependent: - // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. - // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. - while (true) { - const app_name_len_bytes = math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; - var app_name_unicode_string = windows.UNICODE_STRING{ - .Length = app_name_len_bytes, - .MaximumLength = app_name_len_bytes, - .Buffer = @constCast(app_name_wildcard.ptr), - }; - const rc = windows.ntdll.NtQueryDirectoryFile( - dir.fd, - null, - null, - null, - &io_status, - &file_information_buf, - file_information_buf.len, - .FileDirectoryInformation, - windows.FALSE, // single result - &app_name_unicode_string, - windows.FALSE, // restart iteration - ); - - // If we get nothing with the wildcard, then we can just bail out - // as we know appending PATHEXT will not yield anything. - switch (rc) { - .SUCCESS => {}, - .NO_SUCH_FILE => return error.FileNotFound, - .NO_MORE_FILES => break, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - - // According to the docs, this can only happen if there is not enough room in the - // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. - // Therefore, this condition should not be possible to hit with the buffer size we use. - std.debug.assert(io_status.Information != 0); - - var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; - while (it.next()) |info| { - // Skip directories - if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; - const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; - // Because all results start with the app_name since we're using the wildcard `app_name*`, - // if the length is equal to app_name then this is an exact match - if (filename.len == app_name_len) { - // Note: We can't break early here because it's possible that the unappended version - // fails to spawn, in which case we still want to try the PATHEXT appended versions. - unappended_exists = true; - } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { - pathext_seen[@intFromEnum(pathext_ext)] = true; - any_pathext_seen = true; - } - } - } - - const unappended_err = unappended: { - if (unappended_exists) { - if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { - '/', '\\' => {}, - else => try dir_buf.append(allocator, fs.path.sep), - }; - try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); - try dir_buf.append(allocator, 0); - const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - - const is_bat_or_cmd = bat_or_cmd: { - const app_name = app_buf.items[0..app_name_len]; - const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false; - const ext = app_name[ext_start..]; - const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false; - switch (ext_enum) { - .cmd, .bat => break :bat_or_cmd true, - else => break :bat_or_cmd false, - } - }; - const cmd_line_w = if (is_bat_or_cmd) - try cmd_line_cache.scriptCommandLine(full_app_name) - else - try cmd_line_cache.commandLine(); - const app_name_w = if (is_bat_or_cmd) - try cmd_line_cache.cmdExePath() - else - full_app_name; - - if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { - return; - } else |err| switch (err) { - error.FileNotFound, - error.AccessDenied, - => break :unappended err, - error.InvalidExe => { - // On InvalidExe, if the extension of the app name is .exe then - // it's treated as an unrecoverable error. Otherwise, it'll be - // skipped as normal. - const app_name = app_buf.items[0..app_name_len]; - const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; - const ext = app_name[ext_start..]; - if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { - return error.UnrecoverableInvalidExe; - } - break :unappended err; - }, - else => return err, - } - } - break :unappended error.FileNotFound; - }; - - if (!any_pathext_seen) return unappended_err; - - // Now try any PATHEXT appended versions that we've seen - var ext_it = mem.tokenizeScalar(u16, pathext, ';'); - while (ext_it.next()) |ext| { - const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; - if (!pathext_seen[@intFromEnum(ext_enum)]) continue; - - dir_buf.shrinkRetainingCapacity(dir_path_len); - if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { - '/', '\\' => {}, - else => try dir_buf.append(allocator, fs.path.sep), - }; - try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); - try dir_buf.appendSlice(allocator, ext); - try dir_buf.append(allocator, 0); - const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - - const is_bat_or_cmd = switch (ext_enum) { - .cmd, .bat => true, - else => false, - }; - const cmd_line_w = if (is_bat_or_cmd) - try cmd_line_cache.scriptCommandLine(full_app_name) - else - try cmd_line_cache.commandLine(); - const app_name_w = if (is_bat_or_cmd) - try cmd_line_cache.cmdExePath() - else - full_app_name; - - if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { - return; - } else |err| switch (err) { - error.FileNotFound => continue, - error.AccessDenied => continue, - error.InvalidExe => { - // On InvalidExe, if the extension of the app name is .exe then - // it's treated as an unrecoverable error. Otherwise, it'll be - // skipped as normal. - if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { - return error.UnrecoverableInvalidExe; - } - continue; - }, - else => return err, - } - } - - return unappended_err; -} - -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, - @as(?*anyopaque, @ptrCast(envp_ptr)), - cwd_ptr, - lpStartupInfo, - lpProcessInformation, - ); -} - -// Should be kept in sync with `windowsCreateProcessSupportsExtension` -const CreateProcessSupportedExtension = enum { - bat, - cmd, - com, - exe, -}; - -/// Case-insensitive WTF-16 lookup -fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { - if (ext.len != 4) return null; - const State = enum { - start, - dot, - b, - ba, - c, - cm, - co, - e, - ex, - }; - var state: State = .start; - for (ext) |c| switch (state) { - .start => switch (c) { - '.' => state = .dot, - else => return null, - }, - .dot => switch (c) { - 'b', 'B' => state = .b, - 'c', 'C' => state = .c, - 'e', 'E' => state = .e, - else => return null, - }, - .b => switch (c) { - 'a', 'A' => state = .ba, - else => return null, - }, - .c => switch (c) { - 'm', 'M' => state = .cm, - 'o', 'O' => state = .co, - else => return null, - }, - .e => switch (c) { - 'x', 'X' => state = .ex, - else => return null, - }, - .ba => switch (c) { - 't', 'T' => return .bat, - else => return null, - }, - .cm => switch (c) { - 'd', 'D' => return .cmd, - else => return null, - }, - .co => switch (c) { - 'm', 'M' => return .com, - else => return null, - }, - .ex => switch (c) { - 'e', 'E' => return .exe, - else => return null, - }, - }; - return null; -} - -test windowsCreateProcessSupportsExtension { - try std.testing.expectEqual(CreateProcessSupportedExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?); - try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); -} - -/// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW. -/// -/// Serialization is done on-demand and the result is cached in order to allow for: -/// - Only serializing the particular type of command line needed (`.bat`/`.cmd` -/// command line serialization is different from `.exe`/etc) -/// - Reusing the serialized command lines if necessary (i.e. if the execution -/// of a command fails and the PATH is going to be continued to be searched -/// for more candidates) -pub const WindowsCommandLineCache = struct { - cmd_line: ?[:0]u16 = null, - script_cmd_line: ?[:0]u16 = null, - cmd_exe_path: ?[:0]u16 = null, - argv: []const []const u8, - allocator: mem.Allocator, - - pub fn init(allocator: mem.Allocator, argv: []const []const u8) WindowsCommandLineCache { - return .{ - .allocator = allocator, - .argv = argv, - }; - } - - pub fn deinit(self: *WindowsCommandLineCache) void { - if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line); - if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line); - if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path); - } - - pub fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 { - if (self.cmd_line == null) { - self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv); - } - return self.cmd_line.?; - } - - /// Not cached, since the path to the batch script will change during PATH searching. - /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched, - /// then script_path should include both the search path and the script filename - /// (this allows avoiding cmd.exe having to search the PATH again). - pub fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 { - if (self.script_cmd_line) |v| self.allocator.free(v); - self.script_cmd_line = try argvToScriptCommandLineWindows( - self.allocator, - script_path, - self.argv[1..], - ); - return self.script_cmd_line.?; - } - - pub fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 { - if (self.cmd_exe_path == null) { - self.cmd_exe_path = try windowsCmdExePath(self.allocator); - } - return self.cmd_exe_path.?; - } -}; - -pub fn windowsCmdExePath(allocator: mem.Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { - var buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 128); - errdefer buf.deinit(allocator); - while (true) { - const unused_slice = buf.unusedCapacitySlice(); - // TODO: Get the system directory from PEB.ReadOnlyStaticServerData - const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len)); - if (len == 0) { - switch (windows.kernel32.GetLastError()) { - else => |err| return windows.unexpectedError(err), - } - } - if (len > unused_slice.len) { - try buf.ensureUnusedCapacity(allocator, len); - } else { - buf.items.len = len; - break; - } - } - switch (buf.items[buf.items.len - 1]) { - '/', '\\' => {}, - else => try buf.append(allocator, fs.path.sep), - } - try buf.appendSlice(allocator, std.unicode.utf8ToUtf16LeStringLiteral("cmd.exe")); - return try buf.toOwnedSliceSentinel(allocator, 0); -} - -pub const ArgvToScriptCommandLineError = error{ - OutOfMemory, - InvalidWtf8, - /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed - /// within arguments when executing a `.bat`/`.cmd` script. - /// - NUL/LF signifiies end of arguments, so anything afterwards - /// would be lost after execution. - /// - CR is stripped by `cmd.exe`, so any CR codepoints - /// would be lost after execution. - InvalidBatchScriptArg, -}; - -/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific -/// escaping rules. The caller owns the returned slice. -/// -/// Escapes `argv` using the suggested mitigation against arbitrary command execution from: -/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ -pub fn argvToScriptCommandLineWindows( - allocator: mem.Allocator, - /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD. - /// The script must have been verified to exist at this path before calling this function. - script_path: []const u16, - /// Arguments, not including the script name itself. Expected to be encoded as WTF-8. - script_args: []const []const u8, -) ArgvToScriptCommandLineError![:0]u16 { - var buf = try std.ArrayList(u8).initCapacity(allocator, 64); - defer buf.deinit(); - - // `/d` disables execution of AutoRun commands. - // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation: - // > If delayed expansion is enabled via the registry value DelayedExpansion, - // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option. - // > Escaping for % requires the command extension to be enabled. - // > If itโ€™s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option. - // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ - buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \""); - - // Always quote the path to the script arg - buf.appendAssumeCapacity('"'); - // We always want the path to the batch script to include a path separator in order to - // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary - // command execution mitigation, we just know exactly what script we want to execute - // at this point, and potentially making cmd.exe re-find it is unnecessary. - // - // If the script path does not have a path separator, then we know its relative to CWD and - // we can just put `.\` in the front. - if (mem.indexOfAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) { - try buf.appendSlice(".\\"); - } - // Note that we don't do any escaping/mitigations for this argument, since the relevant - // characters (", %, etc) are illegal in file paths and this function should only be called - // with script paths that have been verified to exist. - try std.unicode.wtf16LeToWtf8ArrayList(&buf, script_path); - buf.appendAssumeCapacity('"'); - - for (script_args) |arg| { - // Literal carriage returns get stripped when run through cmd.exe - // and NUL/newlines act as 'end of command.' Because of this, it's basically - // always a mistake to include these characters in argv, so it's - // an error condition in order to ensure that the return of this - // function can always roundtrip through cmd.exe. - if (std.mem.indexOfAny(u8, arg, "\x00\r\n") != null) { - return error.InvalidBatchScriptArg; - } - - // Separate args with a space. - try buf.append(' '); - - // Need to quote if the argument is empty (otherwise the arg would just be lost) - // or if the last character is a `\`, since then something like "%~2" in a .bat - // script would cause the closing " to be escaped which we don't want. - var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\'; - if (!needs_quotes) { - for (arg) |c| { - switch (c) { - // Known good characters that don't need to be quoted - 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {}, - // When in doubt, quote - else => { - needs_quotes = true; - break; - }, - } - } - } - if (needs_quotes) { - try buf.append('"'); - } - var backslashes: usize = 0; - for (arg) |c| { - switch (c) { - '\\' => { - backslashes += 1; - }, - '"' => { - try buf.appendNTimes('\\', backslashes); - try buf.append('"'); - backslashes = 0; - }, - // Replace `%` with `%%cd:~,%`. - // - // cmd.exe allows extracting a substring from an environment - // variable with the syntax: `%foo:~,%`. - // Therefore, `%cd:~,%` will always expand to an empty string - // since both the start and end index are blank, and it is assumed - // that `%cd%` is always available since it is a built-in variable - // that corresponds to the current directory. - // - // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%` - // will stop `%foo%` from being expanded and *after* expansion - // we'll still be left with `%foo%` (the literal string). - '%' => { - // the trailing `%` is appended outside the switch - try buf.appendSlice("%%cd:~,"); - backslashes = 0; - }, - else => { - backslashes = 0; - }, - } - try buf.append(c); - } - if (needs_quotes) { - try buf.appendNTimes('\\', backslashes); - try buf.append('"'); - } - } - - try buf.append('"'); - - return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); -} - -pub const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 }; - -/// Serializes `argv` to a Windows command-line string suitable for passing to a child process and -/// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice. -pub fn argvToCommandLineWindows( - allocator: mem.Allocator, - argv: []const []const u8, -) ArgvToCommandLineError![:0]u16 { - var buf = std.ArrayList(u8).init(allocator); - defer buf.deinit(); - - if (argv.len != 0) { - const arg0 = argv[0]; - - // The first argument must be quoted if it contains spaces or ASCII control characters - // (excluding DEL). It also follows special quoting rules where backslashes have no special - // interpretation, which makes it impossible to pass certain first arguments containing - // double quotes to a child process without characters from the first argument leaking into - // subsequent ones (which could have security implications). - // - // Empty arguments technically don't need quotes, but we quote them anyway for maximum - // compatibility with different implementations of the 'CommandLineToArgvW' algorithm. - // - // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject - // all first arguments containing double quotes, even ones that we could theoretically - // serialize in unquoted form. - var needs_quotes = arg0.len == 0; - for (arg0) |c| { - if (c <= ' ') { - needs_quotes = true; - } else if (c == '"') { - return error.InvalidArg0; - } - } - if (needs_quotes) { - try buf.append('"'); - try buf.appendSlice(arg0); - try buf.append('"'); - } else { - try buf.appendSlice(arg0); - } - - for (argv[1..]) |arg| { - try buf.append(' '); - - // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes, - // or if they are empty. For simplicity and for maximum compatibility with different - // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII - // control characters (again, excluding DEL). - needs_quotes = for (arg) |c| { - if (c <= ' ' or c == '"') { - break true; - } - } else arg.len == 0; - if (!needs_quotes) { - 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 try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); -} - -test argvToCommandLineWindows { - const t = testArgvToCommandLineWindows; - - try t(&.{ - \\C:\Program Files\zig\zig.exe - , - \\run - , - \\.\src\main.zig - , - \\-target - , - \\x86_64-windows-gnu - , - \\-O - , - \\ReleaseSafe - , - \\-- - , - \\--emoji=๐Ÿ—ฟ - , - \\--eval=new Regex("Dwayne \"The Rock\" Johnson") - , - }, - \\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=๐Ÿ—ฟ "--eval=new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")" - ); - - try t(&.{}, ""); - try t(&.{""}, "\"\""); - try t(&.{" "}, "\" \""); - try t(&.{"\t"}, "\"\t\""); - try t(&.{"\x07"}, "\"\x07\""); - try t(&.{"๐ŸฆŽ"}, "๐ŸฆŽ"); - - try t( - &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" }, - "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee", - ); - - try t( - &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" }, - "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"", - ); - - try std.testing.expectError( - error.InvalidArg0, - argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}), - ); - try std.testing.expectError( - error.InvalidArg0, - argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}), - ); - try std.testing.expectError( - error.InvalidArg0, - argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}), - ); -} - -fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void { - const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); - defer std.testing.allocator.free(cmd_line_w); - - const cmd_line = try unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w); - defer std.testing.allocator.free(cmd_line); - - try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); -} - -fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { - if (rd) |h| posix.close(h); - if (wr) |h| posix.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.Value(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.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, - ) catch unreachable; - const len = std.unicode.wtf8ToWtf16Le(&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 posix.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 posix.close(write_handle); - - try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); - - rd.* = read_handle; - wr.* = write_handle; -} - -fn destroyPipe(pipe: [2]posix.fd_t) void { - posix.close(pipe[0]); - if (pipe[0] != pipe[1]) posix.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, @intFromError(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); - } - posix.exit(1); -} - -const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); - -fn writeIntFd(fd: i32, value: ErrInt) !void { - const file = File{ .handle = fd }; - file.writer().writeInt(u64, @intCast(value), .little) catch return error.SystemResources; -} - -fn readIntFd(fd: i32) !ErrInt { - const file = File{ .handle = fd }; - return @as(ErrInt, @intCast(file.reader().readInt(u64, .little) 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.wtf8ToWtf16Le(result[i..], pair.key_ptr.*); - result[i] = '='; - i += 1; - i += try unicode.wtf8ToWtf16Le(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 try allocator.realloc(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); - @memcpy(env_buf[0..pair.key_ptr.len], pair.key_ptr.*); - env_buf[pair.key_ptr.len] = '='; - @memcpy(env_buf[pair.key_ptr.len + 1 ..][0..pair.value_ptr.len], 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 - } - } -} diff --git a/lib/std/process.zig b/lib/std/process.zig index 5007c86736..5bdee4971d 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -6,12 +6,12 @@ const math = std.math; const Allocator = mem.Allocator; const assert = std.debug.assert; const testing = std.testing; -const child_process = @import("child_process.zig"); const native_os = builtin.os.tag; const posix = std.posix; const windows = std.os.windows; +const unicode = std.unicode; -pub const Child = child_process.ChildProcess; +pub const Child = @import("process/Child.zig"); pub const abort = posix.abort; pub const exit = posix.exit; pub const changeCurDir = posix.chdir; @@ -86,7 +86,7 @@ pub const EnvMap = struct { _ = self; if (native_os == .windows) { var h = std.hash.Wyhash.init(0); - var it = std.unicode.Wtf8View.initUnchecked(s).iterator(); + var it = unicode.Wtf8View.initUnchecked(s).iterator(); while (it.nextCodepoint()) |cp| { const cp_upper = upcase(cp); h.update(&[_]u8{ @@ -103,8 +103,8 @@ pub const EnvMap = struct { pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { _ = self; if (native_os == .windows) { - var it_a = std.unicode.Wtf8View.initUnchecked(a).iterator(); - var it_b = std.unicode.Wtf8View.initUnchecked(b).iterator(); + var it_a = unicode.Wtf8View.initUnchecked(a).iterator(); + var it_b = unicode.Wtf8View.initUnchecked(b).iterator(); while (true) { const c_a = it_a.nextCodepoint() orelse break; const c_b = it_b.nextCodepoint() orelse return false; @@ -141,7 +141,7 @@ pub const EnvMap = struct { /// If `putMove` fails, the ownership of key and value does not transfer. /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. pub fn putMove(self: *EnvMap, key: []u8, value: []u8) !void { - assert(std.unicode.wtf8ValidateSlice(key)); + assert(unicode.wtf8ValidateSlice(key)); const get_or_put = try self.hash_map.getOrPut(key); if (get_or_put.found_existing) { self.free(get_or_put.key_ptr.*); @@ -154,7 +154,7 @@ pub const EnvMap = struct { /// `key` and `value` are copied into the EnvMap. /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. pub fn put(self: *EnvMap, key: []const u8, value: []const u8) !void { - assert(std.unicode.wtf8ValidateSlice(key)); + assert(unicode.wtf8ValidateSlice(key)); const value_copy = try self.copy(value); errdefer self.free(value_copy); const get_or_put = try self.hash_map.getOrPut(key); @@ -173,7 +173,7 @@ pub const EnvMap = struct { /// The returned pointer is invalidated if the map resizes. /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. pub fn getPtr(self: EnvMap, key: []const u8) ?*[]const u8 { - assert(std.unicode.wtf8ValidateSlice(key)); + assert(unicode.wtf8ValidateSlice(key)); return self.hash_map.getPtr(key); } @@ -182,7 +182,7 @@ pub const EnvMap = struct { /// key is removed from the map. /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. pub fn get(self: EnvMap, key: []const u8) ?[]const u8 { - assert(std.unicode.wtf8ValidateSlice(key)); + assert(unicode.wtf8ValidateSlice(key)); return self.hash_map.get(key); } @@ -190,7 +190,7 @@ pub const EnvMap = struct { /// This invalidates the value returned by get() for this key. /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. pub fn remove(self: *EnvMap, key: []const u8) void { - assert(std.unicode.wtf8ValidateSlice(key)); + assert(unicode.wtf8ValidateSlice(key)); const kv = self.hash_map.fetchRemove(key) orelse return; self.free(kv.key); self.free(kv.value); @@ -260,7 +260,7 @@ test EnvMap { try testing.expectEqualStrings("something else", env.get("ะบะธั€ะธะปะปะธั†ะฐ").?); // and WTF-8 that's not valid UTF-8 - const wtf8_with_surrogate_pair = try std.unicode.wtf16LeToWtf8Alloc(testing.allocator, &[_]u16{ + const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(testing.allocator, &[_]u16{ std.mem.nativeToLittle(u16, 0xD83D), // unpaired high surrogate }); defer testing.allocator.free(wtf8_with_surrogate_pair); @@ -300,7 +300,7 @@ pub fn getEnvMap(allocator: Allocator) GetEnvMapError!EnvMap { while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {} const key_w = ptr[key_start..i]; - const key = try std.unicode.wtf16LeToWtf8Alloc(allocator, key_w); + const key = try unicode.wtf16LeToWtf8Alloc(allocator, key_w); errdefer allocator.free(key); if (ptr[i] == '=') i += 1; @@ -308,7 +308,7 @@ pub fn getEnvMap(allocator: Allocator) GetEnvMapError!EnvMap { const value_start = i; while (ptr[i] != 0) : (i += 1) {} const value_w = ptr[value_start..i]; - const value = try std.unicode.wtf16LeToWtf8Alloc(allocator, value_w); + const value = try unicode.wtf16LeToWtf8Alloc(allocator, value_w); errdefer allocator.free(value); i += 1; // skip over null byte @@ -401,13 +401,13 @@ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError const result_w = blk: { var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator); const stack_allocator = stack_alloc.get(); - const key_w = try std.unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); + const key_w = try unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); defer stack_allocator.free(key_w); break :blk getenvW(key_w) orelse return error.EnvironmentVariableNotFound; }; // wtf16LeToWtf8Alloc can only fail with OutOfMemory - return std.unicode.wtf16LeToWtf8Alloc(allocator, result_w); + return unicode.wtf16LeToWtf8Alloc(allocator, result_w); } else if (native_os == .wasi and !builtin.link_libc) { var envmap = getEnvMap(allocator) catch return error.OutOfMemory; defer envmap.deinit(); @@ -422,7 +422,7 @@ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError /// On Windows, `key` must be valid UTF-8. pub fn hasEnvVarConstant(comptime key: []const u8) bool { if (native_os == .windows) { - const key_w = comptime std.unicode.utf8ToUtf16LeStringLiteral(key); + const key_w = comptime unicode.utf8ToUtf16LeStringLiteral(key); return getenvW(key_w) != null; } else if (native_os == .wasi and !builtin.link_libc) { @compileError("hasEnvVarConstant is not supported for WASI without libc"); @@ -445,7 +445,7 @@ pub fn hasEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!bool { if (native_os == .windows) { var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator); const stack_allocator = stack_alloc.get(); - const key_w = try std.unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); + const key_w = try unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); defer stack_allocator.free(key_w); return getenvW(key_w) != null; } else if (native_os == .wasi and !builtin.link_libc) { @@ -659,7 +659,7 @@ pub const ArgIteratorWindows = struct { /// The iterator makes a copy of `cmd_line_w` converted WTF-8 and keeps it; it does *not* take /// ownership of `cmd_line_w`. pub fn init(allocator: Allocator, cmd_line_w: [*:0]const u16) InitError!ArgIteratorWindows { - const cmd_line = try std.unicode.wtf16LeToWtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)); + const cmd_line = try unicode.wtf16LeToWtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)); errdefer allocator.free(cmd_line); const buffer = try allocator.alloc(u8, cmd_line.len + 1); @@ -728,17 +728,17 @@ pub const ArgIteratorWindows = struct { // must be 6 bytes for a surrogate pair to exist. if (self.end - self.start >= 6) { const window = self.buffer[self.end - 6 .. self.end]; - const view = std.unicode.Wtf8View.init(window) catch return; + const view = unicode.Wtf8View.init(window) catch return; var it = view.iterator(); var pair: [2]u16 = undefined; pair[0] = std.mem.nativeToLittle(u16, std.math.cast(u16, it.nextCodepoint().?) orelse return); - if (!std.unicode.utf16IsHighSurrogate(std.mem.littleToNative(u16, pair[0]))) return; + if (!unicode.utf16IsHighSurrogate(std.mem.littleToNative(u16, pair[0]))) return; pair[1] = std.mem.nativeToLittle(u16, std.math.cast(u16, it.nextCodepoint().?) orelse return); - if (!std.unicode.utf16IsLowSurrogate(std.mem.littleToNative(u16, pair[1]))) return; + if (!unicode.utf16IsLowSurrogate(std.mem.littleToNative(u16, pair[1]))) return; // We know we have a valid surrogate pair, so convert // it to UTF-8, overwriting the surrogate pair's bytes // and then chop off the extra bytes. - const len = std.unicode.utf16LeToUtf8(window, &pair) catch unreachable; + const len = unicode.utf16LeToUtf8(window, &pair) catch unreachable; const delta = 6 - len; self.end -= delta; } @@ -1341,7 +1341,7 @@ test ArgIteratorWindows { } fn testArgIteratorWindows(cmd_line: []const u8, expected_args: []const []const u8) !void { - const cmd_line_w = try std.unicode.wtf8ToWtf16LeAllocZ(testing.allocator, cmd_line); + const cmd_line_w = try unicode.wtf8ToWtf16LeAllocZ(testing.allocator, cmd_line); defer testing.allocator.free(cmd_line_w); // next @@ -1642,7 +1642,7 @@ pub fn execve( const envp = m: { if (env_map) |m| { - const envp_buf = try child_process.createNullDelimitedEnvMap(arena, m); + const envp_buf = try createNullDelimitedEnvMap(arena, m); break :m envp_buf.ptr; } else if (builtin.link_libc) { break :m std.c.environ; @@ -1789,3 +1789,90 @@ pub fn raiseFileDescriptorLimit() void { test raiseFileDescriptorLimit { raiseFileDescriptorLimit(); } + +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); + @memcpy(env_buf[0..pair.key_ptr.len], pair.key_ptr.*); + env_buf[pair.key_ptr.len] = '='; + @memcpy(env_buf[pair.key_ptr.len + 1 ..][0..pair.value_ptr.len], pair.value_ptr.*); + envp_buf[i] = env_buf.ptr; + } + assert(i == envp_count); + } + return envp_buf; +} + +test createNullDelimitedEnvMap { + 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 + } + } +} + +/// 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.wtf8ToWtf16Le(result[i..], pair.key_ptr.*); + result[i] = '='; + i += 1; + i += try unicode.wtf8ToWtf16Le(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 try allocator.realloc(result, i); +} diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig new file mode 100644 index 0000000000..f7f65d7e85 --- /dev/null +++ b/lib/std/process/Child.zig @@ -0,0 +1,1795 @@ +const std = @import("../std.zig"); +const builtin = @import("builtin"); +const unicode = std.unicode; +const fs = std.fs; +const process = std.process; +const File = std.fs.File; +const windows = std.os.windows; +const linux = std.os.linux; +const posix = std.posix; +const mem = std.mem; +const EnvMap = std.process.EnvMap; +const maxInt = std.math.maxInt; +const assert = std.debug.assert; +const native_os = builtin.os.tag; +const ChildProcess = @This(); + +pub const Id = switch (native_os) { + .windows => windows.HANDLE, + .wasi => void, + else => posix.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 (native_os == .windows) windows.HANDLE else void, + +allocator: mem.Allocator, + +/// The writing end of the child process's standard input pipe. +/// Usage requires `stdin_behavior == StdIo.Pipe`. +/// Available after calling `spawn()`. +stdin: ?File, + +/// The reading end of the child process's standard output pipe. +/// Usage requires `stdout_behavior == StdIo.Pipe`. +/// Available after calling `spawn()`. +stdout: ?File, + +/// The reading end of the child process's standard error pipe. +/// Usage requires `stderr_behavior == StdIo.Pipe`. +/// Available after calling `spawn()`. +stderr: ?File, + +/// Terminated state of the child process. +/// Available after calling `wait()`. +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 (native_os == .windows or native_os == .wasi) void else ?posix.uid_t, + +/// Set to change the group id when spawning the child process. +gid: if (native_os == .windows or native_os == .wasi) void else ?posix.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 (native_os == .windows) void else [2]posix.fd_t, + +expand_arg0: Arg0Expand, + +/// Darwin-only. Disable ASLR for the child process. +disable_aslr: bool = false, + +/// Darwin-only. Start child process in suspended state as if SIGSTOP was sent. +start_suspended: bool = false, + +/// Set to true to obtain rusage information for the child process. +/// Depending on the target platform and implementation status, the +/// requested statistics may or may not be available. If they are +/// available, then the `resource_usage_statistics` field will be populated +/// after calling `wait`. +/// On Linux and Darwin, this obtains rusage statistics from wait4(). +request_resource_usage_statistics: bool = false, + +/// This is available after calling wait if +/// `request_resource_usage_statistics` was set to `true` before calling +/// `spawn`. +resource_usage_statistics: ResourceUsageStatistics = .{}, + +pub const ResourceUsageStatistics = struct { + rusage: @TypeOf(rusage_init) = rusage_init, + + /// Returns the peak resident set size of the child process, in bytes, + /// if available. + pub inline fn getMaxRss(rus: ResourceUsageStatistics) ?usize { + switch (native_os) { + .linux => { + if (rus.rusage) |ru| { + return @as(usize, @intCast(ru.maxrss)) * 1024; + } else { + return null; + } + }, + .windows => { + if (rus.rusage) |ru| { + return ru.PeakWorkingSetSize; + } else { + return null; + } + }, + .macos, .ios => { + if (rus.rusage) |ru| { + // Darwin oddly reports in bytes instead of kilobytes. + return @as(usize, @intCast(ru.maxrss)); + } else { + return null; + } + }, + else => return null, + } + } + + const rusage_init = switch (native_os) { + .linux, .macos, .ios => @as(?posix.rusage, null), + .windows => @as(?windows.VM_COUNTERS, null), + else => {}, + }; +}; + +pub const Arg0Expand = posix.Arg0Expand; + +pub const SpawnError = error{ + OutOfMemory, + + /// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV. + NoDevice, + + /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8. + /// https://simonsapin.github.io/wtf-8/ + InvalidWtf8, + + /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process. + CurrentWorkingDirectoryUnlinked, + + /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed + /// within arguments when executing a `.bat`/`.cmd` script. + /// - NUL/LF signifiies end of arguments, so anything afterwards + /// would be lost after execution. + /// - CR is stripped by `cmd.exe`, so any CR codepoints + /// would be lost after execution. + InvalidBatchScriptArg, +} || + posix.ExecveError || + posix.SetIdError || + posix.ChangeCurDirError || + windows.CreateProcessError || + windows.GetProcessMemoryInfoError || + windows.WaitForSingleObjectError; + +pub const Term = union(enum) { + Exited: u8, + Signal: u32, + Stopped: u32, + Unknown: u32, +}; + +/// Behavior of the child process's standard input, output, and error +/// streams. +pub const StdIo = enum { + /// Inherit the stream from the parent process. + Inherit, + + /// Pass a null stream to the child process. + /// This is /dev/null on POSIX and NUL on Windows. + Ignore, + + /// Create a pipe for the stream. + /// The corresponding field (`stdout`, `stderr`, or `stdin`) + /// will be assigned a `File` object that can be used + /// to read from or write to the pipe. + Pipe, + + /// Close the stream after the child process spawns. + Close, +}; + +/// First argument in argv is the executable. +pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess { + return .{ + .allocator = allocator, + .argv = argv, + .id = undefined, + .thread_handle = undefined, + .err_pipe = null, + .term = null, + .env_map = null, + .cwd = null, + .uid = if (native_os == .windows or native_os == .wasi) {} else null, + .gid = if (native_os == .windows or native_os == .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 process.getUserInfo(name); + self.uid = user_info.uid; + self.gid = user_info.gid; +} + +/// On success must call `kill` or `wait`. +/// After spawning the `id` is available. +pub fn spawn(self: *ChildProcess) SpawnError!void { + if (!process.can_spawn) { + @compileError("the target operating system cannot spawn processes"); + } + + if (native_os == .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 (native_os == .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; + } + + windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) { + error.PermissionDenied => { + // Usually when TerminateProcess triggers a ACCESS_DENIED error, it + // indicates that the process has already exited, but there may be + // some rare edge cases where our process handle no longer has the + // PROCESS_TERMINATE access right, so let's do another check to make + // sure the process is really no longer running: + windows.WaitForSingleObjectEx(self.id, 0, false) catch return err; + return error.AlreadyTerminated; + }, + else => return err, + }; + try self.waitUnwrappedWindows(); + return self.term.?; +} + +pub fn killPosix(self: *ChildProcess) !Term { + if (self.term) |term| { + self.cleanupStreams(); + return term; + } + posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { + error.ProcessNotFound => return error.AlreadyTerminated, + else => return err, + }; + try self.waitUnwrapped(); + return self.term.?; +} + +/// Blocks until child process terminates and then cleans up all resources. +pub fn wait(self: *ChildProcess) !Term { + const term = if (native_os == .windows) + try self.waitWindows() + else + try self.waitPosix(); + + self.id = undefined; + + return term; +} + +pub const RunResult = struct { + term: Term, + stdout: []u8, + stderr: []u8, +}; + +fn fifoToOwnedArrayList(fifo: *std.io.PollFifo) std.ArrayList(u8) { + if (fifo.head > 0) { + @memcpy(fifo.buf[0..fifo.count], fifo.buf[fifo.head..][0..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. +/// +/// The process must be started with stdout_behavior and stderr_behavior == .Pipe +pub fn collectOutput( + child: ChildProcess, + stdout: *std.ArrayList(u8), + stderr: *std.ArrayList(u8), + max_output_bytes: usize, +) !void { + assert(child.stdout_behavior == .Pipe); + assert(child.stderr_behavior == .Pipe); + + // we could make this work with multiple allocators but YAGNI + if (stdout.allocator.ptr != stderr.allocator.ptr or + stdout.allocator.vtable != stderr.allocator.vtable) + { + unreachable; // ChildProcess.collectOutput only supports 1 allocator + } + + var poller = std.io.poll(stdout.allocator, enum { stdout, stderr }, .{ + .stdout = child.stdout.?, + .stderr = child.stderr.?, + }); + defer poller.deinit(); + + 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; + } + + stdout.* = fifoToOwnedArrayList(poller.fifo(.stdout)); + stderr.* = fifoToOwnedArrayList(poller.fifo(.stderr)); +} + +pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.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. +pub fn run(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, +}) RunError!RunResult { + 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; + + var stdout = std.ArrayList(u8).init(args.allocator); + var stderr = std.ArrayList(u8).init(args.allocator); + errdefer { + stdout.deinit(); + stderr.deinit(); + } + + try child.spawn(); + try child.collectOutput(&stdout, &stderr, args.max_output_bytes); + + return RunResult{ + .term = try child.wait(), + .stdout = try stdout.toOwnedSlice(), + .stderr = try 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.id, windows.INFINITE, false); + + self.term = @as(SpawnError!Term, x: { + var exit_code: windows.DWORD = undefined; + if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) { + break :x Term{ .Unknown = 0 }; + } else { + break :x Term{ .Exited = @as(u8, @truncate(exit_code)) }; + } + }); + + if (self.request_resource_usage_statistics) { + self.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(self.id); + } + + posix.close(self.id); + posix.close(self.thread_handle); + self.cleanupStreams(); + return result; +} + +fn waitUnwrapped(self: *ChildProcess) !void { + const res: posix.WaitPidResult = res: { + if (self.request_resource_usage_statistics) { + switch (native_os) { + .linux, .macos, .ios => { + var ru: posix.rusage = undefined; + const res = posix.wait4(self.id, 0, &ru); + self.resource_usage_statistics.rusage = ru; + break :res res; + }, + else => {}, + } + } + + break :res posix.waitpid(self.id, 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 (native_os == .linux) { + var fd = [1]posix.pollfd{posix.pollfd{ + .fd = err_pipe[0], + .events = posix.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. + _ = posix.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 & posix.POLL.IN) != 0) { + const err_int = try readIntFd(err_pipe[0]); + return @as(SpawnError, @errorCast(@errorFromInt(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 @as(SpawnError, @errorCast(@errorFromInt(err_int))); + } + } + } + + return statusToTerm(status); +} + +fn statusToTerm(status: u32) Term { + return if (posix.W.IFEXITED(status)) + Term{ .Exited = posix.W.EXITSTATUS(status) } + else if (posix.W.IFSIGNALED(status)) + Term{ .Signal = posix.W.TERMSIG(status) } + else if (posix.W.IFSTOPPED(status)) + Term{ .Stopped = posix.W.STOPSIG(status) } + else + Term{ .Unknown = status }; +} + +fn spawnPosix(self: *ChildProcess) SpawnError!void { + const pipe_flags: posix.O = .{}; + const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try posix.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 posix.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 posix.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) + posix.openZ("/dev/null", .{ .ACCMODE = .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.WouldBlock => unreachable, + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + } + else + undefined; + defer { + if (any_ignore) posix.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]const u8, self.argv.len, null); + for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; + + const envp = m: { + if (self.env_map) |env_map| { + const envp_buf = try process.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 @as([*:null]const ?[*:0]const u8, @ptrCast(std.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 (native_os == .linux) { + const fd = try posix.eventfd(0, linux.EFD.CLOEXEC); + // There's no distinction between the readable and the writeable + // end with eventfd + break :blk [2]posix.fd_t{ fd, fd }; + } else { + break :blk try posix.pipe2(.{ .CLOEXEC = true }); + } + }; + errdefer destroyPipe(err_pipe); + + const pid_result = try posix.fork(); + if (pid_result == 0) { + // we are the child + setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); + setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); + setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); + + if (self.stdin_behavior == .Pipe) { + posix.close(stdin_pipe[0]); + posix.close(stdin_pipe[1]); + } + if (self.stdout_behavior == .Pipe) { + posix.close(stdout_pipe[0]); + posix.close(stdout_pipe[1]); + } + if (self.stderr_behavior == .Pipe) { + posix.close(stderr_pipe[0]); + posix.close(stderr_pipe[1]); + } + + if (self.cwd_dir) |cwd| { + posix.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err); + } else if (self.cwd) |cwd| { + posix.chdir(cwd) catch |err| forkChildErrReport(err_pipe[1], err); + } + + if (self.gid) |gid| { + posix.setregid(gid, gid) catch |err| forkChildErrReport(err_pipe[1], err); + } + + if (self.uid) |uid| { + posix.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err); + } + + const err = switch (self.expand_arg0) { + .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), + .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), + }; + forkChildErrReport(err_pipe[1], err); + } + + // we are the parent + const pid: i32 = @intCast(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.id = pid; + self.err_pipe = err_pipe; + self.term = null; + + if (self.stdin_behavior == StdIo.Pipe) { + posix.close(stdin_pipe[0]); + } + if (self.stdout_behavior == StdIo.Pipe) { + posix.close(stdout_pipe[1]); + } + if (self.stderr_behavior == StdIo.Pipe) { + posix.close(stderr_pipe[1]); + } +} + +fn spawnWindows(self: *ChildProcess) SpawnError!void { + var 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.GENERIC_WRITE | windows.SYNCHRONIZE, + .share_access = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, + .sa = &saAttr, + .creation = windows.OPEN_EXISTING, + }) catch |err| switch (err) { + error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" + error.PipeBusy => return error.Unexpected, // not possible for "NUL" + error.FileNotFound => return error.Unexpected, // not possible for "NUL" + error.AccessDenied => return error.Unexpected, // not possible for "NUL" + error.NameTooLong => return error.Unexpected, // not possible for "NUL" + error.WouldBlock => return error.Unexpected, // not possible for "NUL" + error.NetworkNotFound => return error.Unexpected, // not possible for "NUL" + error.AntivirusInterference => return error.Unexpected, // not possible for "NUL" + else => |e| return e, + } + else + undefined; + defer { + if (any_ignore) posix.close(nul_handle); + } + + 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.stdout_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.stderr_behavior == StdIo.Pipe) { + windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); + }; + + 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.wtf8ToWtf16LeAllocZ(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 process.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; + + const app_name_wtf8 = self.argv[0]; + const app_name_is_absolute = fs.path.isAbsolute(app_name_wtf8); + + // the cwd set in ChildProcess is in effect when choosing the executable path + // to match posix semantics + var cwd_path_w_needs_free = false; + const cwd_path_w = x: { + // If the app name is absolute, then we need to use its dirname as the cwd + if (app_name_is_absolute) { + cwd_path_w_needs_free = true; + const dir = fs.path.dirname(app_name_wtf8).?; + break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, dir); + } else if (self.cwd) |cwd| { + cwd_path_w_needs_free = true; + break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd); + } else { + break :x &[_:0]u16{}; // empty for cwd + } + }; + defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); + + // If the app name has more than just a filename, then we need to separate that + // into the basename and dirname and use the dirname as an addition to the cwd + // path. This is because NtQueryDirectoryFile cannot accept FileName params with + // path separators. + const app_basename_wtf8 = fs.path.basename(app_name_wtf8); + // If the app name is absolute, then the cwd will already have the app's dirname in it, + // so only populate app_dirname if app name is a relative path with > 0 path separators. + const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_wtf8) else null; + const app_dirname_w: ?[:0]u16 = x: { + if (maybe_app_dirname_wtf8) |app_dirname_wtf8| { + break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_dirname_wtf8); + } + break :x null; + }; + defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); + + const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8); + defer self.allocator.free(app_name_w); + + run: { + const PATH: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; + const PATHEXT: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; + + // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules + // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously + // constructed arguments. + // + // We'll need to wait until we're actually trying to run the command to know for sure + // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually + // serializing the command line until we determine how it should be serialized. + var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv); + defer cmd_line_cache.deinit(); + + var app_buf = std.ArrayListUnmanaged(u16){}; + defer app_buf.deinit(self.allocator); + + try app_buf.appendSlice(self.allocator, app_name_w); + + var dir_buf = std.ArrayListUnmanaged(u16){}; + defer dir_buf.deinit(self.allocator); + + if (cwd_path_w.len > 0) { + try dir_buf.appendSlice(self.allocator, cwd_path_w); + } + if (app_dirname_w) |app_dir| { + if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); + try dir_buf.appendSlice(self.allocator, app_dir); + } + if (dir_buf.items.len > 0) { + // Need to normalize the path, openDirW can't handle things like double backslashes + const normalized_len = windows.normalizePath(u16, dir_buf.items) catch return error.BadPathName; + dir_buf.shrinkRetainingCapacity(normalized_len); + } + + windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| { + const original_err = switch (no_path_err) { + // argv[0] contains unsupported characters that will never resolve to a valid exe. + error.InvalidArg0 => return error.FileNotFound, + error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, + error.UnrecoverableInvalidExe => return error.InvalidExe, + else => |e| return e, + }; + + // If the app name had path separators, that disallows PATH searching, + // and there's no need to search the PATH if the app name is absolute. + // We still search the path if the cwd is absolute because of the + // "cwd set in ChildProcess is in effect when choosing the executable path + // to match posix semantics" behavior--we don't want to skip searching + // the PATH just because we were trying to set the cwd of the child process. + if (app_dirname_w != null or app_name_is_absolute) { + return original_err; + } + + var it = mem.tokenizeScalar(u16, PATH, ';'); + while (it.next()) |search_path| { + dir_buf.clearRetainingCapacity(); + try dir_buf.appendSlice(self.allocator, search_path); + // Need to normalize the path, some PATH values can contain things like double + // backslashes which openDirW can't handle + const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue; + dir_buf.shrinkRetainingCapacity(normalized_len); + + if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) { + break :run; + } else |err| switch (err) { + // argv[0] contains unsupported characters that will never resolve to a valid exe. + error.InvalidArg0 => return error.FileNotFound, + error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, + error.UnrecoverableInvalidExe => return error.InvalidExe, + else => |e| return e, + } + } else { + return original_err; + } + }; + } + + 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.id = piProcInfo.hProcess; + self.thread_handle = piProcInfo.hThread; + self.term = null; + + if (self.stdin_behavior == StdIo.Pipe) { + posix.close(g_hChildStd_IN_Rd.?); + } + if (self.stderr_behavior == StdIo.Pipe) { + posix.close(g_hChildStd_ERR_Wr.?); + } + if (self.stdout_behavior == StdIo.Pipe) { + posix.close(g_hChildStd_OUT_Wr.?); + } +} + +fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { + switch (stdio) { + .Pipe => try posix.dup2(pipe_fd, std_fileno), + .Close => posix.close(std_fileno), + .Inherit => {}, + .Ignore => try posix.dup2(dev_null_fd, std_fileno), + } +} + +fn destroyPipe(pipe: [2]posix.fd_t) void { + posix.close(pipe[0]); + if (pipe[0] != pipe[1]) posix.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, @intFromError(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); + } + posix.exit(1); +} + +fn writeIntFd(fd: i32, value: ErrInt) !void { + const file: File = .{ .handle = fd }; + file.writer().writeInt(u64, @intCast(value), .little) catch return error.SystemResources; +} + +fn readIntFd(fd: i32) !ErrInt { + const file: File = .{ .handle = fd }; + return @intCast(file.reader().readInt(u64, .little) catch return error.SystemResources); +} + +const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); + +/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. +/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. +/// Note: `app_buf` should not contain any leading path separators. +/// Note: If the dir is the cwd, dir_buf should be empty (len = 0). +fn windowsCreateProcessPathExt( + allocator: mem.Allocator, + dir_buf: *std.ArrayListUnmanaged(u16), + app_buf: *std.ArrayListUnmanaged(u16), + pathext: [:0]const u16, + cmd_line_cache: *WindowsCommandLineCache, + envp_ptr: ?[*]u16, + cwd_ptr: ?[*:0]u16, + lpStartupInfo: *windows.STARTUPINFOW, + lpProcessInformation: *windows.PROCESS_INFORMATION, +) !void { + const app_name_len = app_buf.items.len; + const dir_path_len = dir_buf.items.len; + + if (app_name_len == 0) return error.FileNotFound; + + defer app_buf.shrinkRetainingCapacity(app_name_len); + defer dir_buf.shrinkRetainingCapacity(dir_path_len); + + // The name of the game here is to avoid CreateProcessW calls at all costs, + // and only ever try calling it when we have a real candidate for execution. + // Secondarily, we want to minimize the number of syscalls used when checking + // for each PATHEXT-appended version of the app name. + // + // An overview of the technique used: + // - Open the search directory for iteration (either cwd or a path from PATH) + // - Use NtQueryDirectoryFile with a wildcard filename of `*` to + // check if anything that could possibly match either the unappended version + // of the app name or any of the versions with a PATHEXT value appended exists. + // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early + // without needing to use PATHEXT at all. + // + // This allows us to use a sequence + // for any directory that doesn't contain any possible matches, instead of having + // to use a separate look up for each individual filename combination (unappended + + // each PATHEXT appended). For directories where the wildcard *does* match something, + // we iterate the matches and take note of any that are either the unappended version, + // or a version with a supported PATHEXT appended. We then try calling CreateProcessW + // with the found versions in the appropriate order. + + var dir = dir: { + // needs to be null-terminated + try dir_buf.append(allocator, 0); + defer dir_buf.shrinkRetainingCapacity(dir_path_len); + const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); + break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch + return error.FileNotFound; + }; + defer dir.close(); + + // Add wildcard and null-terminator + try app_buf.append(allocator, '*'); + try app_buf.append(allocator, 0); + const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; + + // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries + // returned per NtQueryDirectoryFile call. + var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined; + const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); + if (file_information_buf.len < file_info_maximum_single_entry_size) { + @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); + } + var io_status: windows.IO_STATUS_BLOCK = undefined; + + const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; + var pathext_seen = [_]bool{false} ** num_supported_pathext; + var any_pathext_seen = false; + var unappended_exists = false; + + // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions + // of the app_name we should try to spawn. + // Note: This is necessary because the order of the files returned is filesystem-dependent: + // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. + // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. + while (true) { + const app_name_len_bytes = std.math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; + var app_name_unicode_string = windows.UNICODE_STRING{ + .Length = app_name_len_bytes, + .MaximumLength = app_name_len_bytes, + .Buffer = @constCast(app_name_wildcard.ptr), + }; + const rc = windows.ntdll.NtQueryDirectoryFile( + dir.fd, + null, + null, + null, + &io_status, + &file_information_buf, + file_information_buf.len, + .FileDirectoryInformation, + windows.FALSE, // single result + &app_name_unicode_string, + windows.FALSE, // restart iteration + ); + + // If we get nothing with the wildcard, then we can just bail out + // as we know appending PATHEXT will not yield anything. + switch (rc) { + .SUCCESS => {}, + .NO_SUCH_FILE => return error.FileNotFound, + .NO_MORE_FILES => break, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + + // According to the docs, this can only happen if there is not enough room in the + // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. + // Therefore, this condition should not be possible to hit with the buffer size we use. + std.debug.assert(io_status.Information != 0); + + var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; + while (it.next()) |info| { + // Skip directories + if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; + const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; + // Because all results start with the app_name since we're using the wildcard `app_name*`, + // if the length is equal to app_name then this is an exact match + if (filename.len == app_name_len) { + // Note: We can't break early here because it's possible that the unappended version + // fails to spawn, in which case we still want to try the PATHEXT appended versions. + unappended_exists = true; + } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { + pathext_seen[@intFromEnum(pathext_ext)] = true; + any_pathext_seen = true; + } + } + } + + const unappended_err = unappended: { + if (unappended_exists) { + if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { + '/', '\\' => {}, + else => try dir_buf.append(allocator, fs.path.sep), + }; + try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); + try dir_buf.append(allocator, 0); + const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + + const is_bat_or_cmd = bat_or_cmd: { + const app_name = app_buf.items[0..app_name_len]; + const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false; + const ext = app_name[ext_start..]; + const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false; + switch (ext_enum) { + .cmd, .bat => break :bat_or_cmd true, + else => break :bat_or_cmd false, + } + }; + const cmd_line_w = if (is_bat_or_cmd) + try cmd_line_cache.scriptCommandLine(full_app_name) + else + try cmd_line_cache.commandLine(); + const app_name_w = if (is_bat_or_cmd) + try cmd_line_cache.cmdExePath() + else + full_app_name; + + if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { + return; + } else |err| switch (err) { + error.FileNotFound, + error.AccessDenied, + => break :unappended err, + error.InvalidExe => { + // On InvalidExe, if the extension of the app name is .exe then + // it's treated as an unrecoverable error. Otherwise, it'll be + // skipped as normal. + const app_name = app_buf.items[0..app_name_len]; + const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; + const ext = app_name[ext_start..]; + if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { + return error.UnrecoverableInvalidExe; + } + break :unappended err; + }, + else => return err, + } + } + break :unappended error.FileNotFound; + }; + + if (!any_pathext_seen) return unappended_err; + + // Now try any PATHEXT appended versions that we've seen + var ext_it = mem.tokenizeScalar(u16, pathext, ';'); + while (ext_it.next()) |ext| { + const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; + if (!pathext_seen[@intFromEnum(ext_enum)]) continue; + + dir_buf.shrinkRetainingCapacity(dir_path_len); + if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { + '/', '\\' => {}, + else => try dir_buf.append(allocator, fs.path.sep), + }; + try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); + try dir_buf.appendSlice(allocator, ext); + try dir_buf.append(allocator, 0); + const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + + const is_bat_or_cmd = switch (ext_enum) { + .cmd, .bat => true, + else => false, + }; + const cmd_line_w = if (is_bat_or_cmd) + try cmd_line_cache.scriptCommandLine(full_app_name) + else + try cmd_line_cache.commandLine(); + const app_name_w = if (is_bat_or_cmd) + try cmd_line_cache.cmdExePath() + else + full_app_name; + + if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { + return; + } else |err| switch (err) { + error.FileNotFound => continue, + error.AccessDenied => continue, + error.InvalidExe => { + // On InvalidExe, if the extension of the app name is .exe then + // it's treated as an unrecoverable error. Otherwise, it'll be + // skipped as normal. + if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { + return error.UnrecoverableInvalidExe; + } + continue; + }, + else => return err, + } + } + + return unappended_err; +} + +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, + @as(?*anyopaque, @ptrCast(envp_ptr)), + cwd_ptr, + lpStartupInfo, + lpProcessInformation, + ); +} + +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; +} + +fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { + if (rd) |h| posix.close(h); + if (wr) |h| posix.close(h); +} + +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.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, + ) catch unreachable; + const len = std.unicode.wtf8ToWtf16Le(&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 posix.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 posix.close(write_handle); + + try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); + + rd.* = read_handle; + wr.* = write_handle; +} + +var pipe_name_counter = std.atomic.Value(u32).init(1); + +// Should be kept in sync with `windowsCreateProcessSupportsExtension` +const CreateProcessSupportedExtension = enum { + bat, + cmd, + com, + exe, +}; + +/// Case-insensitive WTF-16 lookup +fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { + if (ext.len != 4) return null; + const State = enum { + start, + dot, + b, + ba, + c, + cm, + co, + e, + ex, + }; + var state: State = .start; + for (ext) |c| switch (state) { + .start => switch (c) { + '.' => state = .dot, + else => return null, + }, + .dot => switch (c) { + 'b', 'B' => state = .b, + 'c', 'C' => state = .c, + 'e', 'E' => state = .e, + else => return null, + }, + .b => switch (c) { + 'a', 'A' => state = .ba, + else => return null, + }, + .c => switch (c) { + 'm', 'M' => state = .cm, + 'o', 'O' => state = .co, + else => return null, + }, + .e => switch (c) { + 'x', 'X' => state = .ex, + else => return null, + }, + .ba => switch (c) { + 't', 'T' => return .bat, + else => return null, + }, + .cm => switch (c) { + 'd', 'D' => return .cmd, + else => return null, + }, + .co => switch (c) { + 'm', 'M' => return .com, + else => return null, + }, + .ex => switch (c) { + 'e', 'E' => return .exe, + else => return null, + }, + }; + return null; +} + +test windowsCreateProcessSupportsExtension { + try std.testing.expectEqual(CreateProcessSupportedExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?); + try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); +} + +/// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW. +/// +/// Serialization is done on-demand and the result is cached in order to allow for: +/// - Only serializing the particular type of command line needed (`.bat`/`.cmd` +/// command line serialization is different from `.exe`/etc) +/// - Reusing the serialized command lines if necessary (i.e. if the execution +/// of a command fails and the PATH is going to be continued to be searched +/// for more candidates) +const WindowsCommandLineCache = struct { + cmd_line: ?[:0]u16 = null, + script_cmd_line: ?[:0]u16 = null, + cmd_exe_path: ?[:0]u16 = null, + argv: []const []const u8, + allocator: mem.Allocator, + + fn init(allocator: mem.Allocator, argv: []const []const u8) WindowsCommandLineCache { + return .{ + .allocator = allocator, + .argv = argv, + }; + } + + fn deinit(self: *WindowsCommandLineCache) void { + if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line); + if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line); + if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path); + } + + fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 { + if (self.cmd_line == null) { + self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv); + } + return self.cmd_line.?; + } + + /// Not cached, since the path to the batch script will change during PATH searching. + /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched, + /// then script_path should include both the search path and the script filename + /// (this allows avoiding cmd.exe having to search the PATH again). + fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 { + if (self.script_cmd_line) |v| self.allocator.free(v); + self.script_cmd_line = try argvToScriptCommandLineWindows( + self.allocator, + script_path, + self.argv[1..], + ); + return self.script_cmd_line.?; + } + + fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 { + if (self.cmd_exe_path == null) { + self.cmd_exe_path = try windowsCmdExePath(self.allocator); + } + return self.cmd_exe_path.?; + } +}; + +fn windowsCmdExePath(allocator: mem.Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { + var buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 128); + errdefer buf.deinit(allocator); + while (true) { + const unused_slice = buf.unusedCapacitySlice(); + // TODO: Get the system directory from PEB.ReadOnlyStaticServerData + const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len)); + if (len == 0) { + switch (windows.kernel32.GetLastError()) { + else => |err| return windows.unexpectedError(err), + } + } + if (len > unused_slice.len) { + try buf.ensureUnusedCapacity(allocator, len); + } else { + buf.items.len = len; + break; + } + } + switch (buf.items[buf.items.len - 1]) { + '/', '\\' => {}, + else => try buf.append(allocator, fs.path.sep), + } + try buf.appendSlice(allocator, unicode.utf8ToUtf16LeStringLiteral("cmd.exe")); + return try buf.toOwnedSliceSentinel(allocator, 0); +} + +const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 }; + +/// Serializes `argv` to a Windows command-line string suitable for passing to a child process and +/// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice. +fn argvToCommandLineWindows( + allocator: mem.Allocator, + argv: []const []const u8, +) ArgvToCommandLineError![:0]u16 { + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + + if (argv.len != 0) { + const arg0 = argv[0]; + + // The first argument must be quoted if it contains spaces or ASCII control characters + // (excluding DEL). It also follows special quoting rules where backslashes have no special + // interpretation, which makes it impossible to pass certain first arguments containing + // double quotes to a child process without characters from the first argument leaking into + // subsequent ones (which could have security implications). + // + // Empty arguments technically don't need quotes, but we quote them anyway for maximum + // compatibility with different implementations of the 'CommandLineToArgvW' algorithm. + // + // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject + // all first arguments containing double quotes, even ones that we could theoretically + // serialize in unquoted form. + var needs_quotes = arg0.len == 0; + for (arg0) |c| { + if (c <= ' ') { + needs_quotes = true; + } else if (c == '"') { + return error.InvalidArg0; + } + } + if (needs_quotes) { + try buf.append('"'); + try buf.appendSlice(arg0); + try buf.append('"'); + } else { + try buf.appendSlice(arg0); + } + + for (argv[1..]) |arg| { + try buf.append(' '); + + // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes, + // or if they are empty. For simplicity and for maximum compatibility with different + // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII + // control characters (again, excluding DEL). + needs_quotes = for (arg) |c| { + if (c <= ' ' or c == '"') { + break true; + } + } else arg.len == 0; + if (!needs_quotes) { + 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 try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); +} + +test argvToCommandLineWindows { + const t = testArgvToCommandLineWindows; + + try t(&.{ + \\C:\Program Files\zig\zig.exe + , + \\run + , + \\.\src\main.zig + , + \\-target + , + \\x86_64-windows-gnu + , + \\-O + , + \\ReleaseSafe + , + \\-- + , + \\--emoji=๐Ÿ—ฟ + , + \\--eval=new Regex("Dwayne \"The Rock\" Johnson") + , + }, + \\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=๐Ÿ—ฟ "--eval=new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")" + ); + + try t(&.{}, ""); + try t(&.{""}, "\"\""); + try t(&.{" "}, "\" \""); + try t(&.{"\t"}, "\"\t\""); + try t(&.{"\x07"}, "\"\x07\""); + try t(&.{"๐ŸฆŽ"}, "๐ŸฆŽ"); + + try t( + &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" }, + "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee", + ); + + try t( + &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" }, + "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"", + ); + + try std.testing.expectError( + error.InvalidArg0, + argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}), + ); + try std.testing.expectError( + error.InvalidArg0, + argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}), + ); + try std.testing.expectError( + error.InvalidArg0, + argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}), + ); +} + +fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void { + const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); + defer std.testing.allocator.free(cmd_line_w); + + const cmd_line = try unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w); + defer std.testing.allocator.free(cmd_line); + + try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); +} + +const ArgvToScriptCommandLineError = error{ + OutOfMemory, + InvalidWtf8, + /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed + /// within arguments when executing a `.bat`/`.cmd` script. + /// - NUL/LF signifiies end of arguments, so anything afterwards + /// would be lost after execution. + /// - CR is stripped by `cmd.exe`, so any CR codepoints + /// would be lost after execution. + InvalidBatchScriptArg, +}; + +/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific +/// escaping rules. The caller owns the returned slice. +/// +/// Escapes `argv` using the suggested mitigation against arbitrary command execution from: +/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ +fn argvToScriptCommandLineWindows( + allocator: mem.Allocator, + /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD. + /// The script must have been verified to exist at this path before calling this function. + script_path: []const u16, + /// Arguments, not including the script name itself. Expected to be encoded as WTF-8. + script_args: []const []const u8, +) ArgvToScriptCommandLineError![:0]u16 { + var buf = try std.ArrayList(u8).initCapacity(allocator, 64); + defer buf.deinit(); + + // `/d` disables execution of AutoRun commands. + // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation: + // > If delayed expansion is enabled via the registry value DelayedExpansion, + // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option. + // > Escaping for % requires the command extension to be enabled. + // > If itโ€™s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option. + // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ + buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \""); + + // Always quote the path to the script arg + buf.appendAssumeCapacity('"'); + // We always want the path to the batch script to include a path separator in order to + // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary + // command execution mitigation, we just know exactly what script we want to execute + // at this point, and potentially making cmd.exe re-find it is unnecessary. + // + // If the script path does not have a path separator, then we know its relative to CWD and + // we can just put `.\` in the front. + if (mem.indexOfAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) { + try buf.appendSlice(".\\"); + } + // Note that we don't do any escaping/mitigations for this argument, since the relevant + // characters (", %, etc) are illegal in file paths and this function should only be called + // with script paths that have been verified to exist. + try unicode.wtf16LeToWtf8ArrayList(&buf, script_path); + buf.appendAssumeCapacity('"'); + + for (script_args) |arg| { + // Literal carriage returns get stripped when run through cmd.exe + // and NUL/newlines act as 'end of command.' Because of this, it's basically + // always a mistake to include these characters in argv, so it's + // an error condition in order to ensure that the return of this + // function can always roundtrip through cmd.exe. + if (std.mem.indexOfAny(u8, arg, "\x00\r\n") != null) { + return error.InvalidBatchScriptArg; + } + + // Separate args with a space. + try buf.append(' '); + + // Need to quote if the argument is empty (otherwise the arg would just be lost) + // or if the last character is a `\`, since then something like "%~2" in a .bat + // script would cause the closing " to be escaped which we don't want. + var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\'; + if (!needs_quotes) { + for (arg) |c| { + switch (c) { + // Known good characters that don't need to be quoted + 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {}, + // When in doubt, quote + else => { + needs_quotes = true; + break; + }, + } + } + } + if (needs_quotes) { + try buf.append('"'); + } + var backslashes: usize = 0; + for (arg) |c| { + switch (c) { + '\\' => { + backslashes += 1; + }, + '"' => { + try buf.appendNTimes('\\', backslashes); + try buf.append('"'); + backslashes = 0; + }, + // Replace `%` with `%%cd:~,%`. + // + // cmd.exe allows extracting a substring from an environment + // variable with the syntax: `%foo:~,%`. + // Therefore, `%cd:~,%` will always expand to an empty string + // since both the start and end index are blank, and it is assumed + // that `%cd%` is always available since it is a built-in variable + // that corresponds to the current directory. + // + // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%` + // will stop `%foo%` from being expanded and *after* expansion + // we'll still be left with `%foo%` (the literal string). + '%' => { + // the trailing `%` is appended outside the switch + try buf.appendSlice("%%cd:~,"); + backslashes = 0; + }, + else => { + backslashes = 0; + }, + } + try buf.append(c); + } + if (needs_quotes) { + try buf.appendNTimes('\\', backslashes); + try buf.append('"'); + } + } + + try buf.append('"'); + + return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); +} diff --git a/lib/std/std.zig b/lib/std/std.zig index 1d557e7abd..e33feddbf5 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -14,8 +14,6 @@ pub const BoundedArrayAligned = @import("bounded_array.zig").BoundedArrayAligned pub const Build = @import("Build.zig"); pub const BufMap = @import("buf_map.zig").BufMap; pub const BufSet = @import("buf_set.zig").BufSet; -/// Deprecated: use `process.Child`. -pub const ChildProcess = @import("child_process.zig").ChildProcess; pub const StaticStringMap = static_string_map.StaticStringMap; pub const StaticStringMapWithEql = static_string_map.StaticStringMapWithEql; pub const DoublyLinkedList = @import("linked_list.zig").DoublyLinkedList; diff --git a/lib/std/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig index 656ea32981..686471bde5 100644 --- a/lib/std/zig/LibCInstallation.zig +++ b/lib/std/zig/LibCInstallation.zig @@ -261,7 +261,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, args: FindNativeOptions) F dev_null, }); - const run_res = std.ChildProcess.run(.{ + const run_res = std.process.Child.run(.{ .allocator = allocator, .argv = argv.items, .max_output_bytes = 1024 * 1024, @@ -588,7 +588,7 @@ fn ccPrintFileName(args: CCPrintFileNameOptions) ![:0]u8 { try appendCcExe(&argv, skip_cc_env_var); try argv.append(arg1); - const run_res = std.ChildProcess.run(.{ + const run_res = std.process.Child.run(.{ .allocator = allocator, .argv = argv.items, .max_output_bytes = 1024 * 1024, diff --git a/src/Compilation.zig b/src/Compilation.zig index 37df4cc838..921d534baf 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -1935,7 +1935,7 @@ pub fn getTarget(self: Compilation) Target { pub fn hotCodeSwap( comp: *Compilation, prog_node: *std.Progress.Node, - pid: std.ChildProcess.Id, + pid: std.process.Child.Id, ) !void { const lf = comp.bin_file.?; lf.child_pid = pid; @@ -4614,7 +4614,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P log.warn("failed to delete '{s}': {s}", .{ dep_file_path, @errorName(err) }); }; if (std.process.can_spawn) { - var child = std.ChildProcess.init(argv.items, arena); + var child = std.process.Child.init(argv.items, arena); if (comp.clang_passthrough_mode) { child.stdin_behavior = .Inherit; child.stdout_behavior = .Inherit; @@ -4972,7 +4972,7 @@ fn spawnZigRc( var node_name: std.ArrayListUnmanaged(u8) = .{}; defer node_name.deinit(arena); - var child = std.ChildProcess.init(argv, arena); + var child = std.process.Child.init(argv, arena); child.stdin_behavior = .Ignore; child.stdout_behavior = .Pipe; child.stderr_behavior = .Pipe; diff --git a/src/link.zig b/src/link.zig index 8aec0799e2..c05b545a89 100644 --- a/src/link.zig +++ b/src/link.zig @@ -72,7 +72,7 @@ pub const File = struct { /// Prevents other processes from clobbering files in the output directory /// of this linking operation. lock: ?Cache.Lock = null, - child_pid: ?std.ChildProcess.Id = null, + child_pid: ?std.process.Child.Id = null, pub const OpenOptions = struct { symbol_count_hint: u64 = 32, @@ -529,7 +529,7 @@ pub const File = struct { } || fs.File.WriteFileError || fs.File.OpenError || - std.ChildProcess.SpawnError || + std.process.Child.SpawnError || fs.Dir.CopyFileError; /// Commit pending changes and write headers. Takes into account final output mode diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 413294489a..31cfe1ca9d 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -893,7 +893,7 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { } } -fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { +fn debugMem(allocator: Allocator, handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { const buffer = try allocator.alloc(u8, code.len); defer allocator.free(buffer); const memread = try std.os.windows.ReadProcessMemory(handle, pvaddr, buffer); @@ -901,7 +901,7 @@ fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, pvaddr: std.os.wi log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(memread)}); } -fn writeMemProtected(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { +fn writeMemProtected(handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { const old_prot = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY); try writeMem(handle, pvaddr, code); // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. @@ -909,7 +909,7 @@ fn writeMemProtected(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, _ = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, old_prot); } -fn writeMem(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { +fn writeMem(handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { const amt = try std.os.windows.WriteProcessMemory(handle, pvaddr, code); if (amt != code.len) return error.InputOutput; } @@ -1031,7 +1031,7 @@ fn resolveRelocs(self: *Coff, atom_index: Atom.Index, relocs: []*const Relocatio } } -pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { +pub fn ptraceAttach(self: *Coff, handle: std.process.Child.Id) !void { if (!is_hot_update_compatible) return; log.debug("attaching to process with handle {*}", .{handle}); @@ -1041,7 +1041,7 @@ pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { }; } -pub fn ptraceDetach(self: *Coff, handle: std.ChildProcess.Id) void { +pub fn ptraceDetach(self: *Coff, handle: std.process.Child.Id) void { if (!is_hot_update_compatible) return; log.debug("detaching from process with handle {*}", .{handle}); diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index e82395ecea..bf345813df 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -3637,7 +3637,7 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node) !vo // If possible, we run LLD as a child process because it does not always // behave properly as a library, unfortunately. // https://github.com/ziglang/zig/issues/3825 - var child = std.ChildProcess.init(argv.items, arena); + var child = std.process.Child.init(argv.items, arena); if (comp.clang_passthrough_mode) { child.stdin_behavior = .Inherit; child.stdout_behavior = .Inherit; diff --git a/src/main.zig b/src/main.zig index 61a97de3f5..099ceb27f9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4026,7 +4026,7 @@ fn serve( }); defer server.deinit(); - var child_pid: ?std.ChildProcess.Id = null; + var child_pid: ?std.process.Child.Id = null; var progress: std.Progress = .{ .terminal = null, @@ -4316,7 +4316,7 @@ fn runOrTest( const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd }); } else if (process.can_spawn) { - var child = std.ChildProcess.init(argv.items, gpa); + var child = std.process.Child.init(argv.items, gpa); child.env_map = &env_map; child.stdin_behavior = .Inherit; child.stdout_behavior = .Inherit; @@ -4379,7 +4379,7 @@ fn runOrTestHotSwap( arg_mode: ArgMode, all_args: []const []const u8, runtime_args_start: ?usize, -) !std.ChildProcess.Id { +) !std.process.Child.Id { const lf = comp.bin_file.?; const exe_path = switch (builtin.target.os.tag) { @@ -4456,7 +4456,7 @@ fn runOrTestHotSwap( return pid; }, else => { - var child = std.ChildProcess.init(argv.items, gpa); + var child = std.process.Child.init(argv.items, gpa); child.stdin_behavior = .Inherit; child.stdout_behavior = .Inherit; @@ -5245,7 +5245,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } if (process.can_spawn) { - var child = std.ChildProcess.init(child_argv.items, gpa); + var child = std.process.Child.init(child_argv.items, gpa); child.stdin_behavior = .Inherit; child.stdout_behavior = .Inherit; child.stderr_behavior = .Inherit; @@ -5549,7 +5549,7 @@ fn jitCmd( }); } - var child = std.ChildProcess.init(child_argv.items, gpa); + var child = std.process.Child.init(child_argv.items, gpa); child.stdin_behavior = .Inherit; child.stdout_behavior = if (options.capture == null) .Inherit else .Pipe; child.stderr_behavior = .Inherit; diff --git a/test/src/Cases.zig b/test/src/Cases.zig index 7dab5af0db..efc2f7dcdc 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -1839,7 +1839,7 @@ fn runOneCase( try comp.makeBinFileExecutable(); while (true) { - break :x std.ChildProcess.run(.{ + break :x std.process.Child.run(.{ .allocator = allocator, .argv = argv.items, .cwd_dir = tmp.dir, diff --git a/test/standalone/child_process/main.zig b/test/standalone/child_process/main.zig index c82fbcb99c..068ff8d1a9 100644 --- a/test/standalone/child_process/main.zig +++ b/test/standalone/child_process/main.zig @@ -13,7 +13,7 @@ pub fn main() !void { _ = it.next() orelse unreachable; // skip binary name const child_path = it.next() orelse unreachable; - var child = std.ChildProcess.init(&.{ child_path, "hello arg" }, gpa); + var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa); child.stdin_behavior = .Pipe; child.stdout_behavior = .Pipe; child.stderr_behavior = .Inherit; diff --git a/test/standalone/windows_bat_args/fuzz.zig b/test/standalone/windows_bat_args/fuzz.zig index 75433d2b4e..37c9223c1b 100644 --- a/test/standalone/windows_bat_args/fuzz.zig +++ b/test/standalone/windows_bat_args/fuzz.zig @@ -87,7 +87,7 @@ fn testExecBat(allocator: std.mem.Allocator, bat: []const u8, args: []const []co const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat"); - const result = try std.ChildProcess.run(.{ + const result = try std.process.Child.run(.{ .allocator = allocator, .env_map = env, .argv = argv.items, diff --git a/test/standalone/windows_bat_args/test.zig b/test/standalone/windows_bat_args/test.zig index 10bdad48ab..b2a9aed6ca 100644 --- a/test/standalone/windows_bat_args/test.zig +++ b/test/standalone/windows_bat_args/test.zig @@ -109,7 +109,7 @@ fn testExecBat(allocator: std.mem.Allocator, bat: []const u8, args: []const []co const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat"); - const result = try std.ChildProcess.run(.{ + const result = try std.process.Child.run(.{ .allocator = allocator, .env_map = env, .argv = argv.items, diff --git a/test/standalone/windows_spawn/main.zig b/test/standalone/windows_spawn/main.zig index 9a2047950a..4c637d36ff 100644 --- a/test/standalone/windows_spawn/main.zig +++ b/test/standalone/windows_spawn/main.zig @@ -158,7 +158,7 @@ fn testExec(allocator: std.mem.Allocator, command: []const u8, expected_stdout: } fn testExecWithCwd(allocator: std.mem.Allocator, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void { - const result = try std.ChildProcess.run(.{ + const result = try std.process.Child.run(.{ .allocator = allocator, .argv = &[_][]const u8{command}, .cwd = cwd, diff --git a/tools/docgen.zig b/tools/docgen.zig index ebfe0eed22..1e9c409fa4 100644 --- a/tools/docgen.zig +++ b/tools/docgen.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const io = std.io; const fs = std.fs; const process = std.process; -const ChildProcess = std.ChildProcess; +const ChildProcess = std.process.Child; const Progress = std.Progress; const print = std.debug.print; const mem = std.mem; diff --git a/tools/generate_linux_syscalls.zig b/tools/generate_linux_syscalls.zig index ba368e1f0b..3e09875e4d 100644 --- a/tools/generate_linux_syscalls.zig +++ b/tools/generate_linux_syscalls.zig @@ -257,7 +257,7 @@ pub fn main() !void { "arch/arm64/include/uapi/asm/unistd.h", }; - const child_result = try std.ChildProcess.run(.{ + const child_result = try std.process.Child.run(.{ .allocator = allocator, .argv = &child_args, .cwd = linux_path, @@ -318,7 +318,7 @@ pub fn main() !void { "arch/riscv/include/uapi/asm/unistd.h", }; - const child_result = try std.ChildProcess.run(.{ + const child_result = try std.process.Child.run(.{ .allocator = allocator, .argv = &child_args, .cwd = linux_path, diff --git a/tools/update_clang_options.zig b/tools/update_clang_options.zig index 6308189bb2..19ced2f972 100644 --- a/tools/update_clang_options.zig +++ b/tools/update_clang_options.zig @@ -624,7 +624,7 @@ pub fn main() anyerror!void { try std.fmt.allocPrint(allocator, "-I={s}/clang/include/clang/Driver", .{llvm_src_root}), }; - const child_result = try std.ChildProcess.run(.{ + const child_result = try std.process.Child.run(.{ .allocator = allocator, .argv = &child_args, .max_output_bytes = 100 * 1024 * 1024, diff --git a/tools/update_cpu_features.zig b/tools/update_cpu_features.zig index 10ea65084d..0690cc2e69 100644 --- a/tools/update_cpu_features.zig +++ b/tools/update_cpu_features.zig @@ -1106,7 +1106,7 @@ fn processOneTarget(job: Job) anyerror!void { }), }; - const child_result = try std.ChildProcess.run(.{ + const child_result = try std.process.Child.run(.{ .allocator = arena, .argv = &child_args, .max_output_bytes = 400 * 1024 * 1024,