diff --git a/build.zig b/build.zig index 338709c7c4..1cef23d206 100644 --- a/build.zig +++ b/build.zig @@ -261,7 +261,7 @@ pub fn build(b: *std.Build) !void { "--git-dir", ".git", // affected by the -C argument "describe", "--match", "*.*.*", // "--tags", "--abbrev=9", - }, &code, .Ignore) catch { + }, &code, .ignore) catch { break :v version_string; }; const git_describe = mem.trim(u8, git_describe_untrimmed, " \n\r"); diff --git a/lib/compiler/aro/aro/Driver.zig b/lib/compiler/aro/aro/Driver.zig index 340a35bdde..6ffaedc2e7 100644 --- a/lib/compiler/aro/aro/Driver.zig +++ b/lib/compiler/aro/aro/Driver.zig @@ -1256,9 +1256,9 @@ fn invokeAssembler(d: *Driver, tc: *Toolchain, input_path: []const u8, output_pa var child = std.process.Child.init(&argv, d.comp.gpa); // TODO handle better - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .inherit; + child.stdout_behavior = .inherit; + child.stderr_behavior = .inherit; const term = child.spawnAndWait() catch |er| { return d.fatal("unable to spawn linker: {s}", .{errorDescription(er)}); @@ -1508,9 +1508,9 @@ pub fn invokeLinker(d: *Driver, tc: *Toolchain, comptime fast_exit: bool) Compil } var child = std.process.Child.init(argv.items, d.comp.gpa); // TODO handle better - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .inherit; + child.stdout_behavior = .inherit; + child.stderr_behavior = .inherit; const term = child.spawnAndWait() catch |er| { return d.fatal("unable to spawn linker: {s}", .{errorDescription(er)}); diff --git a/lib/compiler/reduce.zig b/lib/compiler/reduce.zig index 922031d008..b0fa94cb06 100644 --- a/lib/compiler/reduce.zig +++ b/lib/compiler/reduce.zig @@ -306,7 +306,7 @@ fn termToInteresting(term: std.process.Child.Term) Interestingness { } fn runCheck(arena: Allocator, io: Io, argv: []const []const u8) !Interestingness { - const result = try std.process.Child.run(arena, io, .{ .argv = argv }); + const result = try std.process.run(arena, io, .{ .spawn_options = .{ .argv = argv } }); if (result.stderr.len != 0) std.debug.print("{s}", .{result.stderr}); return termToInteresting(result.term); diff --git a/lib/compiler/std-docs.zig b/lib/compiler/std-docs.zig index e2b58b8668..a9280b6fd9 100644 --- a/lib/compiler/std-docs.zig +++ b/lib/compiler/std-docs.zig @@ -447,9 +447,9 @@ fn openBrowserTabThread(gpa: Allocator, io: Io, url: []const u8) !void { else => "xdg-open", }; var child = std.process.Child.init(&.{ main_exe, url }, gpa); - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Ignore; - child.stderr_behavior = .Ignore; + child.stdin_behavior = .ignore; + child.stdout_behavior = .ignore; + child.stderr_behavior = .ignore; try child.spawn(io); _ = try child.wait(io); } diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 8867ac05df..3ade95ae06 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -190,7 +190,7 @@ pub const RunError = error{ ExitCodeFailure, ProcessTerminated, ExecNotSupported, -} || std.process.Child.SpawnError; +} || std.process.SpawnError; pub const PkgConfigError = error{ PkgConfigCrashed, @@ -1755,7 +1755,7 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 { } fn supportedWindowsProgramExtension(ext: []const u8) bool { - inline for (@typeInfo(std.process.Child.WindowsExtension).@"enum".fields) |field| { + inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| { if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true; } return false; @@ -1830,23 +1830,26 @@ pub fn runAllowFail( b: *Build, argv: []const []const u8, out_code: *u8, - stderr_behavior: std.process.Child.StdIo, + stderr_behavior: std.process.SpawnOptions.StdIo, ) RunError![]u8 { assert(argv.len != 0); if (!process.can_spawn) return error.ExecNotSupported; - const io = b.graph.io; + const graph = b.graph; + const io = graph.io; const max_output_size = 400 * 1024; - var child = std.process.Child.init(b.allocator, argv, .{ .map = &b.graph.env_map }); - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Pipe; - child.stderr_behavior = stderr_behavior; + try Step.handleVerbose2(b, null, &graph.env_map, argv); - try Step.handleVerbose2(b, null, child.environ.map, argv); - try child.spawn(io); + var child = try std.process.spawn(io, .{ + .argv = argv, + .env_map = &graph.env_map, + .stdin = .ignore, + .stdout = .pipe, + .stderr = stderr_behavior, + }); var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch { @@ -1856,14 +1859,18 @@ pub fn runAllowFail( const term = try child.wait(io); switch (term) { - .Exited => |code| { + .exited => |code| { if (code != 0) { out_code.* = @as(u8, @truncate(code)); return error.ExitCodeFailure; } return stdout; }, - .Signal, .Stopped, .Unknown => |code| { + .signal => |sig| { + out_code.* = @as(u8, @truncate(@intFromEnum(sig))); + return error.ProcessTerminated; + }, + .stopped, .unknown => |code| { out_code.* = @as(u8, @truncate(code)); return error.ProcessTerminated; }, @@ -1882,7 +1889,7 @@ pub fn run(b: *Build, argv: []const []const u8) []u8 { } var code: u8 = undefined; - return b.runAllowFail(argv, &code, .Inherit) catch |err| { + return b.runAllowFail(argv, &code, .inherit) catch |err| { const printed_cmd = Step.allocPrintCmd(b.allocator, null, null, argv) catch @panic("OOM"); std.debug.print("unable to spawn the following command: {t}\n{s}\n", .{ err, printed_cmd }); process.exit(1); diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 5d6daf9a89..2699e3f01b 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -348,7 +348,7 @@ pub fn captureChildProcess( gpa: Allocator, progress_node: std.Progress.Node, argv: []const []const u8, -) !std.process.Child.RunResult { +) !std.process.RunResult { const graph = s.owner.graph; const arena = graph.arena; const io = graph.io; @@ -360,11 +360,11 @@ pub fn captureChildProcess( try handleChildProcUnsupported(s); try handleVerbose(s.owner, null, argv); - const result = std.process.Child.run(arena, io, .{ + const result = std.process.run(arena, io, .{ .spawn_options = .{ .argv = argv, - .environ = .{ .map = &graph.env_map }, + .env_map = &graph.env_map, .progress_node = progress_node, - }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err }); + } }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err }); if (result.stderr.len > 0) { try s.result_error_msgs.append(arena, result.stderr); @@ -444,19 +444,20 @@ pub fn evalZigProcess( return result; } assert(argv.len != 0); - const arena = b.allocator; try handleChildProcUnsupported(s); try handleVerbose(s.owner, null, argv); - var child = std.process.Child.init(arena, argv, .{ .map = &b.graph.env_map }); - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; - child.request_resource_usage_statistics = true; - child.progress_node = prog_node; - - child.spawn(io) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); + var child = std.process.spawn(io, .{ + .argv = argv, + .env_map = &b.graph.env_map, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, + .request_resource_usage_statistics = true, + .progress_node = prog_node, + }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); + defer if (!watch) child.kill(io); const zp = try gpa.create(ZigProcess); zp.* = .{ @@ -465,7 +466,7 @@ pub fn evalZigProcess( .stdout = child.stdout.?, .stderr = child.stderr.?, }), - .progress_ipc_fd = if (std.Progress.have_ipc) child.progress_node.getIpcFd() else {}, + .progress_ipc_fd = if (std.Progress.have_ipc) prog_node.getIpcFd() else {}, }; if (watch) s.setZigProcess(zp); defer if (!watch) { @@ -487,7 +488,7 @@ pub fn evalZigProcess( // Special handling for Compile step that is expecting compile errors. if (s.cast(Compile)) |compile| switch (term) { - .Exited => { + .exited => { // Note that the exit code may be 0 in this case due to the // compiler server protocol. if (compile.expect_errors != null) { @@ -719,12 +720,15 @@ pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFaile pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void { assert(s.result_failed_command != null); switch (term) { - .Exited => |code| { + .exited => |code| { if (code != 0) { return s.fail("process exited with error code {d}", .{code}); } }, - .Signal, .Stopped, .Unknown => { + .signal => |sig| { + return s.fail("process terminated with signal {t}", .{sig}); + }, + .stopped, .unknown => { return s.fail("process terminated unexpectedly", .{}); }, } diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index 0454e5b79d..915f734386 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -747,7 +747,7 @@ fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult { pkg_name, "--cflags", "--libs", - }, &code, .Ignore)) |stdout| stdout else |err| switch (err) { + }, &code, .ignore)) |stdout| stdout else |err| switch (err) { error.ProcessTerminated => return error.PkgConfigCrashed, error.ExecNotSupported => return error.PkgConfigFailed, error.ExitCodeFailure => return error.PkgConfigFailed, @@ -1847,7 +1847,7 @@ pub fn doAtomicSymLinks( fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg { const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config"; - const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore); + const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore); var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator); errdefer list.deinit(); var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); diff --git a/lib/std/Build/Step/Fmt.zig b/lib/std/Build/Step/Fmt.zig index ff748af979..2da07b63bb 100644 --- a/lib/std/Build/Step/Fmt.zig +++ b/lib/std/Build/Step/Fmt.zig @@ -69,7 +69,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items); if (fmt.check) switch (run_result.term) { - .Exited => |code| if (code != 0 and run_result.stdout.len != 0) { + .exited => |code| if (code != 0 and run_result.stdout.len != 0) { var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n'); while (it.next()) |bad_file_name| { try step.addError("{s}: non-conforming formatting", .{bad_file_name}); diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index 4a5386f8cd..5500d16eca 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -149,7 +149,7 @@ pub const StdIo = union(enum) { expect_stderr_match: []const u8, expect_stdout_exact: []const u8, expect_stdout_match: []const u8, - expect_term: std.process.Child.Term, + expect_term: process.Child.Term, }; }; @@ -618,7 +618,7 @@ pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void { } pub fn expectExitCode(run: *Run, code: u8) void { - const new_check: StdIo.Check = .{ .expect_term = .{ .Exited = code } }; + const new_check: StdIo.Check = .{ .expect_term = .{ .exited = code } }; run.addCheck(new_check); } @@ -1182,40 +1182,40 @@ fn populateGeneratedPaths( } } -fn formatTerm(term: ?std.process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { +fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { if (term) |t| switch (t) { - .Exited => |code| try w.print("exited with code {d}", .{code}), - .Signal => |sig| try w.print("terminated with signal {d}", .{sig}), - .Stopped => |sig| try w.print("stopped with signal {d}", .{sig}), - .Unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), + .exited => |code| try w.print("exited with code {d}", .{code}), + .signal => |sig| try w.print("terminated with signal {t}", .{sig}), + .stopped => |sig| try w.print("stopped with signal {d}", .{sig}), + .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), } else { try w.writeAll("exited with any code"); } } -fn fmtTerm(term: ?std.process.Child.Term) std.fmt.Alt(?std.process.Child.Term, formatTerm) { +fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) { return .{ .data = term }; } -fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term) bool { +fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool { return if (expected) |e| switch (e) { - .Exited => |expected_code| switch (actual) { - .Exited => |actual_code| expected_code == actual_code, + .exited => |expected_code| switch (actual) { + .exited => |actual_code| expected_code == actual_code, else => false, }, - .Signal => |expected_sig| switch (actual) { - .Signal => |actual_sig| expected_sig == actual_sig, + .signal => |expected_sig| switch (actual) { + .signal => |actual_sig| expected_sig == actual_sig, else => false, }, - .Stopped => |expected_sig| switch (actual) { - .Stopped => |actual_sig| expected_sig == actual_sig, + .stopped => |expected_sig| switch (actual) { + .stopped => |actual_sig| expected_sig == actual_sig, else => false, }, - .Unknown => |expected_code| switch (actual) { - .Unknown => |actual_code| expected_code == actual_code, + .unknown => |expected_code| switch (actual) { + .unknown => |actual_code| expected_code == actual_code, else => false, }, } else switch (actual) { - .Exited => true, + .exited => true, else => false, }; } @@ -1526,8 +1526,8 @@ fn runCommand( else => { // On failure, report captured stderr like normal standard error output. const bad_exit = switch (generic_result.term) { - .Exited => |code| code != 0, - .Signal, .Stopped, .Unknown => true, + .exited => |code| code != 0, + .signal, .stopped, .unknown => true, }; if (bad_exit) { if (generic_result.stderr) |bytes| { @@ -1541,7 +1541,7 @@ fn runCommand( } const EvalGenericResult = struct { - term: std.process.Child.Term, + term: process.Child.Term, stdout: ?[]const u8, stderr: ?[]const u8, }; @@ -1555,7 +1555,6 @@ fn spawnChildAndCollect( fuzz_context: ?FuzzContext, ) !?EvalGenericResult { const b = run.step.owner; - const arena = b.allocator; const graph = b.graph; const io = graph.io; @@ -1564,53 +1563,52 @@ fn spawnChildAndCollect( assert(run.stdio == .zig_test); } - var child = std.process.Child.init(arena, argv, .{ .map = env_map }); - if (run.cwd) |lazy_cwd| { - child.cwd = lazy_cwd.getPath2(b, &run.step); - } - child.request_resource_usage_statistics = true; - - child.stdin_behavior = switch (run.stdio) { - .infer_from_args => if (has_side_effects) .Inherit else .Ignore, - .inherit => .Inherit, - .check => .Ignore, - .zig_test => .Pipe, - }; - child.stdout_behavior = switch (run.stdio) { - .infer_from_args => if (has_side_effects) .Inherit else .Ignore, - .inherit => .Inherit, - .check => |checks| if (checksContainStdout(checks.items)) .Pipe else .Ignore, - .zig_test => .Pipe, - }; - child.stderr_behavior = switch (run.stdio) { - .infer_from_args => if (has_side_effects) .Inherit else .Pipe, - .inherit => .Inherit, - .check => .Pipe, - .zig_test => .Pipe, - }; - if (run.captured_stdout != null) child.stdout_behavior = .Pipe; - if (run.captured_stderr != null) child.stderr_behavior = .Pipe; - if (run.stdin != .none) { - assert(run.stdio != .inherit); - child.stdin_behavior = .Pipe; - } + const child_cwd = if (run.cwd) |lazy_cwd| lazy_cwd.getPath2(b, &run.step) else null; // If an error occurs, it's caused by this command: assert(run.step.result_failed_command == null); - run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child.cwd, .{ + run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{ .child = env_map, .parent = &graph.env_map, }, argv); + var spawn_options: process.SpawnOptions = .{ + .argv = argv, + .cwd = child_cwd, + .env_map = &graph.env_map, + .request_resource_usage_statistics = true, + .stdin = if (run.stdin != .none) s: { + assert(run.stdio != .inherit); + break :s .pipe; + } else switch (run.stdio) { + .infer_from_args => if (has_side_effects) .inherit else .ignore, + .inherit => .inherit, + .check => .ignore, + .zig_test => .pipe, + }, + .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) { + .infer_from_args => if (has_side_effects) .inherit else .ignore, + .inherit => .inherit, + .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore, + .zig_test => .pipe, + }, + .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) { + .infer_from_args => if (has_side_effects) .inherit else .pipe, + .inherit => .inherit, + .check => .pipe, + .zig_test => .pipe, + }, + }; + if (run.stdio == .zig_test) { var timer = try std.time.Timer.start(); defer run.step.result_duration_ns = timer.read(); - try evalZigTest(run, &child, options, fuzz_context); + try evalZigTest(run, spawn_options, options, fuzz_context); return null; } else { - const inherit = child.stdout_behavior == .Inherit or child.stderr_behavior == .Inherit; + const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; if (!run.disable_zig_progress and !inherit) { - child.progress_node = options.progress_node; + spawn_options.progress_node = options.progress_node; } const terminal_mode: Io.Terminal.Mode = if (inherit) m: { const stderr = try io.lockStderr(&.{}, graph.stderr_mode); @@ -1619,7 +1617,7 @@ fn spawnChildAndCollect( defer if (inherit) io.unlockStderr(); try setColorEnvironmentVariables(run, env_map, terminal_mode); var timer = try std.time.Timer.start(); - const res = try evalGeneric(run, &child); + const res = try evalGeneric(run, spawn_options); run.step.result_duration_ns = timer.read(); return .{ .term = res.term, .stdout = res.stdout, .stderr = res.stderr }; } @@ -1658,7 +1656,7 @@ const StdioPollEnum = enum { stdout, stderr }; fn evalZigTest( run: *Run, - child: *std.process.Child, + spawn_options: process.SpawnOptions, options: Step.MakeOptions, fuzz_context: ?FuzzContext, ) !void { @@ -1682,14 +1680,14 @@ fn evalZigTest( var test_metadata: ?TestMetadata = null; while (true) { - try child.spawn(io); + var child = try process.spawn(io, spawn_options); var poller = std.Io.poll(gpa, StdioPollEnum, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }); var child_killed = false; defer if (!child_killed) { - _ = child.kill(io) catch {}; + child.kill(io); poller.deinit(); run.step.result_peak_rss = @max( run.step.result_peak_rss, @@ -1697,11 +1695,9 @@ fn evalZigTest( ); }; - try child.waitForSpawn(); - switch (try pollZigTest( run, - child, + &child, options, fuzz_context, &poller, @@ -1763,7 +1759,7 @@ fn evalZigTest( // Report an error if the child terminated uncleanly or if we were still trying to run more tests. run.step.result_stderr = stderr_owned; const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); - if (!tests_done or !termMatches(.{ .Exited = 0 }, term)) { + if (!tests_done or !termMatches(.{ .exited = 0 }, term)) { // The individual unit test results are irrelevant: the test runner itself broke! // Fail immediately without populating `s.test_results`. return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); @@ -1818,7 +1814,7 @@ fn evalZigTest( /// * `poll` fails, indicating the child closed stdout and stderr fn pollZigTest( run: *Run, - child: *std.process.Child, + child: *process.Child, options: Step.MakeOptions, fuzz_context: ?FuzzContext, poller: *std.Io.Poller(StdioPollEnum), @@ -2176,15 +2172,13 @@ fn sendRunFuzzTestMessage( }; } -fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { +fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult { const b = run.step.owner; const io = b.graph.io; const arena = b.allocator; - try child.spawn(io); - errdefer _ = child.kill(io) catch {}; - - try child.waitForSpawn(); + var child = try process.spawn(io, spawn_options); + defer child.kill(io); switch (run.stdin) { .bytes => |bytes| { @@ -2334,10 +2328,10 @@ fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void { => |s| hh.addBytes(s), .expect_term => |term| { - hh.add(@as(std.meta.Tag(std.process.Child.Term), term)); + hh.add(@as(std.meta.Tag(process.Child.Term), term)); switch (term) { - .Exited => |x| hh.add(x), - .Signal, .Stopped, .Unknown => |x| hh.add(x), + inline .exited, .signal => |x| hh.add(x), + .stopped, .unknown => |x| hh.add(x), } }, } diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index 2c53e103cc..ae9a200f24 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -572,11 +572,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim "--listen=-", }); - var child: std.process.Child = .init(gpa, argv.items, .{ .map = &graph.env_map }); - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; - try child.spawn(io); + var child = try std.process.spawn(io, .{ + .argv = argv.items, + .env_map = &graph.env_map, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, + }); + defer child.kill(io); var poller = Io.poll(gpa, enum { stdout, stderr }, .{ .stdout = child.stdout.?, @@ -636,7 +639,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim child.stdin = null; switch (try child.wait(io)) { - .Exited => |code| { + .exited => |code| { if (code != 0) { log.err( "the following command exited with error code {d}:\n{s}", @@ -645,7 +648,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim return error.WasmCompilationFailed; } }, - .Signal, .Stopped, .Unknown => { + .signal => |sig| { + log.err( + "the following command terminated with signal {t}:\n{s}", + .{ sig, try Build.Step.allocPrintCmd(arena, null, null, argv.items) }, + ); + return error.WasmCompilationFailed; + }, + .stopped, .unknown => { log.err( "the following command terminated unexpectedly:\n{s}", .{try Build.Step.allocPrintCmd(arena, null, null, argv.items)}, diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 31dd500df7..9670ac3bd7 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -717,6 +717,12 @@ pub const VTable = struct { tryLockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!?LockedStderr, unlockStderr: *const fn (?*anyopaque) void, processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void, + processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError, + processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError, + processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, + processSpawnPath: *const fn (?*anyopaque, Dir, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, + childWait: *const fn (?*anyopaque, *std.process.Child) std.process.Child.WaitError!std.process.Child.Term, + childKill: *const fn (?*anyopaque, *std.process.Child) void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 50e9f4cf15..df1ce77fe5 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -13,6 +13,7 @@ const File = std.Io.File; const Dir = std.Io.Dir; const HostName = std.Io.net.HostName; const IpAddress = std.Io.net.IpAddress; +const process = std.process; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; const assert = std.debug.assert; @@ -72,7 +73,7 @@ pub const Argv0 = switch (native_os) { pub const Environ = struct { /// Unmodified data directly from the OS. - block: std.process.Environ.Block = &.{}, + block: process.Environ.Block = &.{}, /// Protected by `mutex`. Determines whether the other fields have been /// memoized based on `block`. initialized: bool = false, @@ -95,10 +96,10 @@ pub const Environ = struct { }; pub const String = switch (native_os) { - .openbsd, .haiku => struct { + .windows, .wasi => struct {}, + else => struct { PATH: ?[:0]const u8 = null, }, - else => struct {}, }; }; @@ -1400,6 +1401,12 @@ pub fn io(t: *Threaded) Io { .tryLockStderr = tryLockStderr, .unlockStderr = unlockStderr, .processSetCurrentDir = processSetCurrentDir, + .processReplace = processReplace, // TODO audit for cancelation and unreachable + .processReplacePath = processReplacePath, // TODO audit for cancelation and unreachable + .processSpawn = processSpawn, // TODO audit for cancelation and unreachable + .processSpawnPath = processSpawnPath, // TODO audit for cancelation and unreachable + .childWait = childWait, // TODO audit for cancelation and unreachable + .childKill = childKill, // TODO audit for cancelation and unreachable .now = now, .sleep = sleep, @@ -1538,6 +1545,12 @@ pub fn ioBasic(t: *Threaded) Io { .tryLockStderr = tryLockStderr, .unlockStderr = unlockStderr, .processSetCurrentDir = processSetCurrentDir, + .processReplace = processReplace, + .processReplacePath = processReplacePath, + .processSpawn = processSpawn, + .processSpawnPath = processSpawnPath, + .childWait = childWait, + .childKill = childKill, .now = now, .sleep = sleep, @@ -1601,6 +1614,11 @@ const have_fchmod = switch (native_os) { else => true, }; +const have_wait4 = switch (native_os) { + .dragonfly, .freebsd, .netbsd, .openbsd, .illumos, .linux, .serenity, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => true, + else => false, +}; + const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; @@ -2247,7 +2265,7 @@ fn dirCreateDirPath( ) Dir.CreateDirPathError!Dir.CreatePathStatus { const t: *Threaded = @ptrCast(@alignCast(userdata)); - var it = std.fs.path.componentIterator(sub_path); + var it = Dir.path.componentIterator(sub_path); var status: Dir.CreatePathStatus = .existed; var component = it.last() orelse return error.BadPathName; while (true) { @@ -2307,9 +2325,9 @@ fn dirCreateDirPathOpenWindows( _ = permissions; // TODO apply these permissions - var it = std.fs.path.componentIterator(sub_path); + var it = Dir.path.componentIterator(sub_path); // If there are no components in the path, then create a dummy component with the full path. - var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{ + var component: Dir.path.NativeComponentIterator.Component = it.last() orelse .{ .name = "", .path = sub_path, }; @@ -2347,7 +2365,7 @@ fn dirCreateDirPathOpenWindows( }, &.{ .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, .Attributes = .{}, .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -2986,7 +3004,7 @@ fn dirAccessWindows( }; var attr: windows.OBJECT_ATTRIBUTES = .{ .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, .Attributes = .{}, .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -3535,7 +3553,7 @@ fn dirOpenFileWindows( _ = t; const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_array.span(); - const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; + const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; return dirOpenFileWtf16(dir_handle, sub_path_w, flags); } @@ -3933,7 +3951,7 @@ pub fn dirOpenDirWindows( }, &.{ .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, .Attributes = .{}, .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -5081,7 +5099,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov } }, &.{ .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, .Attributes = .{}, .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -5358,7 +5376,7 @@ fn dirRenameWindows( .POSIX_SEMANTICS = true, .IGNORE_READONLY_ATTRIBUTE = true, }, - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, + .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, .FileName = new_path_w, }); var io_status_block: w.IO_STATUS_BLOCK = undefined; @@ -5387,7 +5405,7 @@ fn dirRenameWindows( if (need_fallback) { const rename_info: w.FILE.RENAME_INFORMATION = .init(.{ .Flags = .{ .REPLACE_IF_EXISTS = replace_if_exists }, - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, + .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, .FileName = new_path_w, }); var io_status_block: w.IO_STATUS_BLOCK = undefined; @@ -5622,13 +5640,13 @@ fn dirSymLinkWindows( // the C:\ drive. .rooted => break :target_path target_path_w.span(), // Keep relative paths relative, but anything else needs to get NT-prefixed. - else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path_w.span())) + else => if (!Dir.path.isAbsoluteWindowsWtf16(target_path_w.span())) break :target_path target_path_w.span(), } } var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span()); // We do this after prefixing to ensure that drive-relative paths are treated as absolute - is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span()); + is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span()); break :target_path prefixed_target_path.span(); }; @@ -5636,8 +5654,8 @@ fn dirSymLinkWindows( var buffer: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4; const header_len = @sizeOf(w.ULONG) + @sizeOf(w.USHORT) * 2; - const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path); - const symlink_data = SYMLINK_DATA{ + const target_is_absolute = Dir.path.isAbsoluteWindowsWtf16(final_target_path); + const symlink_data: SYMLINK_DATA = .{ .ReparseTag = .SYMLINK, .ReparseDataLength = @intCast(buf_len - header_len), .Reserved = 0, @@ -7890,7 +7908,7 @@ fn posixSeekTo(fd: posix.fd_t, offset: u64) File.SeekError!void { } } -fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.process.OpenExecutableError!File { +fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.OpenExecutableError!File { const t: *Threaded = @ptrCast(@alignCast(userdata)); switch (native_os) { .wasi => return error.OperationUnsupported, @@ -7931,7 +7949,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.proce } } -fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.ExecutablePathError!usize { +fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.ExecutablePathError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); switch (native_os) { @@ -11691,14 +11709,14 @@ fn netLookupFallible( fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. - std.process.stderr_thread_mutex.lock(); + process.stderr_thread_mutex.lock(); return initLockedStderr(t, terminal_mode); } fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. - if (!std.process.stderr_thread_mutex.tryLock()) return null; + if (!process.stderr_thread_mutex.tryLock()) return null; return try initLockedStderr(t, terminal_mode); } @@ -11729,10 +11747,10 @@ fn unlockStderr(userdata: ?*anyopaque) void { }; t.stderr_writer.interface.end = 0; t.stderr_writer.interface.buffer = &.{}; - std.process.stderr_thread_mutex.unlock(); + process.stderr_thread_mutex.unlock(); } -fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) std.process.SetCurrentDirError!void { +fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirError!void { if (native_os == .wasi) return error.OperationUnsupported; const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -12690,6 +12708,1640 @@ fn scanEnviron(t: *Threaded) void { } } +fn processReplace(userdata: ?*anyopaque, options: std.process.ReplaceOptions) std.process.ReplaceError { + _ = userdata; + _ = options; + @panic("TODO"); +} + +fn processReplacePath(userdata: ?*anyopaque, dir: Dir, options: std.process.ReplaceOptions) std.process.ReplaceError { + _ = userdata; + _ = dir; + _ = options; + @panic("TODO"); +} + +fn processSpawnPath(userdata: ?*anyopaque, dir: Dir, options: process.SpawnOptions) process.SpawnError!process.Child { + _ = userdata; + _ = dir; + _ = options; + @panic("TODO"); +} + +const processSpawn = switch (native_os) { + .wasi, .ios, .tvos, .visionos, .watchos => processSpawnUnsupported, + .windows => processSpawnWindows, + else => processSpawnPosix, +}; + +fn processSpawnUnsupported(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { + _ = userdata; + _ = options; + return error.OperationUnsupported; +} + +fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + // The child process does need to access (one end of) these pipes. However, + // we must initially set CLOEXEC to avoid a race condition. If another thread + // is racing to spawn a different child process, we don't want it to inherit + // these FDs in any scenario; that would mean that, for instance, calls to + // `poll` from the parent would not report the child's stdout as closing when + // expected, since the other child may retain a reference to the write end of + // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we + // need to do something in the new child to make sure we preserve the reference + // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it + // turns out, we `dup2` everything anyway, so there's no need! + const pipe_flags: posix.O = .{ .CLOEXEC = true }; + + const stdin_pipe = if (options.stdin == .pipe) try posix.pipe2(pipe_flags) else undefined; + errdefer if (options.stdin == .pipe) { + destroyPipe(stdin_pipe); + }; + + const stdout_pipe = if (options.stdout == .pipe) try posix.pipe2(pipe_flags) else undefined; + errdefer if (options.stdout == .pipe) { + destroyPipe(stdout_pipe); + }; + + const stderr_pipe = if (options.stderr == .pipe) try posix.pipe2(pipe_flags) else undefined; + errdefer if (options.stderr == .pipe) { + destroyPipe(stderr_pipe); + }; + + const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore); + const dev_null_fd = if (any_ignore) + posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) { + error.PathAlreadyExists => unreachable, + error.NoSpaceLeft => unreachable, + error.FileTooBig => unreachable, + error.DeviceBusy => unreachable, + error.FileLocksUnsupported => unreachable, + error.BadPathName => unreachable, // Windows-only + error.WouldBlock => unreachable, + error.NetworkNotFound => unreachable, // Windows-only + error.Canceled => unreachable, // temporarily in the posix error set + error.SharingViolation => unreachable, // Windows-only + error.PipeBusy => unreachable, // not a pipe + error.AntivirusInterference => unreachable, // Windows-only + else => |e| return e, + } + else + undefined; + defer { + if (any_ignore) posix.close(dev_null_fd); + } + + const prog_pipe: [2]posix.fd_t = p: { + if (options.progress_node.index == .none) { + break :p .{ -1, -1 }; + } else { + // We use CLOEXEC for the same reason as in `pipe_flags`. + break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); + } + }; + errdefer destroyPipe(prog_pipe); + + var arena_allocator = std.heap.ArenaAllocator.init(t.allocator); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + // The POSIX standard does not allow malloc() between fork() and execve(), + // and this allocator may be a libc allocator. + // I have personally observed the child process deadlocking when it tries + // to call malloc() due to a heap allocation between fork() and execve(), + // in musl v1.1.24. + // Additionally, we want to reduce the number of possible ways things + // can fail between fork() and execve(). + // Therefore, we do all the allocation for the execve() before the fork(). + // This means we must do the null-termination of argv and env vars here. + const argv_buf = try arena.allocSentinel(?[*:0]const u8, options.argv.len, null); + for (options.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; + + const prog_fileno = 3; + comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno); + + const envp: [*:null]const ?[*:0]const u8 = m: { + const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno; + if (options.env_map) |env_map| { + break :m (try env_map.createBlock(arena, .{ + .zig_progress_fd = prog_fd, + })).ptr; + } + break :m (try process.Environ.createBlock(.{ .block = t.environ.block }, arena, .{ + .zig_progress_fd = prog_fd, + })).ptr; + }; + + // This pipe communicates to the parent errors in the child between `fork` and `execvpe`. + // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds. + const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true }); + errdefer destroyPipe(err_pipe); + + t.scanEnviron(); // for PATH + const PATH = t.environ.string.PATH orelse "/usr/local/bin:/bin/:/usr/bin"; + + const pid_result = try posix.fork(); + if (pid_result == 0) { + // we are the child + setUpChildIo(options.stdin, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err); + setUpChildIo(options.stdout, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err); + setUpChildIo(options.stderr, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err); + + if (options.cwd_dir) |cwd| { + posix.fchdir(cwd.handle) catch |err| forkBail(err_pipe[1], err); + } else if (options.cwd) |cwd| { + posix.chdir(cwd) catch |err| forkBail(err_pipe[1], err); + } + + // Must happen after fchdir above, the cwd file descriptor might be + // equal to prog_fileno and be clobbered by this dup2 call. + if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkBail(err_pipe[1], err); + + if (options.gid) |gid| { + posix.setregid(gid, gid) catch |err| forkBail(err_pipe[1], err); + } + + if (options.uid) |uid| { + switch (posix.errno(posix.system.setreuid(uid, uid))) { + .SUCCESS => {}, + .AGAIN => forkBail(err_pipe[1], error.ResourceLimitReached), + .INVAL => forkBail(err_pipe[1], error.InvalidUserId), + .PERM => forkBail(err_pipe[1], error.PermissionDenied), + else => forkBail(err_pipe[1], error.Unexpected), + } + } + + if (options.pgid) |pid| { + switch (posix.errno(posix.system.setpgid(0, pid))) { + .SUCCESS => {}, + .ACCES => forkBail(err_pipe[1], error.ProcessAlreadyExec), + .INVAL => forkBail(err_pipe[1], error.InvalidProcessGroupId), + .PERM => forkBail(err_pipe[1], error.PermissionDenied), + else => forkBail(err_pipe[1], error.Unexpected), + } + } + + if (options.start_suspended) { + switch (posix.errno(posix.system.kill(posix.system.getpid(), .STOP))) { + .SUCCESS => {}, + .PERM => forkBail(err_pipe[1], error.PermissionDenied), + else => forkBail(err_pipe[1], error.Unexpected), + } + } + + const err = execvpeZ_expandArg0(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, envp, PATH); + forkBail(err_pipe[1], err); + } + + const pid: posix.pid_t = @intCast(pid_result); // We are the parent. + + posix.close(err_pipe[1]); // make sure only the child holds the write end open + defer posix.close(err_pipe[0]); + + if (options.stdin == .pipe) posix.close(stdin_pipe[0]); + if (options.stdout == .pipe) posix.close(stdout_pipe[1]); + if (options.stderr == .pipe) posix.close(stderr_pipe[1]); + + if (prog_pipe[1] != -1) posix.close(prog_pipe[1]); + + options.progress_node.setIpcFd(prog_pipe[0]); + + // Wait for the child to report any errors in or before `execvpe`. + if (readIntFd(t, err_pipe[0])) |child_err_int| { + const child_err: process.SpawnError = @errorCast(@errorFromInt(child_err_int)); + return child_err; + } else |read_err| switch (read_err) { + error.EndOfStream => { + // Write end closed by CLOEXEC at the time of the `execvpe` call, + // indicating success. + }, + else => { + // Problem reading the error from the error reporting pipe. We + // don't know if the child is alive or dead. Better to assume it is + // alive so the resource does not risk being leaked. + }, + } + + return .{ + .id = pid, + .stdin = switch (options.stdin) { + .pipe => .{ .handle = stdin_pipe[1] }, + else => null, + }, + .stdout = switch (options.stdout) { + .pipe => .{ .handle = stdout_pipe[0] }, + else => null, + }, + .stderr = switch (options.stderr) { + .pipe => .{ .handle = stderr_pipe[0] }, + else => null, + }, + .request_resource_usage_statistics = options.request_resource_usage_statistics, + }; +} + +fn childWait(userdata: ?*anyopaque, child: *std.process.Child) process.Child.WaitError!process.Child.Term { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + switch (native_os) { + .windows => return childWaitWindows(t, child), + else => return childWaitPosix(t, child), + } +} + +fn childKill(userdata: ?*anyopaque, child: *std.process.Child) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + if (is_windows) { + childKillWindows(t, child, 1) catch { + childCleanupStreams(child); + child.id = null; + }; + } else { + childKillPosix(t, child) catch { + childCleanupStreams(child); + child.id = null; + }; + } +} + +fn childKillWindows(t: *Threaded, child: *process.Child, exit_code: windows.UINT) !void { + windows.TerminateProcess(child.id, exit_code) catch |err| switch (err) { + error.AccessDenied => { + // 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(child.id, 0, false) catch return err; + return error.AlreadyTerminated; + }, + else => return err, + }; + try childWaitWindows(t, child); +} + +fn childWaitWindows(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term { + _ = t; // TODO cancelation + windows.WaitForSingleObjectEx(child.id, windows.INFINITE, false); + + const term: process.Child.Term = x: { + var exit_code: windows.DWORD = undefined; + if (windows.kernel32.GetExitCodeProcess(child.id, &exit_code) == 0) { + break :x .{ .unknown = 0 }; + } else { + break :x .{ .exited = @as(u8, @truncate(exit_code)) }; + } + }; + + if (child.request_resource_usage_statistics) { + child.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(child.id); + } + + posix.close(child.id); + posix.close(child.thread_handle); + childCleanupStreams(child); + child.id = null; + return term; +} + +fn childWaitPosix(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term { + _ = t; // TODO cancelation + const pid = child.id.?; + const res: posix.WaitPidResult = res: { + if (child.request_resource_usage_statistics and have_wait4) { + var ru: posix.rusage = undefined; + const res = posix.wait4(pid, 0, &ru); + child.resource_usage_statistics.rusage = ru; + break :res res; + } + break :res posix.waitpid(pid, 0); + }; + const status = res.status; + childCleanupStreams(child); + child.id = null; + return statusToTerm(status); +} + +fn statusToTerm(status: u32) process.Child.Term { + return if (posix.W.IFEXITED(status)) + .{ .exited = posix.W.EXITSTATUS(status) } + else if (posix.W.IFSIGNALED(status)) + .{ .signal = posix.W.TERMSIG(status) } + else if (posix.W.IFSTOPPED(status)) + .{ .stopped = posix.W.STOPSIG(status) } + else + .{ .unknown = status }; +} + +fn childKillPosix(t: *Threaded, child: *process.Child) !void { + try posix.kill(child.id.?, posix.SIG.TERM); + _ = try childWaitPosix(t, child); +} + +fn childCleanupStreams(child: *process.Child) void { + if (child.stdin) |*stdin| { + posix.close(stdin.handle); + child.stdin = null; + } + if (child.stdout) |*stdout| { + posix.close(stdout.handle); + child.stdout = null; + } + if (child.stderr) |*stderr| { + posix.close(stderr.handle); + child.stderr = null; + } +} + +/// Errors that can occur between fork() and execv() +const ForkBailError = process.SpawnError || process.ReplaceError; + +/// Child of fork calls this to report an error to the fork parent. Then the +/// child exits. +fn forkBail(fd: posix.fd_t, err: ForkBailError) 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.system.exit(1); +} + +fn writeIntFd(fd: posix.fd_t, value: ErrInt) !void { + var buffer: [8]u8 = undefined; + std.mem.writeInt(u64, &buffer, value, .little); + // Skip the cancel mechanism. + var i: usize = 0; + while (true) { + const rc = posix.system.write(fd, buffer[i..].ptr, buffer.len - i); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @intCast(rc); + i += n; + if (buffer.len - i == 0) return; + }, + .INTR => continue, + else => return error.SystemResources, + } + } +} + +fn readIntFd(t: *Threaded, fd: posix.fd_t) !ErrInt { + _ = t; // TODO cancelation + var buffer: [8]u8 = undefined; + var i: usize = 0; + while (true) { + const rc = posix.system.read(fd, buffer[i..].ptr, buffer.len - i); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @intCast(rc); + if (n == 0) return error.EndOfStream; + i += n; + continue; + }, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + } + return @intCast(std.mem.readInt(u64, &buffer, .little)); +} + +const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); + +fn destroyPipe(pipe: [2]posix.fd_t) void { + if (pipe[0] != -1) posix.close(pipe[0]); + if (pipe[0] != pipe[1]) posix.close(pipe[1]); +} + +fn setUpChildIo(stdio: process.SpawnOptions.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), + .file => @panic("TODO implement setUpChildIo when file is used"), + } +} + +fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.SpawnError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + + var saAttr: windows.SECURITY_ATTRIBUTES = .{ + .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), + .bInheritHandle = windows.TRUE, + .lpSecurityDescriptor = null, + }; + + const any_ignore = + child.stdin_behavior == .ignore or + child.stdout_behavior == .ignore or + child.stderr_behavior == .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 = .{ + .STANDARD = .{ .SYNCHRONIZE = true }, + .GENERIC = .{ .WRITE = true, .READ = true }, + }, + .sa = &saAttr, + .creation = .OPEN, + }) catch |err| switch (err) { + error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" + error.PipeBusy => return error.Unexpected, // not possible for "NUL" + error.NoDevice => 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" + error.OperationCanceled => return error.Unexpected, // we're not canceling the operation + 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 (child.stdin_behavior) { + .pipe => { + try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); + }, + .ignore => { + g_hChildStd_IN_Rd = nul_handle; + }, + .inherit => { + g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; + }, + .close => { + g_hChildStd_IN_Rd = null; + }, + } + errdefer if (child.stdin_behavior == .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 (child.stdout_behavior) { + .pipe => { + try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); + }, + .ignore => { + g_hChildStd_OUT_Wr = nul_handle; + }, + .inherit => { + g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; + }, + .close => { + g_hChildStd_OUT_Wr = null; + }, + } + errdefer if (child.stdout_behavior == .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 (child.stderr_behavior) { + .pipe => { + try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); + }, + .ignore => { + g_hChildStd_ERR_Wr = nul_handle; + }, + .inherit => { + g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; + }, + .close => { + g_hChildStd_ERR_Wr = null; + }, + } + errdefer if (child.stderr_behavior == .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 (child.cwd) |cwd| try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd) else null; + defer if (cwd_w) |cwd| child.allocator.free(cwd); + const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; + + const maybe_envp_buf = if (child.env_map) |env_map| try process.createWindowsEnvBlock(child.allocator, env_map) else null; + defer if (maybe_envp_buf) |envp_buf| child.allocator.free(envp_buf); + const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; + + const app_name_wtf8 = child.argv[0]; + const app_name_is_absolute = Dir.path.isAbsolute(app_name_wtf8); + + // the cwd set in Child 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 = Dir.path.dirname(app_name_wtf8).?; + break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, dir); + } else if (child.cwd) |cwd| { + cwd_path_w_needs_free = true; + break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd); + } else { + break :x &[_:0]u16{}; // empty for cwd + } + }; + defer if (cwd_path_w_needs_free) child.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 = Dir.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) Dir.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 std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_dirname_wtf8); + } + break :x null; + }; + defer if (app_dirname_w != null) child.allocator.free(app_dirname_w.?); + + const app_name_w = try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_basename_wtf8); + defer child.allocator.free(app_name_w); + + const flags: windows.CreateProcessFlags = .{ + .create_suspended = child.start_suspended, + .create_unicode_environment = true, + .create_no_window = child.create_no_window, + }; + + run: { + const PATH: [:0]const u16 = process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; + const PATHEXT: [:0]const u16 = process.getenvW(std.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(child.allocator, child.argv); + defer cmd_line_cache.deinit(); + + var app_buf: std.ArrayList(u16) = .empty; + defer app_buf.deinit(child.allocator); + + try app_buf.appendSlice(child.allocator, app_name_w); + + var dir_buf: std.ArrayList(u16) = .empty; + defer dir_buf.deinit(child.allocator); + + if (cwd_path_w.len > 0) { + try dir_buf.appendSlice(child.allocator, cwd_path_w); + } + if (app_dirname_w) |app_dir| { + if (dir_buf.items.len > 0) try dir_buf.append(child.allocator, Dir.path.sep); + try dir_buf.appendSlice(child.allocator, app_dir); + } + + windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &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 Child 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 = std.mem.tokenizeScalar(u16, PATH, ';'); + while (it.next()) |search_path| { + dir_buf.clearRetainingCapacity(); + try dir_buf.appendSlice(child.allocator, search_path); + + if (windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &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| { + child.stdin = File{ .handle = h }; + } else { + child.stdin = null; + } + if (g_hChildStd_OUT_Rd) |h| { + child.stdout = File{ .handle = h }; + } else { + child.stdout = null; + } + if (g_hChildStd_ERR_Rd) |h| { + child.stderr = File{ .handle = h }; + } else { + child.stderr = null; + } + + child.id = piProcInfo.hProcess; + child.thread_handle = piProcInfo.hThread; + child.term = null; + + if (child.stdin_behavior == .pipe) { + posix.close(g_hChildStd_IN_Rd.?); + } + if (child.stderr_behavior == .pipe) { + posix.close(g_hChildStd_ERR_Wr.?); + } + if (child.stdout_behavior == .pipe) { + posix.close(g_hChildStd_OUT_Wr.?); + } +} + +/// 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: Allocator, + dir_buf: *std.ArrayList(u16), + app_buf: *std.ArrayList(u16), + pathext: [:0]const u16, + cmd_line_cache: *WindowsCommandLineCache, + envp_ptr: ?[*]u16, + cwd_ptr: ?[*:0]u16, + flags: windows.CreateProcessFlags, + 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. + + // In the future, child process execution needs to move to Io implementation. + // Under those conditions, here we will have access to lower level directory + // opening function knowing which implementation we are in. Here, we imitate + // that scenario. + 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 dirOpenDirWindows(.cwd(), prefixed_path.span(), .{ + .iterate = true, + }) catch return error.FileNotFound; + }; + defer windows.CloseHandle(dir.handle); + + // 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(process.WindowsExtension).@"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.handle, + null, + null, + null, + &io_status, + &file_information_buf, + file_information_buf.len, + .Directory, + 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.DIRECTORY) 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, Dir.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, flags, 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, std.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 = std.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, Dir.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, flags, 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, std.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, + flags: windows.CreateProcessFlags, + 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, + flags, + @as(?*anyopaque, @ptrCast(envp_ptr)), + cwd_ptr, + lpStartupInfo, + lpProcessInformation, + ); +} + +/// Case-insensitive WTF-16 lookup +fn windowsCreateProcessSupportsExtension(ext: []const u16) ?process.WindowsExtension { + comptime { + // Ensures keeping this function in sync with the enum. + const fields = @typeInfo(process.WindowsExtension).@"enum".fields; + assert(fields.len == 4); + assert(@intFromEnum(process.WindowsExtension.bat) == 0); + assert(@intFromEnum(process.WindowsExtension.cmd) == 1); + assert(@intFromEnum(process.WindowsExtension.com) == 2); + assert(@intFromEnum(process.WindowsExtension.exe) == 3); + } + + 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(process.WindowsExtension.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: Allocator, + + fn init(allocator: 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.?; + } +}; + +/// Returns the absolute path of `cmd.exe` within the Windows system directory. +/// The caller owns the returned slice. +fn windowsCmdExePath(allocator: Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { + var buf = try std.ArrayList(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.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, Dir.path.sep), + } + try buf.appendSlice(allocator, std.unicode.utf8ToUtf16LeStringLiteral("cmd.exe")); + return try buf.toOwnedSliceSentinel(allocator, 0); +} + +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/ +/// +/// The return of this function will look like +/// `cmd.exe /d /e:ON /v:OFF /c ""` +/// and should be used as the `lpCommandLine` of `CreateProcessW`, while the +/// return of `windowsCmdExePath` should be used as `lpApplicationName`. +/// +/// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise. +/// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem. +fn argvToScriptCommandLineWindows( + allocator: 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.array_list.Managed(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 (std.mem.findAny(u16, script_path, &[_]u16{ + std.mem.nativeToLittle(u16, '\\'), std.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.findAny(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 std.unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); +} + +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. +/// +/// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts. +/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ +/// +/// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead. +fn argvToCommandLineWindows( + allocator: Allocator, + argv: []const []const u8, +) ArgvToCommandLineError![:0]u16 { + var buf = std.array_list.Managed(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 std.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 std.unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w); + defer std.testing.allocator.free(cmd_line); + + try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); +} + +/// Replaces the current process image with the executed process. If this +/// function succeeds, it does not return. +/// +/// This operation is not available on all targets. `can_execv` +/// +/// This function also uses the PATH environment variable to get the full path to the executable. +/// If `file` is an absolute path, this is the same as `execveZ`. +/// +/// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable, +/// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall. +/// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in. +fn execvpeZ_expandArg0( + arg0_expand: process.ArgExpansion, + file: [*:0]const u8, + child_argv: [*:null]?[*:0]const u8, + envp: [*:null]const ?[*:0]const u8, + PATH: []const u8, +) process.ReplaceError { + const file_slice = std.mem.sliceTo(file, 0); + if (std.mem.findScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp); + + // Use of PATH_MAX here is valid as the path_buf will be passed + // directly to the operating system in execveZ. + var path_buf: [posix.PATH_MAX]u8 = undefined; + var it = std.mem.tokenizeScalar(u8, PATH, ':'); + var seen_eacces = false; + var err: process.ReplaceError = error.FileNotFound; + + // In case of expanding arg0 we must put it back if we return with an error. + const prev_arg0 = child_argv[0]; + defer switch (arg0_expand) { + .expand => child_argv[0] = prev_arg0, + .no_expand => {}, + }; + + while (it.next()) |search_path| { + const path_len = search_path.len + file_slice.len + 1; + if (path_buf.len < path_len + 1) return error.NameTooLong; + @memcpy(path_buf[0..search_path.len], search_path); + path_buf[search_path.len] = '/'; + @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice); + path_buf[path_len] = 0; + const full_path = path_buf[0..path_len :0].ptr; + switch (arg0_expand) { + .expand => child_argv[0] = full_path, + .no_expand => {}, + } + err = execveZ(full_path, child_argv, envp); + switch (err) { + error.AccessDenied => seen_eacces = true, + error.FileNotFound, error.NotDir => {}, + else => |e| return e, + } + } + if (seen_eacces) return error.AccessDenied; + return err; +} + +/// This function ignores PATH environment variable. See `execvpeZ` for that. +pub fn execveZ( + path: [*:0]const u8, + child_argv: [*:null]const ?[*:0]const u8, + envp: [*:null]const ?[*:0]const u8, +) process.ReplaceError { + switch (posix.errno(posix.system.execve(path, child_argv, envp))) { + .SUCCESS => unreachable, + .FAULT => |err| return errnoBug(err), // Bad pointer parameter. + .@"2BIG" => return error.SystemResources, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .INVAL => return error.InvalidExe, + .NOEXEC => return error.InvalidExe, + .IO => return error.FileSystem, + .LOOP => return error.FileSystem, + .ISDIR => return error.IsDir, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .TXTBSY => return error.FileBusy, + else => |err| switch (native_os) { + .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => switch (err) { + .BADEXEC => return error.InvalidExe, + .BADARCH => return error.InvalidExe, + else => return posix.unexpectedErrno(err), + }, + .linux => switch (err) { + .LIBBAD => return error.InvalidExe, + else => return posix.unexpectedErrno(err), + }, + else => return posix.unexpectedErrno(err), + }, + } +} + +/// This function also uses the PATH environment variable to get the full path to the executable. +/// If `file` is an absolute path, this is the same as `execveZ`. +pub fn execvpeZ( + file: [*:0]const u8, + argv_ptr: [*:null]const ?[*:0]const u8, + envp: [*:null]const ?[*:0]const u8, + optional_PATH: ?[]const u8, +) process.ReplaceError { + return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp, optional_PATH); +} + +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.bufPrintSentinel( + &tmp_buf, + "\\\\.\\pipe\\zig-childprocess-{d}-{d}", + .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, + 0, + ) 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.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, + .{ .GENERIC = .{ .WRITE = true } }, + 0, + &sattr_copy, + windows.OPEN_EXISTING, + @bitCast(windows.FILE.ATTRIBUTE{ .NORMAL = true }), + null, + ); + if (write_handle == windows.INVALID_HANDLE_VALUE) { + switch (windows.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); + test { _ = @import("Threaded/test.zig"); } diff --git a/lib/std/c.zig b/lib/std/c.zig index 9bf4bef5d2..41c1cd52c0 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -3764,8 +3764,8 @@ pub const W = switch (native_os) { pub fn EXITSTATUS(x: u32) u8 { return @as(u8, @intCast(x >> 8)); } - pub fn TERMSIG(x: u32) u32 { - return status(x); + pub fn TERMSIG(x: u32) SIG { + return @enumFromInt(status(x)); } pub fn STOPSIG(x: u32) u32 { return x >> 8; @@ -3797,14 +3797,14 @@ pub const W = switch (native_os) { pub fn EXITSTATUS(s: u32) u8 { return @as(u8, @intCast((s & 0xff00) >> 8)); } - pub fn TERMSIG(s: u32) u32 { - return s & 0x7f; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn STOPSIG(s: u32) u32 { return EXITSTATUS(s); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFSTOPPED(s: u32) bool { return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00; @@ -3825,14 +3825,14 @@ pub const W = switch (native_os) { pub fn EXITSTATUS(s: u32) u8 { return @as(u8, @intCast((s >> 8) & 0xff)); } - pub fn TERMSIG(s: u32) u32 { - return s & 0x7f; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn STOPSIG(s: u32) u32 { return EXITSTATUS(s); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFCONTINUED(s: u32) bool { @@ -3859,14 +3859,14 @@ pub const W = switch (native_os) { pub fn EXITSTATUS(s: u32) u8 { return @as(u8, @intCast((s >> 8) & 0xff)); } - pub fn TERMSIG(s: u32) u32 { - return s & 0x7f; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn STOPSIG(s: u32) u32 { return EXITSTATUS(s); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFCONTINUED(s: u32) bool { @@ -3893,14 +3893,14 @@ pub const W = switch (native_os) { pub fn EXITSTATUS(s: u32) u8 { return @as(u8, @intCast((s & 0xff00) >> 8)); } - pub fn TERMSIG(s: u32) u32 { - return s & 0x7f; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn STOPSIG(s: u32) u32 { return EXITSTATUS(s); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFSTOPPED(s: u32) bool { return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00; @@ -3921,8 +3921,8 @@ pub const W = switch (native_os) { return @as(u8, @intCast(s & 0xff)); } - pub fn TERMSIG(s: u32) u32 { - return (s >> 8) & 0xff; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt((s >> 8) & 0xff); } pub fn STOPSIG(s: u32) u32 { @@ -3949,14 +3949,14 @@ pub const W = switch (native_os) { pub fn EXITSTATUS(s: u32) u8 { return @as(u8, @intCast((s >> 8) & 0xff)); } - pub fn TERMSIG(s: u32) u32 { - return (s & 0x7f); + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn STOPSIG(s: u32) u32 { return EXITSTATUS(s); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFCONTINUED(s: u32) bool { @@ -3988,12 +3988,12 @@ pub const W = switch (native_os) { return EXITSTATUS(s); } - pub fn TERMSIG(s: u32) u32 { - return s & 0x7f; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFSTOPPED(s: u32) bool { diff --git a/lib/std/os/emscripten.zig b/lib/std/os/emscripten.zig index cb444b360d..a7fb141ed4 100644 --- a/lib/std/os/emscripten.zig +++ b/lib/std/os/emscripten.zig @@ -224,14 +224,14 @@ pub const W = struct { pub fn EXITSTATUS(s: u32) u8 { return @as(u8, @intCast((s & 0xff00) >> 8)); } - pub fn TERMSIG(s: u32) u32 { - return s & 0x7f; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn STOPSIG(s: u32) u32 { return EXITSTATUS(s); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFSTOPPED(s: u32) bool { return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00; diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 174ef64d95..68baa9c626 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -3616,14 +3616,14 @@ pub const W = struct { pub fn EXITSTATUS(s: u32) u8 { return @as(u8, @intCast((s & 0xff00) >> 8)); } - pub fn TERMSIG(s: u32) u32 { - return s & 0x7f; + pub fn TERMSIG(s: u32) SIG { + return @enumFromInt(s & 0x7f); } pub fn STOPSIG(s: u32) u32 { return EXITSTATUS(s); } pub fn IFEXITED(s: u32) bool { - return TERMSIG(s) == 0; + return (s & 0x7f) == 0; } pub fn IFSTOPPED(s: u32) bool { return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 55b19df995..72ddd27676 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -795,79 +795,10 @@ pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void { } } -pub fn getpid() pid_t { - return system.getpid(); -} - pub fn getppid() pid_t { return system.getppid(); } -pub const ExecveError = error{ - SystemResources, - AccessDenied, - PermissionDenied, - InvalidExe, - FileSystem, - IsDir, - FileNotFound, - NotDir, - FileBusy, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - NameTooLong, -} || UnexpectedError; - -/// This function ignores PATH environment variable. See `execvpeZ` for that. -pub fn execveZ( - path: [*:0]const u8, - child_argv: [*:null]const ?[*:0]const u8, - envp: [*:null]const ?[*:0]const u8, -) ExecveError { - switch (errno(system.execve(path, child_argv, envp))) { - .SUCCESS => unreachable, - .FAULT => unreachable, - .@"2BIG" => return error.SystemResources, - .MFILE => return error.ProcessFdQuotaExceeded, - .NAMETOOLONG => return error.NameTooLong, - .NFILE => return error.SystemFdQuotaExceeded, - .NOMEM => return error.SystemResources, - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .INVAL => return error.InvalidExe, - .NOEXEC => return error.InvalidExe, - .IO => return error.FileSystem, - .LOOP => return error.FileSystem, - .ISDIR => return error.IsDir, - .NOENT => return error.FileNotFound, - .NOTDIR => return error.NotDir, - .TXTBSY => return error.FileBusy, - else => |err| switch (native_os) { - .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => switch (err) { - .BADEXEC => return error.InvalidExe, - .BADARCH => return error.InvalidExe, - else => return unexpectedErrno(err), - }, - .linux => switch (err) { - .LIBBAD => return error.InvalidExe, - else => return unexpectedErrno(err), - }, - else => return unexpectedErrno(err), - }, - } -} - -/// This function also uses the PATH environment variable to get the full path to the executable. -/// If `file` is an absolute path, this is the same as `execveZ`. -pub fn execvpeZ( - file: [*:0]const u8, - argv_ptr: [*:null]const ?[*:0]const u8, - envp: [*:null]const ?[*:0]const u8, - optional_PATH: ?[]const u8, -) ExecveError { - return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp, optional_PATH); -} - pub const GetCwdError = error{ NameTooLong, CurrentWorkingDirectoryUnlinked, @@ -1119,16 +1050,6 @@ pub fn seteuid(uid: uid_t) SetEidError!void { } } -pub fn setreuid(ruid: uid_t, euid: uid_t) SetIdError!void { - switch (errno(system.setreuid(ruid, euid))) { - .SUCCESS => return, - .AGAIN => return error.ResourceLimitReached, - .INVAL => return error.InvalidUserId, - .PERM => return error.PermissionDenied, - else => |err| return unexpectedErrno(err), - } -} - pub fn setgid(gid: gid_t) SetIdError!void { switch (errno(system.setgid(gid))) { .SUCCESS => return, @@ -1158,24 +1079,6 @@ pub fn setregid(rgid: gid_t, egid: gid_t) SetIdError!void { } } -pub const SetPgidError = error{ - ProcessAlreadyExec, - InvalidProcessGroupId, - PermissionDenied, - ProcessNotFound, -} || UnexpectedError; - -pub fn setpgid(pid: pid_t, pgid: pid_t) SetPgidError!void { - switch (errno(system.setpgid(pid, pgid))) { - .SUCCESS => return, - .ACCES => return error.ProcessAlreadyExec, - .INVAL => return error.InvalidProcessGroupId, - .PERM => return error.PermissionDenied, - .SRCH => return error.ProcessNotFound, - else => |err| return unexpectedErrno(err), - } -} - pub fn getuid() uid_t { return system.getuid(); } diff --git a/lib/std/process.zig b/lib/std/process.zig index 4221356bdd..1fd31d2634 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -259,20 +259,44 @@ pub fn getBaseAddress() usize { } } -/// Deprecated in favor of `Child.can_spawn`. -pub const can_spawn = Child.can_spawn; -/// Deprecated in favor of `can_replace`. -pub const can_execv = can_replace; - /// Tells whether the target operating system supports replacing the current -/// process image. If this is `false` then calling `execv` or `replace` -/// functions will cause compilation to fail. +/// process image. If this is `false` then calling `replace` or `replaceFile` +/// functions will return `error.OperationUnsupported`. pub const can_replace = switch (native_os) { .windows, .haiku, .wasi => false, else => true, }; -pub const ReplaceError = std.posix.ExecveError || error{OutOfMemory}; +/// Tells whether spawning child processes is supported. +pub const can_spawn = switch (native_os) { + .wasi, .ios, .tvos, .visionos, .watchos => false, + else => true, +}; + +pub const ReplaceError = error{ + /// The target operating system cannot replace the process image with a new + /// one. + OperationUnsupported, + SystemResources, + AccessDenied, + PermissionDenied, + InvalidExe, + FileSystem, + IsDir, + FileNotFound, + NotDir, + FileBusy, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, +} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; + +pub const ReplaceOptions = struct { + argv: []const []const u8, + arg0_expand: ArgExpansion = .no_expand, + /// Replaces the environment when provided. The PATH value from here is + /// never used to resolve `argv[0]`. + env_map: ?*const Environ.Map = null, +}; /// Replaces the current process image with the executed process. If this /// function succeeds, it does not return. @@ -281,25 +305,9 @@ pub const ReplaceError = std.posix.ExecveError || error{OutOfMemory}; /// is not already a file path (i.e. it contains '/'), it is resolved into a /// file path based on PATH from the parent environment. /// -/// This operation is not available on targets for which `can_replace` is -/// `false`. -/// -/// This function must allocate memory to add a null terminating bytes on path -/// and each arg. -/// -/// Due to the heap allocation, it is illegal to call this function in a fork() -/// child. -pub fn replace(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Block) ReplaceError { - if (!can_replace) @compileError("unsupported operation: replace"); - - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - const argv_buf = try arena.allocSentinel(?[*:0]const u8, argv.len, null); - for (argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; - - return posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, env); +/// It is illegal to call this function in a fork() child. +pub fn replace(io: Io, options: ReplaceOptions) ReplaceError { + return io.vtable.processReplace(io.userdata, options); } /// Replaces the current process image with the executed process. If this @@ -309,92 +317,181 @@ pub fn replace(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Bl /// relative to `dir`. It is *always* treated as a file path, even if it does /// not contain '/'. /// -/// This operation is not available on targets for which `can_replace` is -/// `false`. -/// -/// This function must allocate memory to add a null terminating bytes on path -/// and each arg. -/// -/// Due to the heap allocation, it is illegal to call this -/// function in a fork() child. For that use case, use the `std.posix` -/// functions directly. -pub fn replaceFile(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Block) ReplaceError { - if (!can_replace) @compileError("unsupported operation: replaceFile"); - - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - const argv_buf = try arena.allocSentinel(?[*:0]const u8, argv.len, null); - for (argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; - - return posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, env); +/// It is illegal to call this function in a fork() child. +pub fn replacePath(io: Io, dir: Io.Dir, options: ReplaceOptions) ReplaceError { + return io.vtable.processReplacePath(io.userdata, dir, options); } -pub const Arg0Expand = enum { expand, no_expand }; +pub const ArgExpansion = enum { expand, no_expand }; -/// Replaces the current process image with the executed process. If this -/// function succeeds, it does not return. -/// -/// This operation is not available on all targets. `can_execv` -/// -/// This function also uses the PATH environment variable to get the full path to the executable. -/// If `file` is an absolute path, this is the same as `execveZ`. -/// -/// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable, -/// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall. -/// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in. -pub fn replace( - comptime arg0_expand: Arg0Expand, - file: [*:0]const u8, - child_argv: switch (arg0_expand) { - .expand => [*:null]?[*:0]const u8, - .no_expand => [*:null]const ?[*:0]const u8, - }, - envp: [*:null]const ?[*:0]const u8, - optional_PATH: ?[]const u8, -) ExecveError { - const file_slice = mem.sliceTo(file, 0); - if (mem.findScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp); +/// File name extensions supported natively by `CreateProcess()` on Windows. +pub const WindowsExtension = enum { bat, cmd, com, exe }; - const PATH = optional_PATH orelse "/usr/local/bin:/bin/:/usr/bin"; - // Use of PATH_MAX here is valid as the path_buf will be passed - // directly to the operating system in execveZ. - var path_buf: [PATH_MAX]u8 = undefined; - var it = mem.tokenizeScalar(u8, PATH, ':'); - var seen_eacces = false; - var err: ExecveError = error.FileNotFound; +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://wtf-8.codeberg.page/ + 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, + SystemResources, + AccessDenied, + PermissionDenied, + InvalidExe, + FileSystem, + IsDir, + FileNotFound, + NotDir, + FileBusy, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + ResourceLimitReached, + InvalidUserId, + InvalidProcessGroupId, + SymLinkLoop, + InvalidName, + /// An attempt was made to change the process group ID of one of the + /// children of the calling process and the child had already performed an + /// image replacement. + ProcessAlreadyExec, +} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; - // In case of expanding arg0 we must put it back if we return with an error. - const prev_arg0 = child_argv[0]; - defer switch (arg0_expand) { - .expand => child_argv[0] = prev_arg0, - .no_expand => {}, +pub const SpawnOptions = struct { + argv: []const []const u8, + + /// Set to change the current working directory when spawning the child process. + cwd: ?[]const u8 = null, + /// 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: ?Io.Dir = null, + /// Replaces the child environment when provided. The PATH value from here + /// is not used to resolve `argv[0]`; that resolution always uses parent + /// environment. + env_map: ?*const Environ.Map = null, + expand_arg0: ArgExpansion = .no_expand, + /// When populated, a pipe will be created for the child process to + /// communicate progress back to the parent. The file descriptor of the + /// write end of the pipe will be specified in the `ZIG_PROGRESS` + /// environment variable inside the child process. The progress reported by + /// the child will be attached to this progress node in the parent process. + /// + /// The child's progress tree will be grafted into the parent's progress tree, + /// by substituting this node with the child's root node. + progress_node: std.Progress.Node = std.Progress.Node.none, + + stdin: StdIo = .inherit, + stdout: StdIo = .inherit, + stderr: StdIo = .inherit, + + /// 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, + + /// Set to change the user id when spawning the child process. + uid: ?posix.uid_t = null, + /// Set to change the group id when spawning the child process. + gid: ?posix.gid_t = null, + /// Set to change the process group id when spawning the child process. + pgid: ?posix.pid_t = null, + + /// Start child process in suspended state. + /// For Posix systems it's started as if SIGSTOP was sent. + start_suspended: bool = false, + /// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess. + create_no_window: bool = false, + /// Darwin-only. Disable ASLR for the child process. + disable_aslr: bool = false, + + /// Behavior of the child process's standard input, output, and error streams. + pub const StdIo = union(enum) { + /// Inherit the corresponding stream from the parent process. + inherit, + /// Pass an already open file from the parent to the child. + file: File, + /// Pass a null stream to the child process by opening "/dev/null" on POSIX + /// and "NUL" on Windows. + ignore, + /// Create a new 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, + /// Spawn the child process with the corresponding stream missing. This + /// will likely result in the child encountering EBADF if it tries to use + /// stdin, stdout, or stderr, or if only one stream is closed, it will + /// result in them getting mixed up. Generally, this option is for advanced + /// use cases only. + close, }; +}; - while (it.next()) |search_path| { - const path_len = search_path.len + file_slice.len + 1; - if (path_buf.len < path_len + 1) return error.NameTooLong; - @memcpy(path_buf[0..search_path.len], search_path); - path_buf[search_path.len] = '/'; - @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice); - path_buf[path_len] = 0; - const full_path = path_buf[0..path_len :0].ptr; - switch (arg0_expand) { - .expand => child_argv[0] = full_path, - .no_expand => {}, - } - err = execveZ(full_path, child_argv, envp); - switch (err) { - error.AccessDenied => seen_eacces = true, - error.FileNotFound, error.NotDir => {}, - else => |e| return e, - } - } - if (seen_eacces) return error.AccessDenied; - return err; +/// Creates a child process. +/// +/// `argv[0]` is the name of the program to execute. If it is not already a +/// file path (i.e. it contains '/'), it is resolved into a file path based on +/// PATH from the parent environment. +pub fn spawn(io: Io, options: SpawnOptions) SpawnError!Child { + return io.vtable.processSpawn(io.userdata, options); } +/// Creates a child process. +/// +/// `argv[0]` is the file path of the program to execute, relative to `dir`. It +/// is *always* treated as a file path, even if it does not contain '/'. +pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child { + return io.vtable.processSpawnPath(io.userdata, dir, options); +} + +pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{ + StdoutStreamTooLong, + StderrStreamTooLong, +}; + +pub const RunOptions = struct { + spawn_options: SpawnOptions, + max_output_bytes: usize = 50 * 1024, +}; + +pub const RunResult = struct { + term: Child.Term, + stdout: []u8, + stderr: []u8, +}; + +/// 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(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult { + var child = try spawn(io, options.spawn_options); + defer child.kill(io); + + var stdout: std.ArrayList(u8) = .empty; + defer stdout.deinit(gpa); + var stderr: std.ArrayList(u8) = .empty; + defer stderr.deinit(gpa); + + try child.collectOutput(gpa, &stdout, &stderr, options.max_output_bytes); + + return .{ + .stdout = try stdout.toOwnedSlice(gpa), + .stderr = try stderr.toOwnedSlice(gpa), + .term = try child.wait(io), + }; +} pub const TotalSystemMemoryError = error{ UnknownTotalSystemMemory, diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index f2e096bca2..6a170116ce 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -5,120 +5,38 @@ const native_os = builtin.os.tag; const std = @import("../std.zig"); const Io = std.Io; -const unicode = std.unicode; -const fs = std.fs; const process = std.process; const File = std.Io.File; -const windows = std.os.windows; -const linux = std.os.linux; -const posix = std.posix; -const mem = std.mem; -const maxInt = std.math.maxInt; const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; -/// Tells whether spawning child processes is supported. -pub const can_spawn = switch (native_os) { - .wasi, .ios, .tvos, .visionos, .watchos => false, - else => true, -}; - pub const Id = switch (native_os) { - .windows => windows.HANDLE, + .windows => std.os.windows.HANDLE, .wasi => void, - else => posix.pid_t, + else => std.posix.pid_t, }; -/// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`. +/// After `wait` or `kill` is called, this becomes `null`. /// 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: Allocator, - +id: ?Id, +thread_handle: if (native_os == .windows) std.os.windows.HANDLE else void = {}, /// The writing end of the child process's standard input pipe. -/// Usage requires `stdin_behavior == StdIo.Pipe`. -/// Available after calling `spawn()`. +/// Usage requires `process.SpawnOptions.StdIo.pipe`. stdin: ?File, - /// The reading end of the child process's standard output pipe. -/// Usage requires `stdout_behavior == StdIo.Pipe`. -/// Available after calling `spawn()`. +/// Usage requires `process.SpawnOptions.StdIo.pipe`. stdout: ?File, - /// The reading end of the child process's standard error pipe. -/// Usage requires `stderr_behavior == StdIo.Pipe`. -/// Available after calling `spawn()`. +/// Usage requires `process.SpawnOptions.StdIo.pipe`. stderr: ?File, - -/// Terminated state of the child process. -/// Available after calling `wait()`. -term: ?(SpawnError!Term), - -argv: []const []const u8, - -parent_environ: process.Environ, -/// `null` means to use `parent_environ` also for the spawned process. -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 process group id when spawning the child process. -pgid: if (native_os == .windows or native_os == .wasi) void else ?posix.pid_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: ?Io.Dir = null, - -err_pipe: if (native_os == .windows) void else ?posix.fd_t, - -expand_arg0: Arg0Expand, - -/// Darwin-only. Disable ASLR for the child process. -disable_aslr: bool = false, - -/// Start child process in suspended state. -/// For Posix systems it's started as if SIGSTOP was sent. -start_suspended: bool = false, - -/// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess. -create_no_window: 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`. +/// TODO move this data into `Term` resource_usage_statistics: ResourceUsageStatistics = .{}, - -/// When populated, a pipe will be created for the child process to -/// communicate progress back to the parent. The file descriptor of the -/// write end of the pipe will be specified in the `ZIG_PROGRESS` -/// environment variable inside the child process. The progress reported by -/// the child will be attached to this progress node in the parent process. -/// -/// The child's progress tree will be grafted into the parent's progress tree, -/// by substituting this node with the child's root node. -progress_node: std.Progress.Node = std.Progress.Node.none, +request_resource_usage_statistics: bool, pub const ResourceUsageStatistics = struct { rusage: @TypeOf(rusage_init) = rusage_init, @@ -168,233 +86,60 @@ pub const ResourceUsageStatistics = struct { .tvos, .visionos, .watchos, - => @as(?posix.rusage, null), - .windows => @as(?windows.VM_COUNTERS, null), + => @as(?std.posix.rusage, null), + .windows => @as(?std.os.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://wtf-8.codeberg.page/ - 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.SetPgidError || - posix.ChangeCurDirError || - windows.CreateProcessError || - windows.GetProcessMemoryInfoError || - windows.WaitForSingleObjectError; - pub const Term = union(enum) { - Exited: u8, - Signal: u32, - Stopped: u32, - Unknown: u32, + exited: u8, + signal: std.posix.SIG, + 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(gpa: Allocator, argv: []const []const u8, environ: Environ) Child { - return .{ - .allocator = gpa, - .argv = argv, - .environ = environ, - .id = undefined, - .thread_handle = undefined, - .err_pipe = if (native_os == .windows) {} else null, - .term = 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, - .pgid = if (native_os == .windows or native_os == .wasi) {} else null, - .stdin = null, - .stdout = null, - .stderr = null, - .stdin_behavior = .Inherit, - .stdout_behavior = .Inherit, - .stderr_behavior = .Inherit, - .expand_arg0 = .no_expand, - }; -} - -pub fn setUserName(self: *Child, 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: *Child, io: Io) SpawnError!void { - if (!process.can_spawn) { - @compileError("the target operating system cannot spawn processes"); - } - - if (native_os == .windows) { - return self.spawnWindows(io); - } else { - return self.spawnPosix(io); - } -} - -pub fn spawnAndWait(child: *Child, io: Io) SpawnError!Term { - try child.spawn(io); - return child.wait(io); -} - -/// Forcibly terminates child process and then cleans up all resources. -pub fn kill(self: *Child, io: Io) !Term { - if (native_os == .windows) { - return self.killWindows(io, 1); - } else { - return self.killPosix(io); - } -} - -pub fn killWindows(self: *Child, io: Io, exit_code: windows.UINT) !Term { - if (self.term) |term| { - self.cleanupStreams(io); - return term; - } - - windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) { - error.AccessDenied => { - // 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(io); - return self.term.?; -} - -pub fn killPosix(self: *Child, io: Io) !Term { - if (self.term) |term| { - self.cleanupStreams(io); - return term; - } - posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { - error.ProcessNotFound => return error.AlreadyTerminated, - else => return err, - }; - self.waitUnwrappedPosix(io); - return self.term.?; -} - -pub const WaitError = SpawnError || std.os.windows.GetProcessMemoryInfoError; - -/// On some targets, `spawn` may not report all spawn errors, such as `error.InvalidExe`. -/// This function will block until any spawn errors can be reported, and return them. -pub fn waitForSpawn(self: *Child) SpawnError!void { - if (native_os == .windows) return; // `spawn` reports everything - if (self.term) |term| { - _ = term catch |spawn_err| return spawn_err; +/// Requests for the operating system to forcibly terminate the child process, +/// then blocks until it terminates, then cleans up all resources. +/// +/// Idempotent and does nothing after `wait` returns. +/// +/// Uncancelable. Ignores unexpected errors from the operating system. +pub fn kill(child: *Child, io: Io) void { + if (child.id != null) { + assert(child.stdin == null); + assert(child.stdout == null); + assert(child.stderr == null); return; } - - const err_pipe = self.err_pipe orelse return; - self.err_pipe = null; - // Wait for the child to report any errors in or before `execvpe`. - const report = readIntFd(err_pipe); - posix.close(err_pipe); - if (report) |child_err_int| { - const child_err: SpawnError = @errorCast(@errorFromInt(child_err_int)); - self.term = child_err; - return child_err; - } else |read_err| switch (read_err) { - error.EndOfStream => { - // Write end closed by CLOEXEC at the time of the `execvpe` call, - // indicating success. - }, - else => { - // Problem reading the error from the error reporting pipe. We - // don't know if the child is alive or dead. Better to assume it is - // alive so the resource does not risk being leaked. - }, - } + io.vtable.childKill(io.userdata, child); + assert(child.id == null); } +pub const WaitError = error{ + AccessDenied, +} || Io.Cancelable || Io.UnexpectedError; + /// Blocks until child process terminates and then cleans up all resources. -pub fn wait(self: *Child, io: Io) WaitError!Term { - try self.waitForSpawn(); // report spawn errors - if (self.term) |term| { - self.cleanupStreams(io); - return term; - } - switch (native_os) { - .windows => try self.waitUnwrappedWindows(io), - else => self.waitUnwrappedPosix(io), - } - self.id = undefined; - return self.term.?; +pub fn wait(child: *Child, io: Io) WaitError!Term { + assert(child.id != null); + return io.vtable.childWait(io.userdata, child); } -pub const RunResult = struct { - term: Term, - stdout: []u8, - stderr: []u8, -}; - /// 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 +/// The process must have been started with stdout and stderr set to +/// `process.SpawnOptions.StdIo.pipe`. pub fn collectOutput( - child: Child, + child: *const Child, /// Used for `stdout` and `stderr`. allocator: Allocator, stdout: *ArrayList(u8), stderr: *ArrayList(u8), max_output_bytes: usize, ) !void { - assert(child.stdout_behavior == .Pipe); - assert(child.stderr_behavior == .Pipe); - var poller = std.Io.poll(allocator, enum { stdout, stderr }, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, @@ -431,1469 +176,3 @@ pub fn collectOutput( return error.StderrStreamTooLong; } } - -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(gpa: Allocator, io: Io, args: struct { - argv: []const []const u8, - environ: Environ, - cwd: ?[]const u8 = null, - cwd_dir: ?Io.Dir = null, - max_output_bytes: usize = 50 * 1024, - expand_arg0: Arg0Expand = .no_expand, - progress_node: std.Progress.Node = std.Progress.Node.none, -}) RunError!RunResult { - var child = Child.init(gpa, args.argv, args.environ); - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; - child.cwd = args.cwd; - child.cwd_dir = args.cwd_dir; - child.expand_arg0 = args.expand_arg0; - child.progress_node = args.progress_node; - - var stdout: ArrayList(u8) = .empty; - defer stdout.deinit(gpa); - var stderr: ArrayList(u8) = .empty; - defer stderr.deinit(gpa); - - try child.spawn(io); - errdefer { - _ = child.kill(io) catch {}; - } - try child.collectOutput(gpa, &stdout, &stderr, args.max_output_bytes); - - return .{ - .stdout = try stdout.toOwnedSlice(gpa), - .stderr = try stderr.toOwnedSlice(gpa), - .term = try child.wait(io), - }; -} - -fn waitUnwrappedWindows(self: *Child, io: Io) WaitError!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(io); - return result; -} - -fn waitUnwrappedPosix(self: *Child, io: Io) void { - const res: posix.WaitPidResult = res: { - if (self.request_resource_usage_statistics) { - switch (native_os) { - .dragonfly, - .freebsd, - .netbsd, - .openbsd, - .illumos, - .linux, - .serenity, - .driverkit, - .ios, - .maccatalyst, - .macos, - .tvos, - .visionos, - .watchos, - => { - 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(io); - self.handleWaitResult(status); -} - -fn handleWaitResult(self: *Child, status: u32) void { - self.term = statusToTerm(status); -} - -fn cleanupStreams(self: *Child, io: Io) void { - if (self.stdin) |*stdin| { - stdin.close(io); - self.stdin = null; - } - if (self.stdout) |*stdout| { - stdout.close(io); - self.stdout = null; - } - if (self.stderr) |*stderr| { - stderr.close(io); - self.stderr = null; - } -} - -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: *Child, io: Io) SpawnError!void { - // The child process does need to access (one end of) these pipes. However, - // we must initially set CLOEXEC to avoid a race condition. If another thread - // is racing to spawn a different child process, we don't want it to inherit - // these FDs in any scenario; that would mean that, for instance, calls to - // `poll` from the parent would not report the child's stdout as closing when - // expected, since the other child may retain a reference to the write end of - // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we - // need to do something in the new child to make sure we preserve the reference - // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it - // turns out, we `dup2` everything anyway, so there's no need! - const pipe_flags: posix.O = .{ .CLOEXEC = true }; - - const stdin_pipe = if (self.stdin_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined; - errdefer if (self.stdin_behavior == .Pipe) { - destroyPipe(stdin_pipe); - }; - - const stdout_pipe = if (self.stdout_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined; - errdefer if (self.stdout_behavior == .Pipe) { - destroyPipe(stdout_pipe); - }; - - const stderr_pipe = if (self.stderr_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined; - errdefer if (self.stderr_behavior == .Pipe) { - destroyPipe(stderr_pipe); - }; - - const any_ignore = (self.stdin_behavior == .Ignore or self.stdout_behavior == .Ignore or self.stderr_behavior == .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.FileLocksUnsupported => unreachable, - error.BadPathName => unreachable, // Windows-only - error.WouldBlock => unreachable, - error.NetworkNotFound => unreachable, // Windows-only - error.Canceled => unreachable, // temporarily in the posix error set - error.SharingViolation => unreachable, // Windows-only - error.PipeBusy => unreachable, // not a pipe - error.AntivirusInterference => unreachable, // Windows-only - else => |e| return e, - } - else - undefined; - defer { - if (any_ignore) posix.close(dev_null_fd); - } - - const prog_pipe: [2]posix.fd_t = p: { - if (self.progress_node.index == .none) { - break :p .{ -1, -1 }; - } else { - // We use CLOEXEC for the same reason as in `pipe_flags`. - break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); - } - }; - errdefer destroyPipe(prog_pipe); - - 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 prog_fileno = 3; - comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno); - - const envp: [*:null]const ?[*:0]const u8 = m: { - const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno; - switch (self.environ) { - .empty => break :m (try process.Environ.createBlock(.{ .block = &.{} }, arena, .{ - .zig_progress_fd = prog_fd, - })).ptr, - .inherit => |b| break :m (try b.createBlock(arena, .{ - .zig_progress_fd = prog_fd, - })).ptr, - .map => |m| break :m (try m.createBlock(arena, .{ - .zig_progress_fd = prog_fd, - })).ptr, - } - }; - - // This pipe communicates to the parent errors in the child between `fork` and `execvpe`. - // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds. - const err_pipe: [2]posix.fd_t = 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(io, err_pipe[1], err); - setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err); - setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err); - - if (self.cwd_dir) |cwd| { - posix.fchdir(cwd.handle) catch |err| forkChildErrReport(io, err_pipe[1], err); - } else if (self.cwd) |cwd| { - posix.chdir(cwd) catch |err| forkChildErrReport(io, err_pipe[1], err); - } - - // Must happen after fchdir above, the cwd file descriptor might be - // equal to prog_fileno and be clobbered by this dup2 call. - if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkChildErrReport(io, err_pipe[1], err); - - if (self.gid) |gid| { - posix.setregid(gid, gid) catch |err| forkChildErrReport(io, err_pipe[1], err); - } - - if (self.uid) |uid| { - posix.setreuid(uid, uid) catch |err| forkChildErrReport(io, err_pipe[1], err); - } - - if (self.pgid) |pid| { - posix.setpgid(0, pid) catch |err| forkChildErrReport(io, err_pipe[1], err); - } - - if (self.start_suspended) { - posix.kill(posix.getpid(), .STOP) catch |err| forkChildErrReport(io, err_pipe[1], err); - } - - const parent_PATH: ?[]const u8 = switch(self.environ) { - .empty => null, - .inherit => - .map => |m| m.get("PATH"), - }; - - const err = switch (self.expand_arg0) { - .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp, parent_PATH), - .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp, parent_PATH), - }; - forkChildErrReport(io, err_pipe[1], err); - } - - // we are the parent - errdefer comptime unreachable; // The child is forked; we must not error from now on - - posix.close(err_pipe[1]); // make sure only the child holds the write end open - self.err_pipe = err_pipe[0]; - - const pid: i32 = @intCast(pid_result); - if (self.stdin_behavior == .Pipe) { - self.stdin = .{ .handle = stdin_pipe[1] }; - } else { - self.stdin = null; - } - if (self.stdout_behavior == .Pipe) { - self.stdout = .{ .handle = stdout_pipe[0] }; - } else { - self.stdout = null; - } - if (self.stderr_behavior == .Pipe) { - self.stderr = .{ .handle = stderr_pipe[0] }; - } else { - self.stderr = null; - } - - self.id = pid; - self.term = null; - - if (self.stdin_behavior == .Pipe) { - posix.close(stdin_pipe[0]); - } - if (self.stdout_behavior == .Pipe) { - posix.close(stdout_pipe[1]); - } - if (self.stderr_behavior == .Pipe) { - posix.close(stderr_pipe[1]); - } - - if (prog_pipe[1] != -1) { - posix.close(prog_pipe[1]); - } - self.progress_node.setIpcFd(prog_pipe[0]); -} - -fn spawnWindows(self: *Child, io: Io) 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 = .{ - .STANDARD = .{ .SYNCHRONIZE = true }, - .GENERIC = .{ .WRITE = true, .READ = true }, - }, - .sa = &saAttr, - .creation = .OPEN, - }) catch |err| switch (err) { - error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" - error.PipeBusy => return error.Unexpected, // not possible for "NUL" - error.NoDevice => 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" - error.OperationCanceled => return error.Unexpected, // we're not canceling the operation - 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 Child 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); - - const flags: windows.CreateProcessFlags = .{ - .create_suspended = self.start_suspended, - .create_unicode_environment = true, - .create_no_window = self.create_no_window, - }; - - 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: ArrayList(u16) = .empty; - defer app_buf.deinit(self.allocator); - - try app_buf.appendSlice(self.allocator, app_name_w); - - var dir_buf: ArrayList(u16) = .empty; - 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); - } - - windowsCreateProcessPathExt(self.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &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 Child 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); - - if (windowsCreateProcessPathExt(self.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &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 { - if (pipe[0] != -1) 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(io: Io, fd: i32, err: Child.SpawnError) noreturn { - writeIntFd(io, 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.system.exit(1); -} - -fn writeIntFd(io: Io, fd: i32, value: ErrInt) !void { - var buffer: [8]u8 = undefined; - var fw: File.Writer = .initStreaming(.{ .handle = fd }, io, &buffer); - fw.interface.writeInt(u64, value, .little) catch unreachable; - fw.interface.flush() catch return error.SystemResources; -} - -fn readIntFd(fd: i32) !ErrInt { - var buffer: [8]u8 = undefined; - var i: usize = 0; - while (i < buffer.len) { - const n = try std.posix.read(fd, buffer[i..]); - if (n == 0) return error.EndOfStream; - i += n; - } - const int = mem.readInt(u64, &buffer, .little); - return @intCast(int); -} - -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: Allocator, - io: Io, - dir_buf: *ArrayList(u16), - app_buf: *ArrayList(u16), - pathext: [:0]const u16, - cmd_line_cache: *WindowsCommandLineCache, - envp_ptr: ?[*]u16, - cwd_ptr: ?[*:0]u16, - flags: windows.CreateProcessFlags, - 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. - - // In the future, child process execution needs to move to Io implementation. - // Under those conditions, here we will have access to lower level directory - // opening function knowing which implementation we are in. Here, we imitate - // that scenario. - 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 Io.Threaded.dirOpenDirWindows(.cwd(), prefixed_path.span(), .{ - .iterate = true, - }) catch return error.FileNotFound; - }; - defer dir.close(io); - - // 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(WindowsExtension).@"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.handle, - null, - null, - null, - &io_status, - &file_information_buf, - file_information_buf.len, - .Directory, - 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.DIRECTORY) 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, flags, 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, flags, 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, - flags: windows.CreateProcessFlags, - 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, - flags, - @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.bufPrintSentinel( - &tmp_buf, - "\\\\.\\pipe\\zig-childprocess-{d}-{d}", - .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, - 0, - ) 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.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, - .{ .GENERIC = .{ .WRITE = true } }, - 0, - &sattr_copy, - windows.OPEN_EXISTING, - @bitCast(windows.FILE.ATTRIBUTE{ .NORMAL = true }), - null, - ); - if (write_handle == windows.INVALID_HANDLE_VALUE) { - switch (windows.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); - -/// File name extensions supported natively by `CreateProcess()` on Windows. -// Should be kept in sync with `windowsCreateProcessSupportsExtension`. -pub const WindowsExtension = enum { - bat, - cmd, - com, - exe, -}; - -/// Case-insensitive WTF-16 lookup -fn windowsCreateProcessSupportsExtension(ext: []const u16) ?WindowsExtension { - 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(WindowsExtension.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: Allocator, - - fn init(allocator: 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.?; - } -}; - -/// Returns the absolute path of `cmd.exe` within the Windows system directory. -/// The caller owns the returned slice. -fn windowsCmdExePath(allocator: Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { - var buf = try ArrayList(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.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. -/// -/// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts. -/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ -/// -/// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead. -fn argvToCommandLineWindows( - allocator: Allocator, - argv: []const []const u8, -) ArgvToCommandLineError![:0]u16 { - var buf = std.array_list.Managed(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/ -/// -/// The return of this function will look like -/// `cmd.exe /d /e:ON /v:OFF /c ""` -/// and should be used as the `lpCommandLine` of `CreateProcessW`, while the -/// return of `windowsCmdExePath` should be used as `lpApplicationName`. -/// -/// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise. -/// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem. -fn argvToScriptCommandLineWindows( - allocator: 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.array_list.Managed(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.findAny(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.findAny(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/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig index f2489f9ee7..444a9c7df9 100644 --- a/lib/std/zig/LibCInstallation.zig +++ b/lib/std/zig/LibCInstallation.zig @@ -268,15 +268,17 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar dev_null, }); - const run_res = std.process.Child.run(gpa, io, .{ - .argv = argv.items, + const run_res = std.process.run(gpa, io, .{ .max_output_bytes = 1024 * 1024, - .env_map = &env_map, - // Some C compilers, such as Clang, are known to rely on argv[0] to find the path - // to their own executable, without even bothering to resolve PATH. This results in the message: - // error: unable to execute command: Executable "" doesn't exist! - // So we use the expandArg0 variant of ChildProcess to give them a helping hand. - .expand_arg0 = .expand, + .spawn_options = .{ + .argv = argv.items, + .env_map = &env_map, + // Some C compilers, such as Clang, are known to rely on argv[0] to find the path + // to their own executable, without even bothering to resolve PATH. This results in the message: + // error: unable to execute command: Executable "" doesn't exist! + // So we use the expandArg0 variant of ChildProcess to give them a helping hand. + .expand_arg0 = .expand, + }, }) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => { @@ -289,7 +291,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar gpa.free(run_res.stderr); } switch (run_res.term) { - .Exited => |code| if (code != 0) { + .exited => |code| if (code != 0) { printVerboseInvocation(argv.items, null, args.verbose, run_res.stderr); return error.CCompilerExitCode; }, @@ -585,15 +587,17 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8 try appendCcExe(&argv, skip_cc_env_var); try argv.append(arg1); - const run_res = std.process.Child.run(gpa, io, .{ - .argv = argv.items, + const run_res = std.process.run(gpa, io, .{ .max_output_bytes = 1024 * 1024, - .env_map = &env_map, - // Some C compilers, such as Clang, are known to rely on argv[0] to find the path - // to their own executable, without even bothering to resolve PATH. This results in the message: - // error: unable to execute command: Executable "" doesn't exist! - // So we use the expandArg0 variant of ChildProcess to give them a helping hand. - .expand_arg0 = .expand, + .spawn_options = .{ + .argv = argv.items, + .env_map = &env_map, + // Some C compilers, such as Clang, are known to rely on argv[0] to find the path + // to their own executable, without even bothering to resolve PATH. This results in the message: + // error: unable to execute command: Executable "" doesn't exist! + // So we use the expandArg0 variant of ChildProcess to give them a helping hand. + .expand_arg0 = .expand, + }, }) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.UnableToSpawnCCompiler, @@ -603,7 +607,7 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8 gpa.free(run_res.stderr); } switch (run_res.term) { - .Exited => |code| if (code != 0) { + .exited => |code| if (code != 0) { printVerboseInvocation(argv.items, args.search_basename, args.verbose, run_res.stderr); return error.CCompilerExitCode; }, diff --git a/lib/std/zig/system/darwin.zig b/lib/std/zig/system/darwin.zig index e69de48d26..df07c68cba 100644 --- a/lib/std/zig/system/darwin.zig +++ b/lib/std/zig/system/darwin.zig @@ -17,15 +17,15 @@ pub const macos = @import("darwin/macos.zig"); /// /// If error.OutOfMemory occurs in Allocator, this function returns null. pub fn isSdkInstalled(gpa: Allocator, io: Io) bool { - const result = std.process.Child.run(gpa, io, .{ + const result = std.process.run(gpa, io, .{ .spawn_options = .{ .argv = &.{ "xcode-select", "--print-path" }, - }) catch return false; + } }) catch return false; defer { gpa.free(result.stderr); gpa.free(result.stdout); } return switch (result.term) { - .Exited => |code| if (code == 0) result.stdout.len > 0 else false, + .exited => |code| if (code == 0) result.stdout.len > 0 else false, else => false, }; } @@ -35,7 +35,7 @@ pub fn isSdkInstalled(gpa: Allocator, io: Io) bool { /// Caller owns the memory. /// stderr from xcrun is ignored. /// If error.OutOfMemory occurs in Allocator, this function returns null. -pub fn getSdk(gpa: Allocator, io: Io, environ: std.process.Child.Environ, target: *const Target) ?[]const u8 { +pub fn getSdk(gpa: Allocator, io: Io, target: *const Target) ?[]const u8 { const is_simulator_abi = target.abi == .simulator; const sdk = switch (target.os.tag) { .driverkit => "driverkit", @@ -47,16 +47,13 @@ pub fn getSdk(gpa: Allocator, io: Io, environ: std.process.Child.Environ, target else => return null, }; const argv = &[_][]const u8{ "xcrun", "--sdk", sdk, "--show-sdk-path" }; - const result = std.process.Child.run(gpa, io, .{ - .argv = argv, - .environ = environ, - }) catch return null; + const result = std.process.run(gpa, io, .{ .spawn_options = .{ .argv = argv } }) catch return null; defer { gpa.free(result.stderr); gpa.free(result.stdout); } switch (result.term) { - .Exited => |code| if (code != 0) return null, + .exited => |code| if (code != 0) return null, else => return null, } return gpa.dupe(u8, mem.trimEnd(u8, result.stdout, "\r\n")) catch null; diff --git a/src/Compilation.zig b/src/Compilation.zig index a11519d07a..b3751b5582 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -6339,15 +6339,15 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr if (std.process.can_spawn) { var child = std.process.Child.init(argv.items, arena); if (comp.clang_passthrough_mode) { - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .inherit; + child.stdout_behavior = .inherit; + child.stderr_behavior = .inherit; const term = child.spawnAndWait(io) catch |err| { return comp.failCObj(c_object, "failed to spawn zig clang (passthrough mode) {s}: {s}", .{ argv.items[0], @errorName(err) }); }; switch (term) { - .Exited => |code| { + .exited => |code| { if (code != 0) { std.process.exit(code); } @@ -6357,9 +6357,9 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr else => std.process.abort(), } } else { - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Ignore; - child.stderr_behavior = .Pipe; + child.stdin_behavior = .ignore; + child.stdout_behavior = .ignore; + child.stderr_behavior = .pipe; try child.spawn(io); @@ -6371,7 +6371,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr }; switch (term) { - .Exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| { + .exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| { const bundle = CObject.Diag.Bundle.parse(gpa, io, diag_file_path) catch |err| { log.err("{}: failed to parse clang diagnostics: {s}", .{ err, stderr }); return comp.failCObj(c_object, "clang exited with code {d}", .{code}); @@ -6742,9 +6742,9 @@ fn spawnZigRc( defer node_name.deinit(arena); var child = std.process.Child.init(argv, arena); - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; + child.stdin_behavior = .ignore; + child.stdout_behavior = .pipe; + child.stderr_behavior = .pipe; child.progress_node = child_progress_node; child.spawn(io) catch |err| { @@ -6785,12 +6785,16 @@ fn spawnZigRc( }; switch (term) { - .Exited => |code| { + .exited => |code| { if (code != 0) { log.err("zig rc failed with stderr:\n{s}", .{stderr.buffered()}); return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code}); } }, + .signal => |sig| { + log.err("zig rc signaled {t} with stderr:\n{s}", .{ sig, stderr.buffered() }); + return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); + }, else => { log.err("zig rc terminated with stderr:\n{s}", .{stderr.buffered()}); return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); diff --git a/src/link/Lld.zig b/src/link/Lld.zig index b2a0f6e396..d1d2ebf07a 100644 --- a/src/link/Lld.zig +++ b/src/link/Lld.zig @@ -1606,15 +1606,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi var child = std.process.Child.init(argv, arena); const term = (if (comp.clang_passthrough_mode) term: { - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .inherit; + child.stdout_behavior = .inherit; + child.stderr_behavior = .inherit; break :term child.spawnAndWait(io); } else term: { - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Ignore; - child.stderr_behavior = .Pipe; + child.stdin_behavior = .ignore; + child.stdout_behavior = .ignore; + child.stderr_behavior = .pipe; child.spawn(io) catch |err| break :term err; var stderr_reader = child.stderr.?.readerStreaming(io, &.{}); @@ -1656,15 +1656,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi .{try comp.dirs.local_cache.join(arena, &.{rsp_path})}, ) }, arena); if (comp.clang_passthrough_mode) { - rsp_child.stdin_behavior = .Inherit; - rsp_child.stdout_behavior = .Inherit; - rsp_child.stderr_behavior = .Inherit; + rsp_child.stdin_behavior = .inherit; + rsp_child.stdout_behavior = .inherit; + rsp_child.stderr_behavior = .inherit; break :term rsp_child.spawnAndWait(io) catch |err| break :err err; } else { - rsp_child.stdin_behavior = .Ignore; - rsp_child.stdout_behavior = .Ignore; - rsp_child.stderr_behavior = .Pipe; + rsp_child.stdin_behavior = .ignore; + rsp_child.stdout_behavior = .ignore; + rsp_child.stderr_behavior = .pipe; rsp_child.spawn(io) catch |err| break :err err; var stderr_reader = rsp_child.stderr.?.readerStreaming(io, &.{}); @@ -1680,7 +1680,7 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi const diags = &comp.link_diags; switch (term) { - .Exited => |code| if (code != 0) { + .exited => |code| if (code != 0) { if (comp.clang_passthrough_mode) std.process.exit(code); diags.lockAndParseLldStderr(argv[1], stderr); return error.LinkFailure; diff --git a/src/main.zig b/src/main.zig index f71cf5d144..25d14f0d7a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4442,9 +4442,9 @@ fn runOrTest( } else if (process.can_spawn) { var child = std.process.Child.init(argv.items, gpa); child.env_map = &env_map; - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .inherit; + child.stdout_behavior = .inherit; + child.stderr_behavior = .inherit; // Here we release all the locks associated with the Compilation so // that whatever this child process wants to do won't deadlock. @@ -4587,9 +4587,9 @@ fn runOrTestHotSwap( else => { var child = std.process.Child.init(argv.items, gpa); - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .inherit; + child.stdout_behavior = .inherit; + child.stderr_behavior = .inherit; try child.spawn(io); @@ -5417,9 +5417,9 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) if (process.can_spawn) { var child = std.process.Child.init(child_argv.items, gpa); - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .inherit; + child.stdout_behavior = .inherit; + child.stderr_behavior = .inherit; const term = t: { _ = try io.lockStderr(&.{}, .no_color); @@ -5686,9 +5686,9 @@ fn jitCmd( } 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; + child.stdin_behavior = .inherit; + child.stdout_behavior = if (options.capture == null) .inherit else .pipe; + child.stderr_behavior = .inherit; const term = t: { _ = try io.lockStderr(&.{}, .no_color); diff --git a/test/link/macho.zig b/test/link/macho.zig index 844273b8e5..ccfecefa44 100644 --- a/test/link/macho.zig +++ b/test/link/macho.zig @@ -871,7 +871,7 @@ fn testLinkDirectlyCppTbd(b: *Build, opts: Options) *Step { const io = b.graph.io; const test_step = addTestStep(b, "link-directly-cpp-tbd", opts); - const sdk = std.zig.system.darwin.getSdk(b.allocator, io, .{ .map = &b.graph.env_map }, &opts.target.result) orelse + const sdk = std.zig.system.darwin.getSdk(b.allocator, io, &opts.target.result) orelse @panic("macOS SDK is required to run the test"); const exe = addExecutable(b, opts, .{ diff --git a/test/src/Debugger.zig b/test/src/Debugger.zig index d975c8fbb5..0d389f2508 100644 --- a/test/src/Debugger.zig +++ b/test/src/Debugger.zig @@ -2306,7 +2306,7 @@ fn addTest( run.addArgs(db_argv2); run.addArtifactArg(exe); for (expected_output) |expected| run.addCheck(.{ .expect_stdout_match = db.b.fmt("{s}\n", .{expected}) }); - run.addCheck(.{ .expect_term = .{ .Exited = success } }); + run.addCheck(.{ .expect_term = .{ .exited = success } }); run.setStdIn(.{ .bytes = "" }); db.root_step.dependOn(&run.step); } diff --git a/test/src/StackTrace.zig b/test/src/StackTrace.zig index 57e29dd800..4e5c946682 100644 --- a/test/src/StackTrace.zig +++ b/test/src/StackTrace.zig @@ -193,9 +193,9 @@ fn addCaseInstance( run.removeEnvironmentVariable("CLICOLOR_FORCE"); run.setEnvironmentVariable("NO_COLOR", "1"); run.addCheck(.{ .expect_term = term: { - if (!expect_panic) break :term .{ .Exited = 0 }; - if (target.result.os.tag == .windows) break :term .{ .Exited = 3 }; - break :term .{ .Signal = 6 }; + if (!expect_panic) break :term .{ .exited = 0 }; + if (target.result.os.tag == .windows) break :term .{ .exited = 3 }; + break :term .{ .signal = @enumFromInt(6) }; } }); run.expectStdOutEqual(""); diff --git a/test/standalone/child_process/main.zig b/test/standalone/child_process/main.zig index 98d38bdee3..0bd96061b4 100644 --- a/test/standalone/child_process/main.zig +++ b/test/standalone/child_process/main.zig @@ -26,9 +26,9 @@ pub fn main() !void { const io = threaded.io(); var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa); - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Inherit; + child.stdin_behavior = .pipe; + child.stdout_behavior = .pipe; + child.stderr_behavior = .inherit; try child.spawn(io); const child_stdin = child.stdin.?; try child_stdin.writeStreamingAll(io, "hello from stdin"); // verified in child diff --git a/test/standalone/ios/build.zig b/test/standalone/ios/build.zig index d9bd93875b..b87d55993b 100644 --- a/test/standalone/ios/build.zig +++ b/test/standalone/ios/build.zig @@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void { const io = b.graph.io; - if (std.zig.system.darwin.getSdk(b.allocator, io, .{ .map = &b.graph.env_map }, &target.result)) |sdk| { + if (std.zig.system.darwin.getSdk(b.allocator, io, &target.result)) |sdk| { b.sysroot = sdk; exe.root_module.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/include" }) }); exe.root_module.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/System/Library/Frameworks" }) }); diff --git a/test/standalone/simple/hello_world/hello.zig b/test/standalone/simple/hello_world/hello.zig index a031d6c6f0..27cedd428e 100644 --- a/test/standalone/simple/hello_world/hello.zig +++ b/test/standalone/simple/hello_world/hello.zig @@ -1,15 +1,5 @@ const std = @import("std"); -// See https://github.com/ziglang/zig/issues/24510 -// for the plan to simplify this code. -pub fn main() !void { - var debug_allocator: std.heap.DebugAllocator(.{}) = .init; - defer _ = debug_allocator.deinit(); - const gpa = debug_allocator.allocator(); - - var threaded: std.Io.Threaded = .init(gpa, .{}); - defer threaded.deinit(); - const io = threaded.io(); - - try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); +pub fn main(init: std.process.Init) !void { + try std.Io.File.stdout().writeStreamingAll(init.io, "Hello, World!\n"); } diff --git a/test/tests.zig b/test/tests.zig index aa3c018a62..1634112dae 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -2718,7 +2718,7 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons if (b.enable_wasmtime) run.addArg("-fwasmtime"); if (b.enable_darling) run.addArg("-fdarling"); - run.addCheck(.{ .expect_term = .{ .Exited = 0 } }); + run.addCheck(.{ .expect_term = .{ .exited = 0 } }); test_step.dependOn(&run.step); }