| author | |
| committer | |
| log | 32af0f6154edb1c1434d0bb489d2abfea7da1685 |
| tree | 2afb3763f4f8ba8facd35ebe47525810e69f93a3 |
| parent | d6a1e73142396732be80a7e0757cca1c07551d30 |
this gets the build runner compiling again on linux
this work is incomplete; it only moves code around so that environment
variables can be wrangled properly. a future commit will need to audit
the cancelation and error handling of this moved logic.30 files changed, 2170 insertions(+), 2223 deletions(-)
build.zig+1-1| ... | @@ -261,7 +261,7 @@ pub fn build(b: *std.Build) !void { | ... | @@ -261,7 +261,7 @@ pub fn build(b: *std.Build) !void { |
| 261 | "--git-dir", ".git", // affected by the -C argument | 261 | "--git-dir", ".git", // affected by the -C argument |
| 262 | "describe", "--match", "*.*.*", // | 262 | "describe", "--match", "*.*.*", // |
| 263 | "--tags", "--abbrev=9", | 263 | "--tags", "--abbrev=9", |
| 264 | }, &code, .Ignore) catch { | 264 | }, &code, .ignore) catch { |
| 265 | break :v version_string; | 265 | break :v version_string; |
| 266 | }; | 266 | }; |
| 267 | const git_describe = mem.trim(u8, git_describe_untrimmed, " \n\r"); | 267 | const git_describe = mem.trim(u8, git_describe_untrimmed, " \n\r"); |
lib/compiler/aro/aro/Driver.zig+6-6| ... | @@ -1256,9 +1256,9 @@ fn invokeAssembler(d: *Driver, tc: *Toolchain, input_path: []const u8, output_pa | ... | @@ -1256,9 +1256,9 @@ fn invokeAssembler(d: *Driver, tc: *Toolchain, input_path: []const u8, output_pa |
| 1256 | 1256 | ||
| 1257 | var child = std.process.Child.init(&argv, d.comp.gpa); | 1257 | var child = std.process.Child.init(&argv, d.comp.gpa); |
| 1258 | // TODO handle better | 1258 | // TODO handle better |
| 1259 | child.stdin_behavior = .Inherit; | 1259 | child.stdin_behavior = .inherit; |
| 1260 | child.stdout_behavior = .Inherit; | 1260 | child.stdout_behavior = .inherit; |
| 1261 | child.stderr_behavior = .Inherit; | 1261 | child.stderr_behavior = .inherit; |
| 1262 | 1262 | ||
| 1263 | const term = child.spawnAndWait() catch |er| { | 1263 | const term = child.spawnAndWait() catch |er| { |
| 1264 | return d.fatal("unable to spawn linker: {s}", .{errorDescription(er)}); | 1264 | 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 | ... | @@ -1508,9 +1508,9 @@ pub fn invokeLinker(d: *Driver, tc: *Toolchain, comptime fast_exit: bool) Compil |
| 1508 | } | 1508 | } |
| 1509 | var child = std.process.Child.init(argv.items, d.comp.gpa); | 1509 | var child = std.process.Child.init(argv.items, d.comp.gpa); |
| 1510 | // TODO handle better | 1510 | // TODO handle better |
| 1511 | child.stdin_behavior = .Inherit; | 1511 | child.stdin_behavior = .inherit; |
| 1512 | child.stdout_behavior = .Inherit; | 1512 | child.stdout_behavior = .inherit; |
| 1513 | child.stderr_behavior = .Inherit; | 1513 | child.stderr_behavior = .inherit; |
| 1514 | 1514 | ||
| 1515 | const term = child.spawnAndWait() catch |er| { | 1515 | const term = child.spawnAndWait() catch |er| { |
| 1516 | return d.fatal("unable to spawn linker: {s}", .{errorDescription(er)}); | 1516 | return d.fatal("unable to spawn linker: {s}", .{errorDescription(er)}); |
lib/compiler/reduce.zig+1-1| ... | @@ -306,7 +306,7 @@ fn termToInteresting(term: std.process.Child.Term) Interestingness { | ... | @@ -306,7 +306,7 @@ fn termToInteresting(term: std.process.Child.Term) Interestingness { |
| 306 | } | 306 | } |
| 307 | 307 | ||
| 308 | fn runCheck(arena: Allocator, io: Io, argv: []const []const u8) !Interestingness { | 308 | fn runCheck(arena: Allocator, io: Io, argv: []const []const u8) !Interestingness { |
| 309 | const result = try std.process.Child.run(arena, io, .{ .argv = argv }); | 309 | const result = try std.process.run(arena, io, .{ .spawn_options = .{ .argv = argv } }); |
| 310 | if (result.stderr.len != 0) | 310 | if (result.stderr.len != 0) |
| 311 | std.debug.print("{s}", .{result.stderr}); | 311 | std.debug.print("{s}", .{result.stderr}); |
| 312 | return termToInteresting(result.term); | 312 | return termToInteresting(result.term); |
lib/compiler/std-docs.zig+3-3| ... | @@ -447,9 +447,9 @@ fn openBrowserTabThread(gpa: Allocator, io: Io, url: []const u8) !void { | ... | @@ -447,9 +447,9 @@ fn openBrowserTabThread(gpa: Allocator, io: Io, url: []const u8) !void { |
| 447 | else => "xdg-open", | 447 | else => "xdg-open", |
| 448 | }; | 448 | }; |
| 449 | var child = std.process.Child.init(&.{ main_exe, url }, gpa); | 449 | var child = std.process.Child.init(&.{ main_exe, url }, gpa); |
| 450 | child.stdin_behavior = .Ignore; | 450 | child.stdin_behavior = .ignore; |
| 451 | child.stdout_behavior = .Ignore; | 451 | child.stdout_behavior = .ignore; |
| 452 | child.stderr_behavior = .Ignore; | 452 | child.stderr_behavior = .ignore; |
| 453 | try child.spawn(io); | 453 | try child.spawn(io); |
| 454 | _ = try child.wait(io); | 454 | _ = try child.wait(io); |
| 455 | } | 455 | } |
lib/std/Build.zig+21-14| ... | @@ -190,7 +190,7 @@ pub const RunError = error{ | ... | @@ -190,7 +190,7 @@ pub const RunError = error{ |
| 190 | ExitCodeFailure, | 190 | ExitCodeFailure, |
| 191 | ProcessTerminated, | 191 | ProcessTerminated, |
| 192 | ExecNotSupported, | 192 | ExecNotSupported, |
| 193 | } || std.process.Child.SpawnError; | 193 | } || std.process.SpawnError; |
| 194 | 194 | ||
| 195 | pub const PkgConfigError = error{ | 195 | pub const PkgConfigError = error{ |
| 196 | PkgConfigCrashed, | 196 | PkgConfigCrashed, |
| ... | @@ -1755,7 +1755,7 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 { | ... | @@ -1755,7 +1755,7 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 { |
| 1755 | } | 1755 | } |
| 1756 | 1756 | ||
| 1757 | fn supportedWindowsProgramExtension(ext: []const u8) bool { | 1757 | fn supportedWindowsProgramExtension(ext: []const u8) bool { |
| 1758 | inline for (@typeInfo(std.process.Child.WindowsExtension).@"enum".fields) |field| { | 1758 | inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| { |
| 1759 | if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true; | 1759 | if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true; |
| 1760 | } | 1760 | } |
| 1761 | return false; | 1761 | return false; |
| ... | @@ -1830,23 +1830,26 @@ pub fn runAllowFail( | ... | @@ -1830,23 +1830,26 @@ pub fn runAllowFail( |
| 1830 | b: *Build, | 1830 | b: *Build, |
| 1831 | argv: []const []const u8, | 1831 | argv: []const []const u8, |
| 1832 | out_code: *u8, | 1832 | out_code: *u8, |
| 1833 | stderr_behavior: std.process.Child.StdIo, | 1833 | stderr_behavior: std.process.SpawnOptions.StdIo, |
| 1834 | ) RunError![]u8 { | 1834 | ) RunError![]u8 { |
| 1835 | assert(argv.len != 0); | 1835 | assert(argv.len != 0); |
| 1836 | 1836 | ||
| 1837 | if (!process.can_spawn) | 1837 | if (!process.can_spawn) |
| 1838 | return error.ExecNotSupported; | 1838 | return error.ExecNotSupported; |
| 1839 | 1839 | ||
| 1840 | const io = b.graph.io; | 1840 | const graph = b.graph; |
| 1841 | const io = graph.io; | ||
| 1841 | 1842 | ||
| 1842 | const max_output_size = 400 * 1024; | 1843 | const max_output_size = 400 * 1024; |
| 1843 | var child = std.process.Child.init(b.allocator, argv, .{ .map = &b.graph.env_map }); | 1844 | try Step.handleVerbose2(b, null, &graph.env_map, argv); |
| 1844 | child.stdin_behavior = .Ignore; | 1845 | |
| 1845 | child.stdout_behavior = .Pipe; | 1846 | var child = try std.process.spawn(io, .{ |
| 1846 | child.stderr_behavior = stderr_behavior; | 1847 | .argv = argv, |
| 1847 | 1848 | .env_map = &graph.env_map, | |
| 1848 | try Step.handleVerbose2(b, null, child.environ.map, argv); | 1849 | .stdin = .ignore, |
| 1849 | try child.spawn(io); | 1850 | .stdout = .pipe, |
| 1851 | .stderr = stderr_behavior, | ||
| 1852 | }); | ||
| 1850 | 1853 | ||
| 1851 | var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); | 1854 | var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); |
| 1852 | const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch { | 1855 | const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch { |
| ... | @@ -1856,14 +1859,18 @@ pub fn runAllowFail( | ... | @@ -1856,14 +1859,18 @@ pub fn runAllowFail( |
| 1856 | 1859 | ||
| 1857 | const term = try child.wait(io); | 1860 | const term = try child.wait(io); |
| 1858 | switch (term) { | 1861 | switch (term) { |
| 1859 | .Exited => |code| { | 1862 | .exited => |code| { |
| 1860 | if (code != 0) { | 1863 | if (code != 0) { |
| 1861 | out_code.* = @as(u8, @truncate(code)); | 1864 | out_code.* = @as(u8, @truncate(code)); |
| 1862 | return error.ExitCodeFailure; | 1865 | return error.ExitCodeFailure; |
| 1863 | } | 1866 | } |
| 1864 | return stdout; | 1867 | return stdout; |
| 1865 | }, | 1868 | }, |
| 1866 | .Signal, .Stopped, .Unknown => |code| { | 1869 | .signal => |sig| { |
| 1870 | out_code.* = @as(u8, @truncate(@intFromEnum(sig))); | ||
| 1871 | return error.ProcessTerminated; | ||
| 1872 | }, | ||
| 1873 | .stopped, .unknown => |code| { | ||
| 1867 | out_code.* = @as(u8, @truncate(code)); | 1874 | out_code.* = @as(u8, @truncate(code)); |
| 1868 | return error.ProcessTerminated; | 1875 | return error.ProcessTerminated; |
| 1869 | }, | 1876 | }, |
| ... | @@ -1882,7 +1889,7 @@ pub fn run(b: *Build, argv: []const []const u8) []u8 { | ... | @@ -1882,7 +1889,7 @@ pub fn run(b: *Build, argv: []const []const u8) []u8 { |
| 1882 | } | 1889 | } |
| 1883 | 1890 | ||
| 1884 | var code: u8 = undefined; | 1891 | var code: u8 = undefined; |
| 1885 | return b.runAllowFail(argv, &code, .Inherit) catch |err| { | 1892 | return b.runAllowFail(argv, &code, .inherit) catch |err| { |
| 1886 | const printed_cmd = Step.allocPrintCmd(b.allocator, null, null, argv) catch @panic("OOM"); | 1893 | const printed_cmd = Step.allocPrintCmd(b.allocator, null, null, argv) catch @panic("OOM"); |
| 1887 | std.debug.print("unable to spawn the following command: {t}\n{s}\n", .{ err, printed_cmd }); | 1894 | std.debug.print("unable to spawn the following command: {t}\n{s}\n", .{ err, printed_cmd }); |
| 1888 | process.exit(1); | 1895 | process.exit(1); |
lib/std/Build/Step.zig+21-17| ... | @@ -348,7 +348,7 @@ pub fn captureChildProcess( | ... | @@ -348,7 +348,7 @@ pub fn captureChildProcess( |
| 348 | gpa: Allocator, | 348 | gpa: Allocator, |
| 349 | progress_node: std.Progress.Node, | 349 | progress_node: std.Progress.Node, |
| 350 | argv: []const []const u8, | 350 | argv: []const []const u8, |
| 351 | ) !std.process.Child.RunResult { | 351 | ) !std.process.RunResult { |
| 352 | const graph = s.owner.graph; | 352 | const graph = s.owner.graph; |
| 353 | const arena = graph.arena; | 353 | const arena = graph.arena; |
| 354 | const io = graph.io; | 354 | const io = graph.io; |
| ... | @@ -360,11 +360,11 @@ pub fn captureChildProcess( | ... | @@ -360,11 +360,11 @@ pub fn captureChildProcess( |
| 360 | try handleChildProcUnsupported(s); | 360 | try handleChildProcUnsupported(s); |
| 361 | try handleVerbose(s.owner, null, argv); | 361 | try handleVerbose(s.owner, null, argv); |
| 362 | 362 | ||
| 363 | const result = std.process.Child.run(arena, io, .{ | 363 | const result = std.process.run(arena, io, .{ .spawn_options = .{ |
| 364 | .argv = argv, | 364 | .argv = argv, |
| 365 | .environ = .{ .map = &graph.env_map }, | 365 | .env_map = &graph.env_map, |
| 366 | .progress_node = progress_node, | 366 | .progress_node = progress_node, |
| 367 | }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err }); | 367 | } }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err }); |
| 368 | 368 | ||
| 369 | if (result.stderr.len > 0) { | 369 | if (result.stderr.len > 0) { |
| 370 | try s.result_error_msgs.append(arena, result.stderr); | 370 | try s.result_error_msgs.append(arena, result.stderr); |
| ... | @@ -444,19 +444,20 @@ pub fn evalZigProcess( | ... | @@ -444,19 +444,20 @@ pub fn evalZigProcess( |
| 444 | return result; | 444 | return result; |
| 445 | } | 445 | } |
| 446 | assert(argv.len != 0); | 446 | assert(argv.len != 0); |
| 447 | const arena = b.allocator; | ||
| 448 | 447 | ||
| 449 | try handleChildProcUnsupported(s); | 448 | try handleChildProcUnsupported(s); |
| 450 | try handleVerbose(s.owner, null, argv); | 449 | try handleVerbose(s.owner, null, argv); |
| 451 | 450 | ||
| 452 | var child = std.process.Child.init(arena, argv, .{ .map = &b.graph.env_map }); | 451 | var child = std.process.spawn(io, .{ |
| 453 | child.stdin_behavior = .Pipe; | 452 | .argv = argv, |
| 454 | child.stdout_behavior = .Pipe; | 453 | .env_map = &b.graph.env_map, |
| 455 | child.stderr_behavior = .Pipe; | 454 | .stdin = .pipe, |
| 456 | child.request_resource_usage_statistics = true; | 455 | .stdout = .pipe, |
| 457 | child.progress_node = prog_node; | 456 | .stderr = .pipe, |
| 458 | 457 | .request_resource_usage_statistics = true, | |
| 459 | child.spawn(io) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); | 458 | .progress_node = prog_node, |
| 459 | }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); | ||
| 460 | defer if (!watch) child.kill(io); | ||
| 460 | 461 | ||
| 461 | const zp = try gpa.create(ZigProcess); | 462 | const zp = try gpa.create(ZigProcess); |
| 462 | zp.* = .{ | 463 | zp.* = .{ |
| ... | @@ -465,7 +466,7 @@ pub fn evalZigProcess( | ... | @@ -465,7 +466,7 @@ pub fn evalZigProcess( |
| 465 | .stdout = child.stdout.?, | 466 | .stdout = child.stdout.?, |
| 466 | .stderr = child.stderr.?, | 467 | .stderr = child.stderr.?, |
| 467 | }), | 468 | }), |
| 468 | .progress_ipc_fd = if (std.Progress.have_ipc) child.progress_node.getIpcFd() else {}, | 469 | .progress_ipc_fd = if (std.Progress.have_ipc) prog_node.getIpcFd() else {}, |
| 469 | }; | 470 | }; |
| 470 | if (watch) s.setZigProcess(zp); | 471 | if (watch) s.setZigProcess(zp); |
| 471 | defer if (!watch) { | 472 | defer if (!watch) { |
| ... | @@ -487,7 +488,7 @@ pub fn evalZigProcess( | ... | @@ -487,7 +488,7 @@ pub fn evalZigProcess( |
| 487 | 488 | ||
| 488 | // Special handling for Compile step that is expecting compile errors. | 489 | // Special handling for Compile step that is expecting compile errors. |
| 489 | if (s.cast(Compile)) |compile| switch (term) { | 490 | if (s.cast(Compile)) |compile| switch (term) { |
| 490 | .Exited => { | 491 | .exited => { |
| 491 | // Note that the exit code may be 0 in this case due to the | 492 | // Note that the exit code may be 0 in this case due to the |
| 492 | // compiler server protocol. | 493 | // compiler server protocol. |
| 493 | if (compile.expect_errors != null) { | 494 | if (compile.expect_errors != null) { |
| ... | @@ -719,12 +720,15 @@ pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFaile | ... | @@ -719,12 +720,15 @@ pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFaile |
| 719 | pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void { | 720 | pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void { |
| 720 | assert(s.result_failed_command != null); | 721 | assert(s.result_failed_command != null); |
| 721 | switch (term) { | 722 | switch (term) { |
| 722 | .Exited => |code| { | 723 | .exited => |code| { |
| 723 | if (code != 0) { | 724 | if (code != 0) { |
| 724 | return s.fail("process exited with error code {d}", .{code}); | 725 | return s.fail("process exited with error code {d}", .{code}); |
| 725 | } | 726 | } |
| 726 | }, | 727 | }, |
| 727 | .Signal, .Stopped, .Unknown => { | 728 | .signal => |sig| { |
| 729 | return s.fail("process terminated with signal {t}", .{sig}); | ||
| 730 | }, | ||
| 731 | .stopped, .unknown => { | ||
| 728 | return s.fail("process terminated unexpectedly", .{}); | 732 | return s.fail("process terminated unexpectedly", .{}); |
| 729 | }, | 733 | }, |
| 730 | } | 734 | } |
lib/std/Build/Step/Compile.zig+2-2| ... | @@ -747,7 +747,7 @@ fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult { | ... | @@ -747,7 +747,7 @@ fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult { |
| 747 | pkg_name, | 747 | pkg_name, |
| 748 | "--cflags", | 748 | "--cflags", |
| 749 | "--libs", | 749 | "--libs", |
| 750 | }, &code, .Ignore)) |stdout| stdout else |err| switch (err) { | 750 | }, &code, .ignore)) |stdout| stdout else |err| switch (err) { |
| 751 | error.ProcessTerminated => return error.PkgConfigCrashed, | 751 | error.ProcessTerminated => return error.PkgConfigCrashed, |
| 752 | error.ExecNotSupported => return error.PkgConfigFailed, | 752 | error.ExecNotSupported => return error.PkgConfigFailed, |
| 753 | error.ExitCodeFailure => return error.PkgConfigFailed, | 753 | error.ExitCodeFailure => return error.PkgConfigFailed, |
| ... | @@ -1847,7 +1847,7 @@ pub fn doAtomicSymLinks( | ... | @@ -1847,7 +1847,7 @@ pub fn doAtomicSymLinks( |
| 1847 | 1847 | ||
| 1848 | fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg { | 1848 | fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg { |
| 1849 | const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config"; | 1849 | const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config"; |
| 1850 | const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore); | 1850 | const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore); |
| 1851 | var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator); | 1851 | var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator); |
| 1852 | errdefer list.deinit(); | 1852 | errdefer list.deinit(); |
| 1853 | var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); | 1853 | var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); |
lib/std/Build/Step/Fmt.zig+1-1| ... | @@ -69,7 +69,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { | ... | @@ -69,7 +69,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { |
| 69 | 69 | ||
| 70 | const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items); | 70 | const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items); |
| 71 | if (fmt.check) switch (run_result.term) { | 71 | if (fmt.check) switch (run_result.term) { |
| 72 | .Exited => |code| if (code != 0 and run_result.stdout.len != 0) { | 72 | .exited => |code| if (code != 0 and run_result.stdout.len != 0) { |
| 73 | var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n'); | 73 | var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n'); |
| 74 | while (it.next()) |bad_file_name| { | 74 | while (it.next()) |bad_file_name| { |
| 75 | try step.addError("{s}: non-conforming formatting", .{bad_file_name}); | 75 | try step.addError("{s}: non-conforming formatting", .{bad_file_name}); |
lib/std/Build/Step/Run.zig+67-73| ... | @@ -149,7 +149,7 @@ pub const StdIo = union(enum) { | ... | @@ -149,7 +149,7 @@ pub const StdIo = union(enum) { |
| 149 | expect_stderr_match: []const u8, | 149 | expect_stderr_match: []const u8, |
| 150 | expect_stdout_exact: []const u8, | 150 | expect_stdout_exact: []const u8, |
| 151 | expect_stdout_match: []const u8, | 151 | expect_stdout_match: []const u8, |
| 152 | expect_term: std.process.Child.Term, | 152 | expect_term: process.Child.Term, |
| 153 | }; | 153 | }; |
| 154 | }; | 154 | }; |
| 155 | 155 | ||
| ... | @@ -618,7 +618,7 @@ pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void { | ... | @@ -618,7 +618,7 @@ pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void { |
| 618 | } | 618 | } |
| 619 | 619 | ||
| 620 | pub fn expectExitCode(run: *Run, code: u8) void { | 620 | pub fn expectExitCode(run: *Run, code: u8) void { |
| 621 | const new_check: StdIo.Check = .{ .expect_term = .{ .Exited = code } }; | 621 | const new_check: StdIo.Check = .{ .expect_term = .{ .exited = code } }; |
| 622 | run.addCheck(new_check); | 622 | run.addCheck(new_check); |
| 623 | } | 623 | } |
| 624 | 624 | ||
| ... | @@ -1182,40 +1182,40 @@ fn populateGeneratedPaths( | ... | @@ -1182,40 +1182,40 @@ fn populateGeneratedPaths( |
| 1182 | } | 1182 | } |
| 1183 | } | 1183 | } |
| 1184 | 1184 | ||
| 1185 | fn formatTerm(term: ?std.process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { | 1185 | fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { |
| 1186 | if (term) |t| switch (t) { | 1186 | if (term) |t| switch (t) { |
| 1187 | .Exited => |code| try w.print("exited with code {d}", .{code}), | 1187 | .exited => |code| try w.print("exited with code {d}", .{code}), |
| 1188 | .Signal => |sig| try w.print("terminated with signal {d}", .{sig}), | 1188 | .signal => |sig| try w.print("terminated with signal {t}", .{sig}), |
| 1189 | .Stopped => |sig| try w.print("stopped with signal {d}", .{sig}), | 1189 | .stopped => |sig| try w.print("stopped with signal {d}", .{sig}), |
| 1190 | .Unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), | 1190 | .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), |
| 1191 | } else { | 1191 | } else { |
| 1192 | try w.writeAll("exited with any code"); | 1192 | try w.writeAll("exited with any code"); |
| 1193 | } | 1193 | } |
| 1194 | } | 1194 | } |
| 1195 | fn fmtTerm(term: ?std.process.Child.Term) std.fmt.Alt(?std.process.Child.Term, formatTerm) { | 1195 | fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) { |
| 1196 | return .{ .data = term }; | 1196 | return .{ .data = term }; |
| 1197 | } | 1197 | } |
| 1198 | 1198 | ||
| 1199 | fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term) bool { | 1199 | fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool { |
| 1200 | return if (expected) |e| switch (e) { | 1200 | return if (expected) |e| switch (e) { |
| 1201 | .Exited => |expected_code| switch (actual) { | 1201 | .exited => |expected_code| switch (actual) { |
| 1202 | .Exited => |actual_code| expected_code == actual_code, | 1202 | .exited => |actual_code| expected_code == actual_code, |
| 1203 | else => false, | 1203 | else => false, |
| 1204 | }, | 1204 | }, |
| 1205 | .Signal => |expected_sig| switch (actual) { | 1205 | .signal => |expected_sig| switch (actual) { |
| 1206 | .Signal => |actual_sig| expected_sig == actual_sig, | 1206 | .signal => |actual_sig| expected_sig == actual_sig, |
| 1207 | else => false, | 1207 | else => false, |
| 1208 | }, | 1208 | }, |
| 1209 | .Stopped => |expected_sig| switch (actual) { | 1209 | .stopped => |expected_sig| switch (actual) { |
| 1210 | .Stopped => |actual_sig| expected_sig == actual_sig, | 1210 | .stopped => |actual_sig| expected_sig == actual_sig, |
| 1211 | else => false, | 1211 | else => false, |
| 1212 | }, | 1212 | }, |
| 1213 | .Unknown => |expected_code| switch (actual) { | 1213 | .unknown => |expected_code| switch (actual) { |
| 1214 | .Unknown => |actual_code| expected_code == actual_code, | 1214 | .unknown => |actual_code| expected_code == actual_code, |
| 1215 | else => false, | 1215 | else => false, |
| 1216 | }, | 1216 | }, |
| 1217 | } else switch (actual) { | 1217 | } else switch (actual) { |
| 1218 | .Exited => true, | 1218 | .exited => true, |
| 1219 | else => false, | 1219 | else => false, |
| 1220 | }; | 1220 | }; |
| 1221 | } | 1221 | } |
| ... | @@ -1526,8 +1526,8 @@ fn runCommand( | ... | @@ -1526,8 +1526,8 @@ fn runCommand( |
| 1526 | else => { | 1526 | else => { |
| 1527 | // On failure, report captured stderr like normal standard error output. | 1527 | // On failure, report captured stderr like normal standard error output. |
| 1528 | const bad_exit = switch (generic_result.term) { | 1528 | const bad_exit = switch (generic_result.term) { |
| 1529 | .Exited => |code| code != 0, | 1529 | .exited => |code| code != 0, |
| 1530 | .Signal, .Stopped, .Unknown => true, | 1530 | .signal, .stopped, .unknown => true, |
| 1531 | }; | 1531 | }; |
| 1532 | if (bad_exit) { | 1532 | if (bad_exit) { |
| 1533 | if (generic_result.stderr) |bytes| { | 1533 | if (generic_result.stderr) |bytes| { |
| ... | @@ -1541,7 +1541,7 @@ fn runCommand( | ... | @@ -1541,7 +1541,7 @@ fn runCommand( |
| 1541 | } | 1541 | } |
| 1542 | 1542 | ||
| 1543 | const EvalGenericResult = struct { | 1543 | const EvalGenericResult = struct { |
| 1544 | term: std.process.Child.Term, | 1544 | term: process.Child.Term, |
| 1545 | stdout: ?[]const u8, | 1545 | stdout: ?[]const u8, |
| 1546 | stderr: ?[]const u8, | 1546 | stderr: ?[]const u8, |
| 1547 | }; | 1547 | }; |
| ... | @@ -1555,7 +1555,6 @@ fn spawnChildAndCollect( | ... | @@ -1555,7 +1555,6 @@ fn spawnChildAndCollect( |
| 1555 | fuzz_context: ?FuzzContext, | 1555 | fuzz_context: ?FuzzContext, |
| 1556 | ) !?EvalGenericResult { | 1556 | ) !?EvalGenericResult { |
| 1557 | const b = run.step.owner; | 1557 | const b = run.step.owner; |
| 1558 | const arena = b.allocator; | ||
| 1559 | const graph = b.graph; | 1558 | const graph = b.graph; |
| 1560 | const io = graph.io; | 1559 | const io = graph.io; |
| 1561 | 1560 | ||
| ... | @@ -1564,53 +1563,52 @@ fn spawnChildAndCollect( | ... | @@ -1564,53 +1563,52 @@ fn spawnChildAndCollect( |
| 1564 | assert(run.stdio == .zig_test); | 1563 | assert(run.stdio == .zig_test); |
| 1565 | } | 1564 | } |
| 1566 | 1565 | ||
| 1567 | var child = std.process.Child.init(arena, argv, .{ .map = env_map }); | 1566 | const child_cwd = if (run.cwd) |lazy_cwd| lazy_cwd.getPath2(b, &run.step) else null; |
| 1568 | if (run.cwd) |lazy_cwd| { | ||
| 1569 | child.cwd = lazy_cwd.getPath2(b, &run.step); | ||
| 1570 | } | ||
| 1571 | child.request_resource_usage_statistics = true; | ||
| 1572 | |||
| 1573 | child.stdin_behavior = switch (run.stdio) { | ||
| 1574 | .infer_from_args => if (has_side_effects) .Inherit else .Ignore, | ||
| 1575 | .inherit => .Inherit, | ||
| 1576 | .check => .Ignore, | ||
| 1577 | .zig_test => .Pipe, | ||
| 1578 | }; | ||
| 1579 | child.stdout_behavior = switch (run.stdio) { | ||
| 1580 | .infer_from_args => if (has_side_effects) .Inherit else .Ignore, | ||
| 1581 | .inherit => .Inherit, | ||
| 1582 | .check => |checks| if (checksContainStdout(checks.items)) .Pipe else .Ignore, | ||
| 1583 | .zig_test => .Pipe, | ||
| 1584 | }; | ||
| 1585 | child.stderr_behavior = switch (run.stdio) { | ||
| 1586 | .infer_from_args => if (has_side_effects) .Inherit else .Pipe, | ||
| 1587 | .inherit => .Inherit, | ||
| 1588 | .check => .Pipe, | ||
| 1589 | .zig_test => .Pipe, | ||
| 1590 | }; | ||
| 1591 | if (run.captured_stdout != null) child.stdout_behavior = .Pipe; | ||
| 1592 | if (run.captured_stderr != null) child.stderr_behavior = .Pipe; | ||
| 1593 | if (run.stdin != .none) { | ||
| 1594 | assert(run.stdio != .inherit); | ||
| 1595 | child.stdin_behavior = .Pipe; | ||
| 1596 | } | ||
| 1597 | 1567 | ||
| 1598 | // If an error occurs, it's caused by this command: | 1568 | // If an error occurs, it's caused by this command: |
| 1599 | assert(run.step.result_failed_command == null); | 1569 | assert(run.step.result_failed_command == null); |
| 1600 | run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child.cwd, .{ | 1570 | run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{ |
| 1601 | .child = env_map, | 1571 | .child = env_map, |
| 1602 | .parent = &graph.env_map, | 1572 | .parent = &graph.env_map, |
| 1603 | }, argv); | 1573 | }, argv); |
| 1604 | 1574 | ||
| 1575 | var spawn_options: process.SpawnOptions = .{ | ||
| 1576 | .argv = argv, | ||
| 1577 | .cwd = child_cwd, | ||
| 1578 | .env_map = &graph.env_map, | ||
| 1579 | .request_resource_usage_statistics = true, | ||
| 1580 | .stdin = if (run.stdin != .none) s: { | ||
| 1581 | assert(run.stdio != .inherit); | ||
| 1582 | break :s .pipe; | ||
| 1583 | } else switch (run.stdio) { | ||
| 1584 | .infer_from_args => if (has_side_effects) .inherit else .ignore, | ||
| 1585 | .inherit => .inherit, | ||
| 1586 | .check => .ignore, | ||
| 1587 | .zig_test => .pipe, | ||
| 1588 | }, | ||
| 1589 | .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) { | ||
| 1590 | .infer_from_args => if (has_side_effects) .inherit else .ignore, | ||
| 1591 | .inherit => .inherit, | ||
| 1592 | .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore, | ||
| 1593 | .zig_test => .pipe, | ||
| 1594 | }, | ||
| 1595 | .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) { | ||
| 1596 | .infer_from_args => if (has_side_effects) .inherit else .pipe, | ||
| 1597 | .inherit => .inherit, | ||
| 1598 | .check => .pipe, | ||
| 1599 | .zig_test => .pipe, | ||
| 1600 | }, | ||
| 1601 | }; | ||
| 1602 | |||
| 1605 | if (run.stdio == .zig_test) { | 1603 | if (run.stdio == .zig_test) { |
| 1606 | var timer = try std.time.Timer.start(); | 1604 | var timer = try std.time.Timer.start(); |
| 1607 | defer run.step.result_duration_ns = timer.read(); | 1605 | defer run.step.result_duration_ns = timer.read(); |
| 1608 | try evalZigTest(run, &child, options, fuzz_context); | 1606 | try evalZigTest(run, spawn_options, options, fuzz_context); |
| 1609 | return null; | 1607 | return null; |
| 1610 | } else { | 1608 | } else { |
| 1611 | const inherit = child.stdout_behavior == .Inherit or child.stderr_behavior == .Inherit; | 1609 | const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; |
| 1612 | if (!run.disable_zig_progress and !inherit) { | 1610 | if (!run.disable_zig_progress and !inherit) { |
| 1613 | child.progress_node = options.progress_node; | 1611 | spawn_options.progress_node = options.progress_node; |
| 1614 | } | 1612 | } |
| 1615 | const terminal_mode: Io.Terminal.Mode = if (inherit) m: { | 1613 | const terminal_mode: Io.Terminal.Mode = if (inherit) m: { |
| 1616 | const stderr = try io.lockStderr(&.{}, graph.stderr_mode); | 1614 | const stderr = try io.lockStderr(&.{}, graph.stderr_mode); |
| ... | @@ -1619,7 +1617,7 @@ fn spawnChildAndCollect( | ... | @@ -1619,7 +1617,7 @@ fn spawnChildAndCollect( |
| 1619 | defer if (inherit) io.unlockStderr(); | 1617 | defer if (inherit) io.unlockStderr(); |
| 1620 | try setColorEnvironmentVariables(run, env_map, terminal_mode); | 1618 | try setColorEnvironmentVariables(run, env_map, terminal_mode); |
| 1621 | var timer = try std.time.Timer.start(); | 1619 | var timer = try std.time.Timer.start(); |
| 1622 | const res = try evalGeneric(run, &child); | 1620 | const res = try evalGeneric(run, spawn_options); |
| 1623 | run.step.result_duration_ns = timer.read(); | 1621 | run.step.result_duration_ns = timer.read(); |
| 1624 | return .{ .term = res.term, .stdout = res.stdout, .stderr = res.stderr }; | 1622 | return .{ .term = res.term, .stdout = res.stdout, .stderr = res.stderr }; |
| 1625 | } | 1623 | } |
| ... | @@ -1658,7 +1656,7 @@ const StdioPollEnum = enum { stdout, stderr }; | ... | @@ -1658,7 +1656,7 @@ const StdioPollEnum = enum { stdout, stderr }; |
| 1658 | 1656 | ||
| 1659 | fn evalZigTest( | 1657 | fn evalZigTest( |
| 1660 | run: *Run, | 1658 | run: *Run, |
| 1661 | child: *std.process.Child, | 1659 | spawn_options: process.SpawnOptions, |
| 1662 | options: Step.MakeOptions, | 1660 | options: Step.MakeOptions, |
| 1663 | fuzz_context: ?FuzzContext, | 1661 | fuzz_context: ?FuzzContext, |
| 1664 | ) !void { | 1662 | ) !void { |
| ... | @@ -1682,14 +1680,14 @@ fn evalZigTest( | ... | @@ -1682,14 +1680,14 @@ fn evalZigTest( |
| 1682 | var test_metadata: ?TestMetadata = null; | 1680 | var test_metadata: ?TestMetadata = null; |
| 1683 | 1681 | ||
| 1684 | while (true) { | 1682 | while (true) { |
| 1685 | try child.spawn(io); | 1683 | var child = try process.spawn(io, spawn_options); |
| 1686 | var poller = std.Io.poll(gpa, StdioPollEnum, .{ | 1684 | var poller = std.Io.poll(gpa, StdioPollEnum, .{ |
| 1687 | .stdout = child.stdout.?, | 1685 | .stdout = child.stdout.?, |
| 1688 | .stderr = child.stderr.?, | 1686 | .stderr = child.stderr.?, |
| 1689 | }); | 1687 | }); |
| 1690 | var child_killed = false; | 1688 | var child_killed = false; |
| 1691 | defer if (!child_killed) { | 1689 | defer if (!child_killed) { |
| 1692 | _ = child.kill(io) catch {}; | 1690 | child.kill(io); |
| 1693 | poller.deinit(); | 1691 | poller.deinit(); |
| 1694 | run.step.result_peak_rss = @max( | 1692 | run.step.result_peak_rss = @max( |
| 1695 | run.step.result_peak_rss, | 1693 | run.step.result_peak_rss, |
| ... | @@ -1697,11 +1695,9 @@ fn evalZigTest( | ... | @@ -1697,11 +1695,9 @@ fn evalZigTest( |
| 1697 | ); | 1695 | ); |
| 1698 | }; | 1696 | }; |
| 1699 | 1697 | ||
| 1700 | try child.waitForSpawn(); | ||
| 1701 | |||
| 1702 | switch (try pollZigTest( | 1698 | switch (try pollZigTest( |
| 1703 | run, | 1699 | run, |
| 1704 | child, | 1700 | &child, |
| 1705 | options, | 1701 | options, |
| 1706 | fuzz_context, | 1702 | fuzz_context, |
| 1707 | &poller, | 1703 | &poller, |
| ... | @@ -1763,7 +1759,7 @@ fn evalZigTest( | ... | @@ -1763,7 +1759,7 @@ fn evalZigTest( |
| 1763 | // Report an error if the child terminated uncleanly or if we were still trying to run more tests. | 1759 | // Report an error if the child terminated uncleanly or if we were still trying to run more tests. |
| 1764 | run.step.result_stderr = stderr_owned; | 1760 | run.step.result_stderr = stderr_owned; |
| 1765 | const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); | 1761 | const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); |
| 1766 | if (!tests_done or !termMatches(.{ .Exited = 0 }, term)) { | 1762 | if (!tests_done or !termMatches(.{ .exited = 0 }, term)) { |
| 1767 | // The individual unit test results are irrelevant: the test runner itself broke! | 1763 | // The individual unit test results are irrelevant: the test runner itself broke! |
| 1768 | // Fail immediately without populating `s.test_results`. | 1764 | // Fail immediately without populating `s.test_results`. |
| 1769 | return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); | 1765 | return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); |
| ... | @@ -1818,7 +1814,7 @@ fn evalZigTest( | ... | @@ -1818,7 +1814,7 @@ fn evalZigTest( |
| 1818 | /// * `poll` fails, indicating the child closed stdout and stderr | 1814 | /// * `poll` fails, indicating the child closed stdout and stderr |
| 1819 | fn pollZigTest( | 1815 | fn pollZigTest( |
| 1820 | run: *Run, | 1816 | run: *Run, |
| 1821 | child: *std.process.Child, | 1817 | child: *process.Child, |
| 1822 | options: Step.MakeOptions, | 1818 | options: Step.MakeOptions, |
| 1823 | fuzz_context: ?FuzzContext, | 1819 | fuzz_context: ?FuzzContext, |
| 1824 | poller: *std.Io.Poller(StdioPollEnum), | 1820 | poller: *std.Io.Poller(StdioPollEnum), |
| ... | @@ -2176,15 +2172,13 @@ fn sendRunFuzzTestMessage( | ... | @@ -2176,15 +2172,13 @@ fn sendRunFuzzTestMessage( |
| 2176 | }; | 2172 | }; |
| 2177 | } | 2173 | } |
| 2178 | 2174 | ||
| 2179 | fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { | 2175 | fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult { |
| 2180 | const b = run.step.owner; | 2176 | const b = run.step.owner; |
| 2181 | const io = b.graph.io; | 2177 | const io = b.graph.io; |
| 2182 | const arena = b.allocator; | 2178 | const arena = b.allocator; |
| 2183 | 2179 | ||
| 2184 | try child.spawn(io); | 2180 | var child = try process.spawn(io, spawn_options); |
| 2185 | errdefer _ = child.kill(io) catch {}; | 2181 | defer child.kill(io); |
| 2186 | |||
| 2187 | try child.waitForSpawn(); | ||
| 2188 | 2182 | ||
| 2189 | switch (run.stdin) { | 2183 | switch (run.stdin) { |
| 2190 | .bytes => |bytes| { | 2184 | .bytes => |bytes| { |
| ... | @@ -2334,10 +2328,10 @@ fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void { | ... | @@ -2334,10 +2328,10 @@ fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void { |
| 2334 | => |s| hh.addBytes(s), | 2328 | => |s| hh.addBytes(s), |
| 2335 | 2329 | ||
| 2336 | .expect_term => |term| { | 2330 | .expect_term => |term| { |
| 2337 | hh.add(@as(std.meta.Tag(std.process.Child.Term), term)); | 2331 | hh.add(@as(std.meta.Tag(process.Child.Term), term)); |
| 2338 | switch (term) { | 2332 | switch (term) { |
| 2339 | .Exited => |x| hh.add(x), | 2333 | inline .exited, .signal => |x| hh.add(x), |
| 2340 | .Signal, .Stopped, .Unknown => |x| hh.add(x), | 2334 | .stopped, .unknown => |x| hh.add(x), |
| 2341 | } | 2335 | } |
| 2342 | }, | 2336 | }, |
| 2343 | } | 2337 | } |
lib/std/Build/WebServer.zig+17-7| ... | @@ -572,11 +572,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim | ... | @@ -572,11 +572,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim |
| 572 | "--listen=-", | 572 | "--listen=-", |
| 573 | }); | 573 | }); |
| 574 | 574 | ||
| 575 | var child: std.process.Child = .init(gpa, argv.items, .{ .map = &graph.env_map }); | 575 | var child = try std.process.spawn(io, .{ |
| 576 | child.stdin_behavior = .Pipe; | 576 | .argv = argv.items, |
| 577 | child.stdout_behavior = .Pipe; | 577 | .env_map = &graph.env_map, |
| 578 | child.stderr_behavior = .Pipe; | 578 | .stdin = .pipe, |
| 579 | try child.spawn(io); | 579 | .stdout = .pipe, |
| 580 | .stderr = .pipe, | ||
| 581 | }); | ||
| 582 | defer child.kill(io); | ||
| 580 | 583 | ||
| 581 | var poller = Io.poll(gpa, enum { stdout, stderr }, .{ | 584 | var poller = Io.poll(gpa, enum { stdout, stderr }, .{ |
| 582 | .stdout = child.stdout.?, | 585 | .stdout = child.stdout.?, |
| ... | @@ -636,7 +639,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim | ... | @@ -636,7 +639,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim |
| 636 | child.stdin = null; | 639 | child.stdin = null; |
| 637 | 640 | ||
| 638 | switch (try child.wait(io)) { | 641 | switch (try child.wait(io)) { |
| 639 | .Exited => |code| { | 642 | .exited => |code| { |
| 640 | if (code != 0) { | 643 | if (code != 0) { |
| 641 | log.err( | 644 | log.err( |
| 642 | "the following command exited with error code {d}:\n{s}", | 645 | "the following command exited with error code {d}:\n{s}", |
| ... | @@ -645,7 +648,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim | ... | @@ -645,7 +648,14 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim |
| 645 | return error.WasmCompilationFailed; | 648 | return error.WasmCompilationFailed; |
| 646 | } | 649 | } |
| 647 | }, | 650 | }, |
| 648 | .Signal, .Stopped, .Unknown => { | 651 | .signal => |sig| { |
| 652 | log.err( | ||
| 653 | "the following command terminated with signal {t}:\n{s}", | ||
| 654 | .{ sig, try Build.Step.allocPrintCmd(arena, null, null, argv.items) }, | ||
| 655 | ); | ||
| 656 | return error.WasmCompilationFailed; | ||
| 657 | }, | ||
| 658 | .stopped, .unknown => { | ||
| 649 | log.err( | 659 | log.err( |
| 650 | "the following command terminated unexpectedly:\n{s}", | 660 | "the following command terminated unexpectedly:\n{s}", |
| 651 | .{try Build.Step.allocPrintCmd(arena, null, null, argv.items)}, | 661 | .{try Build.Step.allocPrintCmd(arena, null, null, argv.items)}, |
lib/std/Io.zig+6| ... | @@ -717,6 +717,12 @@ pub const VTable = struct { | ... | @@ -717,6 +717,12 @@ pub const VTable = struct { |
| 717 | tryLockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!?LockedStderr, | 717 | tryLockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!?LockedStderr, |
| 718 | unlockStderr: *const fn (?*anyopaque) void, | 718 | unlockStderr: *const fn (?*anyopaque) void, |
| 719 | processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void, | 719 | processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void, |
| 720 | processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError, | ||
| 721 | processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError, | ||
| 722 | processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, | ||
| 723 | processSpawnPath: *const fn (?*anyopaque, Dir, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, | ||
| 724 | childWait: *const fn (?*anyopaque, *std.process.Child) std.process.Child.WaitError!std.process.Child.Term, | ||
| 725 | childKill: *const fn (?*anyopaque, *std.process.Child) void, | ||
| 720 | 726 | ||
| 721 | now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, | 727 | now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, |
| 722 | sleep: *const fn (?*anyopaque, Timeout) SleepError!void, | 728 | sleep: *const fn (?*anyopaque, Timeout) SleepError!void, |
lib/std/Io/Threaded.zig+1675-23| ... | @@ -13,6 +13,7 @@ const File = std.Io.File; | ... | @@ -13,6 +13,7 @@ const File = std.Io.File; |
| 13 | const Dir = std.Io.Dir; | 13 | const Dir = std.Io.Dir; |
| 14 | const HostName = std.Io.net.HostName; | 14 | const HostName = std.Io.net.HostName; |
| 15 | const IpAddress = std.Io.net.IpAddress; | 15 | const IpAddress = std.Io.net.IpAddress; |
| 16 | const process = std.process; | ||
| 16 | const Allocator = std.mem.Allocator; | 17 | const Allocator = std.mem.Allocator; |
| 17 | const Alignment = std.mem.Alignment; | 18 | const Alignment = std.mem.Alignment; |
| 18 | const assert = std.debug.assert; | 19 | const assert = std.debug.assert; |
| ... | @@ -72,7 +73,7 @@ pub const Argv0 = switch (native_os) { | ... | @@ -72,7 +73,7 @@ pub const Argv0 = switch (native_os) { |
| 72 | 73 | ||
| 73 | pub const Environ = struct { | 74 | pub const Environ = struct { |
| 74 | /// Unmodified data directly from the OS. | 75 | /// Unmodified data directly from the OS. |
| 75 | block: std.process.Environ.Block = &.{}, | 76 | block: process.Environ.Block = &.{}, |
| 76 | /// Protected by `mutex`. Determines whether the other fields have been | 77 | /// Protected by `mutex`. Determines whether the other fields have been |
| 77 | /// memoized based on `block`. | 78 | /// memoized based on `block`. |
| 78 | initialized: bool = false, | 79 | initialized: bool = false, |
| ... | @@ -95,10 +96,10 @@ pub const Environ = struct { | ... | @@ -95,10 +96,10 @@ pub const Environ = struct { |
| 95 | }; | 96 | }; |
| 96 | 97 | ||
| 97 | pub const String = switch (native_os) { | 98 | pub const String = switch (native_os) { |
| 98 | .openbsd, .haiku => struct { | 99 | .windows, .wasi => struct {}, |
| 100 | else => struct { | ||
| 99 | PATH: ?[:0]const u8 = null, | 101 | PATH: ?[:0]const u8 = null, |
| 100 | }, | 102 | }, |
| 101 | else => struct {}, | ||
| 102 | }; | 103 | }; |
| 103 | }; | 104 | }; |
| 104 | 105 | ||
| ... | @@ -1400,6 +1401,12 @@ pub fn io(t: *Threaded) Io { | ... | @@ -1400,6 +1401,12 @@ pub fn io(t: *Threaded) Io { |
| 1400 | .tryLockStderr = tryLockStderr, | 1401 | .tryLockStderr = tryLockStderr, |
| 1401 | .unlockStderr = unlockStderr, | 1402 | .unlockStderr = unlockStderr, |
| 1402 | .processSetCurrentDir = processSetCurrentDir, | 1403 | .processSetCurrentDir = processSetCurrentDir, |
| 1404 | .processReplace = processReplace, // TODO audit for cancelation and unreachable | ||
| 1405 | .processReplacePath = processReplacePath, // TODO audit for cancelation and unreachable | ||
| 1406 | .processSpawn = processSpawn, // TODO audit for cancelation and unreachable | ||
| 1407 | .processSpawnPath = processSpawnPath, // TODO audit for cancelation and unreachable | ||
| 1408 | .childWait = childWait, // TODO audit for cancelation and unreachable | ||
| 1409 | .childKill = childKill, // TODO audit for cancelation and unreachable | ||
| 1403 | 1410 | ||
| 1404 | .now = now, | 1411 | .now = now, |
| 1405 | .sleep = sleep, | 1412 | .sleep = sleep, |
| ... | @@ -1538,6 +1545,12 @@ pub fn ioBasic(t: *Threaded) Io { | ... | @@ -1538,6 +1545,12 @@ pub fn ioBasic(t: *Threaded) Io { |
| 1538 | .tryLockStderr = tryLockStderr, | 1545 | .tryLockStderr = tryLockStderr, |
| 1539 | .unlockStderr = unlockStderr, | 1546 | .unlockStderr = unlockStderr, |
| 1540 | .processSetCurrentDir = processSetCurrentDir, | 1547 | .processSetCurrentDir = processSetCurrentDir, |
| 1548 | .processReplace = processReplace, | ||
| 1549 | .processReplacePath = processReplacePath, | ||
| 1550 | .processSpawn = processSpawn, | ||
| 1551 | .processSpawnPath = processSpawnPath, | ||
| 1552 | .childWait = childWait, | ||
| 1553 | .childKill = childKill, | ||
| 1541 | 1554 | ||
| 1542 | .now = now, | 1555 | .now = now, |
| 1543 | .sleep = sleep, | 1556 | .sleep = sleep, |
| ... | @@ -1601,6 +1614,11 @@ const have_fchmod = switch (native_os) { | ... | @@ -1601,6 +1614,11 @@ const have_fchmod = switch (native_os) { |
| 1601 | else => true, | 1614 | else => true, |
| 1602 | }; | 1615 | }; |
| 1603 | 1616 | ||
| 1617 | const have_wait4 = switch (native_os) { | ||
| 1618 | .dragonfly, .freebsd, .netbsd, .openbsd, .illumos, .linux, .serenity, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => true, | ||
| 1619 | else => false, | ||
| 1620 | }; | ||
| 1621 | |||
| 1604 | const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; | 1622 | const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; |
| 1605 | const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; | 1623 | const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; |
| 1606 | const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; | 1624 | const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; |
| ... | @@ -2247,7 +2265,7 @@ fn dirCreateDirPath( | ... | @@ -2247,7 +2265,7 @@ fn dirCreateDirPath( |
| 2247 | ) Dir.CreateDirPathError!Dir.CreatePathStatus { | 2265 | ) Dir.CreateDirPathError!Dir.CreatePathStatus { |
| 2248 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | 2266 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 2249 | 2267 | ||
| 2250 | var it = std.fs.path.componentIterator(sub_path); | 2268 | var it = Dir.path.componentIterator(sub_path); |
| 2251 | var status: Dir.CreatePathStatus = .existed; | 2269 | var status: Dir.CreatePathStatus = .existed; |
| 2252 | var component = it.last() orelse return error.BadPathName; | 2270 | var component = it.last() orelse return error.BadPathName; |
| 2253 | while (true) { | 2271 | while (true) { |
| ... | @@ -2307,9 +2325,9 @@ fn dirCreateDirPathOpenWindows( | ... | @@ -2307,9 +2325,9 @@ fn dirCreateDirPathOpenWindows( |
| 2307 | 2325 | ||
| 2308 | _ = permissions; // TODO apply these permissions | 2326 | _ = permissions; // TODO apply these permissions |
| 2309 | 2327 | ||
| 2310 | var it = std.fs.path.componentIterator(sub_path); | 2328 | var it = Dir.path.componentIterator(sub_path); |
| 2311 | // If there are no components in the path, then create a dummy component with the full path. | 2329 | // If there are no components in the path, then create a dummy component with the full path. |
| 2312 | var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{ | 2330 | var component: Dir.path.NativeComponentIterator.Component = it.last() orelse .{ |
| 2313 | .name = "", | 2331 | .name = "", |
| 2314 | .path = sub_path, | 2332 | .path = sub_path, |
| 2315 | }; | 2333 | }; |
| ... | @@ -2347,7 +2365,7 @@ fn dirCreateDirPathOpenWindows( | ... | @@ -2347,7 +2365,7 @@ fn dirCreateDirPathOpenWindows( |
| 2347 | }, | 2365 | }, |
| 2348 | &.{ | 2366 | &.{ |
| 2349 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), | 2367 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), |
| 2350 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, | 2368 | .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, |
| 2351 | .Attributes = .{}, | 2369 | .Attributes = .{}, |
| 2352 | .ObjectName = &nt_name, | 2370 | .ObjectName = &nt_name, |
| 2353 | .SecurityDescriptor = null, | 2371 | .SecurityDescriptor = null, |
| ... | @@ -2986,7 +3004,7 @@ fn dirAccessWindows( | ... | @@ -2986,7 +3004,7 @@ fn dirAccessWindows( |
| 2986 | }; | 3004 | }; |
| 2987 | var attr: windows.OBJECT_ATTRIBUTES = .{ | 3005 | var attr: windows.OBJECT_ATTRIBUTES = .{ |
| 2988 | .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), | 3006 | .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), |
| 2989 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, | 3007 | .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, |
| 2990 | .Attributes = .{}, | 3008 | .Attributes = .{}, |
| 2991 | .ObjectName = &nt_name, | 3009 | .ObjectName = &nt_name, |
| 2992 | .SecurityDescriptor = null, | 3010 | .SecurityDescriptor = null, |
| ... | @@ -3535,7 +3553,7 @@ fn dirOpenFileWindows( | ... | @@ -3535,7 +3553,7 @@ fn dirOpenFileWindows( |
| 3535 | _ = t; | 3553 | _ = t; |
| 3536 | const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); | 3554 | const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); |
| 3537 | const sub_path_w = sub_path_w_array.span(); | 3555 | const sub_path_w = sub_path_w_array.span(); |
| 3538 | const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; | 3556 | const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; |
| 3539 | return dirOpenFileWtf16(dir_handle, sub_path_w, flags); | 3557 | return dirOpenFileWtf16(dir_handle, sub_path_w, flags); |
| 3540 | } | 3558 | } |
| 3541 | 3559 | ||
| ... | @@ -3933,7 +3951,7 @@ pub fn dirOpenDirWindows( | ... | @@ -3933,7 +3951,7 @@ pub fn dirOpenDirWindows( |
| 3933 | }, | 3951 | }, |
| 3934 | &.{ | 3952 | &.{ |
| 3935 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), | 3953 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), |
| 3936 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, | 3954 | .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, |
| 3937 | .Attributes = .{}, | 3955 | .Attributes = .{}, |
| 3938 | .ObjectName = &nt_name, | 3956 | .ObjectName = &nt_name, |
| 3939 | .SecurityDescriptor = null, | 3957 | .SecurityDescriptor = null, |
| ... | @@ -5081,7 +5099,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov | ... | @@ -5081,7 +5099,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov |
| 5081 | } }, | 5099 | } }, |
| 5082 | &.{ | 5100 | &.{ |
| 5083 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), | 5101 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), |
| 5084 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, | 5102 | .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, |
| 5085 | .Attributes = .{}, | 5103 | .Attributes = .{}, |
| 5086 | .ObjectName = &nt_name, | 5104 | .ObjectName = &nt_name, |
| 5087 | .SecurityDescriptor = null, | 5105 | .SecurityDescriptor = null, |
| ... | @@ -5358,7 +5376,7 @@ fn dirRenameWindows( | ... | @@ -5358,7 +5376,7 @@ fn dirRenameWindows( |
| 5358 | .POSIX_SEMANTICS = true, | 5376 | .POSIX_SEMANTICS = true, |
| 5359 | .IGNORE_READONLY_ATTRIBUTE = true, | 5377 | .IGNORE_READONLY_ATTRIBUTE = true, |
| 5360 | }, | 5378 | }, |
| 5361 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, | 5379 | .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, |
| 5362 | .FileName = new_path_w, | 5380 | .FileName = new_path_w, |
| 5363 | }); | 5381 | }); |
| 5364 | var io_status_block: w.IO_STATUS_BLOCK = undefined; | 5382 | var io_status_block: w.IO_STATUS_BLOCK = undefined; |
| ... | @@ -5387,7 +5405,7 @@ fn dirRenameWindows( | ... | @@ -5387,7 +5405,7 @@ fn dirRenameWindows( |
| 5387 | if (need_fallback) { | 5405 | if (need_fallback) { |
| 5388 | const rename_info: w.FILE.RENAME_INFORMATION = .init(.{ | 5406 | const rename_info: w.FILE.RENAME_INFORMATION = .init(.{ |
| 5389 | .Flags = .{ .REPLACE_IF_EXISTS = replace_if_exists }, | 5407 | .Flags = .{ .REPLACE_IF_EXISTS = replace_if_exists }, |
| 5390 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, | 5408 | .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle, |
| 5391 | .FileName = new_path_w, | 5409 | .FileName = new_path_w, |
| 5392 | }); | 5410 | }); |
| 5393 | var io_status_block: w.IO_STATUS_BLOCK = undefined; | 5411 | var io_status_block: w.IO_STATUS_BLOCK = undefined; |
| ... | @@ -5622,13 +5640,13 @@ fn dirSymLinkWindows( | ... | @@ -5622,13 +5640,13 @@ fn dirSymLinkWindows( |
| 5622 | // the C:\ drive. | 5640 | // the C:\ drive. |
| 5623 | .rooted => break :target_path target_path_w.span(), | 5641 | .rooted => break :target_path target_path_w.span(), |
| 5624 | // Keep relative paths relative, but anything else needs to get NT-prefixed. | 5642 | // Keep relative paths relative, but anything else needs to get NT-prefixed. |
| 5625 | else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path_w.span())) | 5643 | else => if (!Dir.path.isAbsoluteWindowsWtf16(target_path_w.span())) |
| 5626 | break :target_path target_path_w.span(), | 5644 | break :target_path target_path_w.span(), |
| 5627 | } | 5645 | } |
| 5628 | } | 5646 | } |
| 5629 | var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span()); | 5647 | var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span()); |
| 5630 | // We do this after prefixing to ensure that drive-relative paths are treated as absolute | 5648 | // We do this after prefixing to ensure that drive-relative paths are treated as absolute |
| 5631 | is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span()); | 5649 | is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span()); |
| 5632 | break :target_path prefixed_target_path.span(); | 5650 | break :target_path prefixed_target_path.span(); |
| 5633 | }; | 5651 | }; |
| 5634 | 5652 | ||
| ... | @@ -5636,8 +5654,8 @@ fn dirSymLinkWindows( | ... | @@ -5636,8 +5654,8 @@ fn dirSymLinkWindows( |
| 5636 | var buffer: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; | 5654 | var buffer: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; |
| 5637 | const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4; | 5655 | const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4; |
| 5638 | const header_len = @sizeOf(w.ULONG) + @sizeOf(w.USHORT) * 2; | 5656 | const header_len = @sizeOf(w.ULONG) + @sizeOf(w.USHORT) * 2; |
| 5639 | const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path); | 5657 | const target_is_absolute = Dir.path.isAbsoluteWindowsWtf16(final_target_path); |
| 5640 | const symlink_data = SYMLINK_DATA{ | 5658 | const symlink_data: SYMLINK_DATA = .{ |
| 5641 | .ReparseTag = .SYMLINK, | 5659 | .ReparseTag = .SYMLINK, |
| 5642 | .ReparseDataLength = @intCast(buf_len - header_len), | 5660 | .ReparseDataLength = @intCast(buf_len - header_len), |
| 5643 | .Reserved = 0, | 5661 | .Reserved = 0, |
| ... | @@ -7890,7 +7908,7 @@ fn posixSeekTo(fd: posix.fd_t, offset: u64) File.SeekError!void { | ... | @@ -7890,7 +7908,7 @@ fn posixSeekTo(fd: posix.fd_t, offset: u64) File.SeekError!void { |
| 7890 | } | 7908 | } |
| 7891 | } | 7909 | } |
| 7892 | 7910 | ||
| 7893 | fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.process.OpenExecutableError!File { | 7911 | fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.OpenExecutableError!File { |
| 7894 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | 7912 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 7895 | switch (native_os) { | 7913 | switch (native_os) { |
| 7896 | .wasi => return error.OperationUnsupported, | 7914 | .wasi => return error.OperationUnsupported, |
| ... | @@ -7931,7 +7949,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.proce | ... | @@ -7931,7 +7949,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.proce |
| 7931 | } | 7949 | } |
| 7932 | } | 7950 | } |
| 7933 | 7951 | ||
| 7934 | fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.ExecutablePathError!usize { | 7952 | fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.ExecutablePathError!usize { |
| 7935 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | 7953 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 7936 | 7954 | ||
| 7937 | switch (native_os) { | 7955 | switch (native_os) { |
| ... | @@ -11691,14 +11709,14 @@ fn netLookupFallible( | ... | @@ -11691,14 +11709,14 @@ fn netLookupFallible( |
| 11691 | fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr { | 11709 | fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr { |
| 11692 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | 11710 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 11693 | // Only global mutex since this is Threaded. | 11711 | // Only global mutex since this is Threaded. |
| 11694 | std.process.stderr_thread_mutex.lock(); | 11712 | process.stderr_thread_mutex.lock(); |
| 11695 | return initLockedStderr(t, terminal_mode); | 11713 | return initLockedStderr(t, terminal_mode); |
| 11696 | } | 11714 | } |
| 11697 | 11715 | ||
| 11698 | fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr { | 11716 | fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr { |
| 11699 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | 11717 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 11700 | // Only global mutex since this is Threaded. | 11718 | // Only global mutex since this is Threaded. |
| 11701 | if (!std.process.stderr_thread_mutex.tryLock()) return null; | 11719 | if (!process.stderr_thread_mutex.tryLock()) return null; |
| 11702 | return try initLockedStderr(t, terminal_mode); | 11720 | return try initLockedStderr(t, terminal_mode); |
| 11703 | } | 11721 | } |
| 11704 | 11722 | ||
| ... | @@ -11729,10 +11747,10 @@ fn unlockStderr(userdata: ?*anyopaque) void { | ... | @@ -11729,10 +11747,10 @@ fn unlockStderr(userdata: ?*anyopaque) void { |
| 11729 | }; | 11747 | }; |
| 11730 | t.stderr_writer.interface.end = 0; | 11748 | t.stderr_writer.interface.end = 0; |
| 11731 | t.stderr_writer.interface.buffer = &.{}; | 11749 | t.stderr_writer.interface.buffer = &.{}; |
| 11732 | std.process.stderr_thread_mutex.unlock(); | 11750 | process.stderr_thread_mutex.unlock(); |
| 11733 | } | 11751 | } |
| 11734 | 11752 | ||
| 11735 | fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) std.process.SetCurrentDirError!void { | 11753 | fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirError!void { |
| 11736 | if (native_os == .wasi) return error.OperationUnsupported; | 11754 | if (native_os == .wasi) return error.OperationUnsupported; |
| 11737 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | 11755 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 11738 | _ = t; | 11756 | _ = t; |
| ... | @@ -12690,6 +12708,1640 @@ fn scanEnviron(t: *Threaded) void { | ... | @@ -12690,6 +12708,1640 @@ fn scanEnviron(t: *Threaded) void { |
| 12690 | } | 12708 | } |
| 12691 | } | 12709 | } |
| 12692 | 12710 | ||
| 12711 | fn processReplace(userdata: ?*anyopaque, options: std.process.ReplaceOptions) std.process.ReplaceError { | ||
| 12712 | _ = userdata; | ||
| 12713 | _ = options; | ||
| 12714 | @panic("TODO"); | ||
| 12715 | } | ||
| 12716 | |||
| 12717 | fn processReplacePath(userdata: ?*anyopaque, dir: Dir, options: std.process.ReplaceOptions) std.process.ReplaceError { | ||
| 12718 | _ = userdata; | ||
| 12719 | _ = dir; | ||
| 12720 | _ = options; | ||
| 12721 | @panic("TODO"); | ||
| 12722 | } | ||
| 12723 | |||
| 12724 | fn processSpawnPath(userdata: ?*anyopaque, dir: Dir, options: process.SpawnOptions) process.SpawnError!process.Child { | ||
| 12725 | _ = userdata; | ||
| 12726 | _ = dir; | ||
| 12727 | _ = options; | ||
| 12728 | @panic("TODO"); | ||
| 12729 | } | ||
| 12730 | |||
| 12731 | const processSpawn = switch (native_os) { | ||
| 12732 | .wasi, .ios, .tvos, .visionos, .watchos => processSpawnUnsupported, | ||
| 12733 | .windows => processSpawnWindows, | ||
| 12734 | else => processSpawnPosix, | ||
| 12735 | }; | ||
| 12736 | |||
| 12737 | fn processSpawnUnsupported(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { | ||
| 12738 | _ = userdata; | ||
| 12739 | _ = options; | ||
| 12740 | return error.OperationUnsupported; | ||
| 12741 | } | ||
| 12742 | |||
| 12743 | fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { | ||
| 12744 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | ||
| 12745 | |||
| 12746 | // The child process does need to access (one end of) these pipes. However, | ||
| 12747 | // we must initially set CLOEXEC to avoid a race condition. If another thread | ||
| 12748 | // is racing to spawn a different child process, we don't want it to inherit | ||
| 12749 | // these FDs in any scenario; that would mean that, for instance, calls to | ||
| 12750 | // `poll` from the parent would not report the child's stdout as closing when | ||
| 12751 | // expected, since the other child may retain a reference to the write end of | ||
| 12752 | // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we | ||
| 12753 | // need to do something in the new child to make sure we preserve the reference | ||
| 12754 | // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it | ||
| 12755 | // turns out, we `dup2` everything anyway, so there's no need! | ||
| 12756 | const pipe_flags: posix.O = .{ .CLOEXEC = true }; | ||
| 12757 | |||
| 12758 | const stdin_pipe = if (options.stdin == .pipe) try posix.pipe2(pipe_flags) else undefined; | ||
| 12759 | errdefer if (options.stdin == .pipe) { | ||
| 12760 | destroyPipe(stdin_pipe); | ||
| 12761 | }; | ||
| 12762 | |||
| 12763 | const stdout_pipe = if (options.stdout == .pipe) try posix.pipe2(pipe_flags) else undefined; | ||
| 12764 | errdefer if (options.stdout == .pipe) { | ||
| 12765 | destroyPipe(stdout_pipe); | ||
| 12766 | }; | ||
| 12767 | |||
| 12768 | const stderr_pipe = if (options.stderr == .pipe) try posix.pipe2(pipe_flags) else undefined; | ||
| 12769 | errdefer if (options.stderr == .pipe) { | ||
| 12770 | destroyPipe(stderr_pipe); | ||
| 12771 | }; | ||
| 12772 | |||
| 12773 | const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore); | ||
| 12774 | const dev_null_fd = if (any_ignore) | ||
| 12775 | posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) { | ||
| 12776 | error.PathAlreadyExists => unreachable, | ||
| 12777 | error.NoSpaceLeft => unreachable, | ||
| 12778 | error.FileTooBig => unreachable, | ||
| 12779 | error.DeviceBusy => unreachable, | ||
| 12780 | error.FileLocksUnsupported => unreachable, | ||
| 12781 | error.BadPathName => unreachable, // Windows-only | ||
| 12782 | error.WouldBlock => unreachable, | ||
| 12783 | error.NetworkNotFound => unreachable, // Windows-only | ||
| 12784 | error.Canceled => unreachable, // temporarily in the posix error set | ||
| 12785 | error.SharingViolation => unreachable, // Windows-only | ||
| 12786 | error.PipeBusy => unreachable, // not a pipe | ||
| 12787 | error.AntivirusInterference => unreachable, // Windows-only | ||
| 12788 | else => |e| return e, | ||
| 12789 | } | ||
| 12790 | else | ||
| 12791 | undefined; | ||
| 12792 | defer { | ||
| 12793 | if (any_ignore) posix.close(dev_null_fd); | ||
| 12794 | } | ||
| 12795 | |||
| 12796 | const prog_pipe: [2]posix.fd_t = p: { | ||
| 12797 | if (options.progress_node.index == .none) { | ||
| 12798 | break :p .{ -1, -1 }; | ||
| 12799 | } else { | ||
| 12800 | // We use CLOEXEC for the same reason as in `pipe_flags`. | ||
| 12801 | break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); | ||
| 12802 | } | ||
| 12803 | }; | ||
| 12804 | errdefer destroyPipe(prog_pipe); | ||
| 12805 | |||
| 12806 | var arena_allocator = std.heap.ArenaAllocator.init(t.allocator); | ||
| 12807 | defer arena_allocator.deinit(); | ||
| 12808 | const arena = arena_allocator.allocator(); | ||
| 12809 | |||
| 12810 | // The POSIX standard does not allow malloc() between fork() and execve(), | ||
| 12811 | // and this allocator may be a libc allocator. | ||
| 12812 | // I have personally observed the child process deadlocking when it tries | ||
| 12813 | // to call malloc() due to a heap allocation between fork() and execve(), | ||
| 12814 | // in musl v1.1.24. | ||
| 12815 | // Additionally, we want to reduce the number of possible ways things | ||
| 12816 | // can fail between fork() and execve(). | ||
| 12817 | // Therefore, we do all the allocation for the execve() before the fork(). | ||
| 12818 | // This means we must do the null-termination of argv and env vars here. | ||
| 12819 | const argv_buf = try arena.allocSentinel(?[*:0]const u8, options.argv.len, null); | ||
| 12820 | for (options.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; | ||
| 12821 | |||
| 12822 | const prog_fileno = 3; | ||
| 12823 | comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno); | ||
| 12824 | |||
| 12825 | const envp: [*:null]const ?[*:0]const u8 = m: { | ||
| 12826 | const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno; | ||
| 12827 | if (options.env_map) |env_map| { | ||
| 12828 | break :m (try env_map.createBlock(arena, .{ | ||
| 12829 | .zig_progress_fd = prog_fd, | ||
| 12830 | })).ptr; | ||
| 12831 | } | ||
| 12832 | break :m (try process.Environ.createBlock(.{ .block = t.environ.block }, arena, .{ | ||
| 12833 | .zig_progress_fd = prog_fd, | ||
| 12834 | })).ptr; | ||
| 12835 | }; | ||
| 12836 | |||
| 12837 | // This pipe communicates to the parent errors in the child between `fork` and `execvpe`. | ||
| 12838 | // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds. | ||
| 12839 | const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true }); | ||
| 12840 | errdefer destroyPipe(err_pipe); | ||
| 12841 | |||
| 12842 | t.scanEnviron(); // for PATH | ||
| 12843 | const PATH = t.environ.string.PATH orelse "/usr/local/bin:/bin/:/usr/bin"; | ||
| 12844 | |||
| 12845 | const pid_result = try posix.fork(); | ||
| 12846 | if (pid_result == 0) { | ||
| 12847 | // we are the child | ||
| 12848 | setUpChildIo(options.stdin, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err); | ||
| 12849 | setUpChildIo(options.stdout, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err); | ||
| 12850 | setUpChildIo(options.stderr, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err); | ||
| 12851 | |||
| 12852 | if (options.cwd_dir) |cwd| { | ||
| 12853 | posix.fchdir(cwd.handle) catch |err| forkBail(err_pipe[1], err); | ||
| 12854 | } else if (options.cwd) |cwd| { | ||
| 12855 | posix.chdir(cwd) catch |err| forkBail(err_pipe[1], err); | ||
| 12856 | } | ||
| 12857 | |||
| 12858 | // Must happen after fchdir above, the cwd file descriptor might be | ||
| 12859 | // equal to prog_fileno and be clobbered by this dup2 call. | ||
| 12860 | if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkBail(err_pipe[1], err); | ||
| 12861 | |||
| 12862 | if (options.gid) |gid| { | ||
| 12863 | posix.setregid(gid, gid) catch |err| forkBail(err_pipe[1], err); | ||
| 12864 | } | ||
| 12865 | |||
| 12866 | if (options.uid) |uid| { | ||
| 12867 | switch (posix.errno(posix.system.setreuid(uid, uid))) { | ||
| 12868 | .SUCCESS => {}, | ||
| 12869 | .AGAIN => forkBail(err_pipe[1], error.ResourceLimitReached), | ||
| 12870 | .INVAL => forkBail(err_pipe[1], error.InvalidUserId), | ||
| 12871 | .PERM => forkBail(err_pipe[1], error.PermissionDenied), | ||
| 12872 | else => forkBail(err_pipe[1], error.Unexpected), | ||
| 12873 | } | ||
| 12874 | } | ||
| 12875 | |||
| 12876 | if (options.pgid) |pid| { | ||
| 12877 | switch (posix.errno(posix.system.setpgid(0, pid))) { | ||
| 12878 | .SUCCESS => {}, | ||
| 12879 | .ACCES => forkBail(err_pipe[1], error.ProcessAlreadyExec), | ||
| 12880 | .INVAL => forkBail(err_pipe[1], error.InvalidProcessGroupId), | ||
| 12881 | .PERM => forkBail(err_pipe[1], error.PermissionDenied), | ||
| 12882 | else => forkBail(err_pipe[1], error.Unexpected), | ||
| 12883 | } | ||
| 12884 | } | ||
| 12885 | |||
| 12886 | if (options.start_suspended) { | ||
| 12887 | switch (posix.errno(posix.system.kill(posix.system.getpid(), .STOP))) { | ||
| 12888 | .SUCCESS => {}, | ||
| 12889 | .PERM => forkBail(err_pipe[1], error.PermissionDenied), | ||
| 12890 | else => forkBail(err_pipe[1], error.Unexpected), | ||
| 12891 | } | ||
| 12892 | } | ||
| 12893 | |||
| 12894 | const err = execvpeZ_expandArg0(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, envp, PATH); | ||
| 12895 | forkBail(err_pipe[1], err); | ||
| 12896 | } | ||
| 12897 | |||
| 12898 | const pid: posix.pid_t = @intCast(pid_result); // We are the parent. | ||
| 12899 | |||
| 12900 | posix.close(err_pipe[1]); // make sure only the child holds the write end open | ||
| 12901 | defer posix.close(err_pipe[0]); | ||
| 12902 | |||
| 12903 | if (options.stdin == .pipe) posix.close(stdin_pipe[0]); | ||
| 12904 | if (options.stdout == .pipe) posix.close(stdout_pipe[1]); | ||
| 12905 | if (options.stderr == .pipe) posix.close(stderr_pipe[1]); | ||
| 12906 | |||
| 12907 | if (prog_pipe[1] != -1) posix.close(prog_pipe[1]); | ||
| 12908 | |||
| 12909 | options.progress_node.setIpcFd(prog_pipe[0]); | ||
| 12910 | |||
| 12911 | // Wait for the child to report any errors in or before `execvpe`. | ||
| 12912 | if (readIntFd(t, err_pipe[0])) |child_err_int| { | ||
| 12913 | const child_err: process.SpawnError = @errorCast(@errorFromInt(child_err_int)); | ||
| 12914 | return child_err; | ||
| 12915 | } else |read_err| switch (read_err) { | ||
| 12916 | error.EndOfStream => { | ||
| 12917 | // Write end closed by CLOEXEC at the time of the `execvpe` call, | ||
| 12918 | // indicating success. | ||
| 12919 | }, | ||
| 12920 | else => { | ||
| 12921 | // Problem reading the error from the error reporting pipe. We | ||
| 12922 | // don't know if the child is alive or dead. Better to assume it is | ||
| 12923 | // alive so the resource does not risk being leaked. | ||
| 12924 | }, | ||
| 12925 | } | ||
| 12926 | |||
| 12927 | return .{ | ||
| 12928 | .id = pid, | ||
| 12929 | .stdin = switch (options.stdin) { | ||
| 12930 | .pipe => .{ .handle = stdin_pipe[1] }, | ||
| 12931 | else => null, | ||
| 12932 | }, | ||
| 12933 | .stdout = switch (options.stdout) { | ||
| 12934 | .pipe => .{ .handle = stdout_pipe[0] }, | ||
| 12935 | else => null, | ||
| 12936 | }, | ||
| 12937 | .stderr = switch (options.stderr) { | ||
| 12938 | .pipe => .{ .handle = stderr_pipe[0] }, | ||
| 12939 | else => null, | ||
| 12940 | }, | ||
| 12941 | .request_resource_usage_statistics = options.request_resource_usage_statistics, | ||
| 12942 | }; | ||
| 12943 | } | ||
| 12944 | |||
| 12945 | fn childWait(userdata: ?*anyopaque, child: *std.process.Child) process.Child.WaitError!process.Child.Term { | ||
| 12946 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | ||
| 12947 | switch (native_os) { | ||
| 12948 | .windows => return childWaitWindows(t, child), | ||
| 12949 | else => return childWaitPosix(t, child), | ||
| 12950 | } | ||
| 12951 | } | ||
| 12952 | |||
| 12953 | fn childKill(userdata: ?*anyopaque, child: *std.process.Child) void { | ||
| 12954 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | ||
| 12955 | if (is_windows) { | ||
| 12956 | childKillWindows(t, child, 1) catch { | ||
| 12957 | childCleanupStreams(child); | ||
| 12958 | child.id = null; | ||
| 12959 | }; | ||
| 12960 | } else { | ||
| 12961 | childKillPosix(t, child) catch { | ||
| 12962 | childCleanupStreams(child); | ||
| 12963 | child.id = null; | ||
| 12964 | }; | ||
| 12965 | } | ||
| 12966 | } | ||
| 12967 | |||
| 12968 | fn childKillWindows(t: *Threaded, child: *process.Child, exit_code: windows.UINT) !void { | ||
| 12969 | windows.TerminateProcess(child.id, exit_code) catch |err| switch (err) { | ||
| 12970 | error.AccessDenied => { | ||
| 12971 | // Usually when TerminateProcess triggers a ACCESS_DENIED error, it | ||
| 12972 | // indicates that the process has already exited, but there may be | ||
| 12973 | // some rare edge cases where our process handle no longer has the | ||
| 12974 | // PROCESS_TERMINATE access right, so let's do another check to make | ||
| 12975 | // sure the process is really no longer running: | ||
| 12976 | windows.WaitForSingleObjectEx(child.id, 0, false) catch return err; | ||
| 12977 | return error.AlreadyTerminated; | ||
| 12978 | }, | ||
| 12979 | else => return err, | ||
| 12980 | }; | ||
| 12981 | try childWaitWindows(t, child); | ||
| 12982 | } | ||
| 12983 | |||
| 12984 | fn childWaitWindows(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term { | ||
| 12985 | _ = t; // TODO cancelation | ||
| 12986 | windows.WaitForSingleObjectEx(child.id, windows.INFINITE, false); | ||
| 12987 | |||
| 12988 | const term: process.Child.Term = x: { | ||
| 12989 | var exit_code: windows.DWORD = undefined; | ||
| 12990 | if (windows.kernel32.GetExitCodeProcess(child.id, &exit_code) == 0) { | ||
| 12991 | break :x .{ .unknown = 0 }; | ||
| 12992 | } else { | ||
| 12993 | break :x .{ .exited = @as(u8, @truncate(exit_code)) }; | ||
| 12994 | } | ||
| 12995 | }; | ||
| 12996 | |||
| 12997 | if (child.request_resource_usage_statistics) { | ||
| 12998 | child.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(child.id); | ||
| 12999 | } | ||
| 13000 | |||
| 13001 | posix.close(child.id); | ||
| 13002 | posix.close(child.thread_handle); | ||
| 13003 | childCleanupStreams(child); | ||
| 13004 | child.id = null; | ||
| 13005 | return term; | ||
| 13006 | } | ||
| 13007 | |||
| 13008 | fn childWaitPosix(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term { | ||
| 13009 | _ = t; // TODO cancelation | ||
| 13010 | const pid = child.id.?; | ||
| 13011 | const res: posix.WaitPidResult = res: { | ||
| 13012 | if (child.request_resource_usage_statistics and have_wait4) { | ||
| 13013 | var ru: posix.rusage = undefined; | ||
| 13014 | const res = posix.wait4(pid, 0, &ru); | ||
| 13015 | child.resource_usage_statistics.rusage = ru; | ||
| 13016 | break :res res; | ||
| 13017 | } | ||
| 13018 | break :res posix.waitpid(pid, 0); | ||
| 13019 | }; | ||
| 13020 | const status = res.status; | ||
| 13021 | childCleanupStreams(child); | ||
| 13022 | child.id = null; | ||
| 13023 | return statusToTerm(status); | ||
| 13024 | } | ||
| 13025 | |||
| 13026 | fn statusToTerm(status: u32) process.Child.Term { | ||
| 13027 | return if (posix.W.IFEXITED(status)) | ||
| 13028 | .{ .exited = posix.W.EXITSTATUS(status) } | ||
| 13029 | else if (posix.W.IFSIGNALED(status)) | ||
| 13030 | .{ .signal = posix.W.TERMSIG(status) } | ||
| 13031 | else if (posix.W.IFSTOPPED(status)) | ||
| 13032 | .{ .stopped = posix.W.STOPSIG(status) } | ||
| 13033 | else | ||
| 13034 | .{ .unknown = status }; | ||
| 13035 | } | ||
| 13036 | |||
| 13037 | fn childKillPosix(t: *Threaded, child: *process.Child) !void { | ||
| 13038 | try posix.kill(child.id.?, posix.SIG.TERM); | ||
| 13039 | _ = try childWaitPosix(t, child); | ||
| 13040 | } | ||
| 13041 | |||
| 13042 | fn childCleanupStreams(child: *process.Child) void { | ||
| 13043 | if (child.stdin) |*stdin| { | ||
| 13044 | posix.close(stdin.handle); | ||
| 13045 | child.stdin = null; | ||
| 13046 | } | ||
| 13047 | if (child.stdout) |*stdout| { | ||
| 13048 | posix.close(stdout.handle); | ||
| 13049 | child.stdout = null; | ||
| 13050 | } | ||
| 13051 | if (child.stderr) |*stderr| { | ||
| 13052 | posix.close(stderr.handle); | ||
| 13053 | child.stderr = null; | ||
| 13054 | } | ||
| 13055 | } | ||
| 13056 | |||
| 13057 | /// Errors that can occur between fork() and execv() | ||
| 13058 | const ForkBailError = process.SpawnError || process.ReplaceError; | ||
| 13059 | |||
| 13060 | /// Child of fork calls this to report an error to the fork parent. Then the | ||
| 13061 | /// child exits. | ||
| 13062 | fn forkBail(fd: posix.fd_t, err: ForkBailError) noreturn { | ||
| 13063 | writeIntFd(fd, @as(ErrInt, @intFromError(err))) catch {}; | ||
| 13064 | // If we're linking libc, some naughty applications may have registered atexit handlers | ||
| 13065 | // which we really do not want to run in the fork child. I caught LLVM doing this and | ||
| 13066 | // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne, | ||
| 13067 | // "Why'd you have to go and make things so complicated?" | ||
| 13068 | if (builtin.link_libc) { | ||
| 13069 | // The _exit(2) function does nothing but make the exit syscall, unlike exit(3) | ||
| 13070 | std.c._exit(1); | ||
| 13071 | } | ||
| 13072 | posix.system.exit(1); | ||
| 13073 | } | ||
| 13074 | |||
| 13075 | fn writeIntFd(fd: posix.fd_t, value: ErrInt) !void { | ||
| 13076 | var buffer: [8]u8 = undefined; | ||
| 13077 | std.mem.writeInt(u64, &buffer, value, .little); | ||
| 13078 | // Skip the cancel mechanism. | ||
| 13079 | var i: usize = 0; | ||
| 13080 | while (true) { | ||
| 13081 | const rc = posix.system.write(fd, buffer[i..].ptr, buffer.len - i); | ||
| 13082 | switch (posix.errno(rc)) { | ||
| 13083 | .SUCCESS => { | ||
| 13084 | const n: usize = @intCast(rc); | ||
| 13085 | i += n; | ||
| 13086 | if (buffer.len - i == 0) return; | ||
| 13087 | }, | ||
| 13088 | .INTR => continue, | ||
| 13089 | else => return error.SystemResources, | ||
| 13090 | } | ||
| 13091 | } | ||
| 13092 | } | ||
| 13093 | |||
| 13094 | fn readIntFd(t: *Threaded, fd: posix.fd_t) !ErrInt { | ||
| 13095 | _ = t; // TODO cancelation | ||
| 13096 | var buffer: [8]u8 = undefined; | ||
| 13097 | var i: usize = 0; | ||
| 13098 | while (true) { | ||
| 13099 | const rc = posix.system.read(fd, buffer[i..].ptr, buffer.len - i); | ||
| 13100 | switch (posix.errno(rc)) { | ||
| 13101 | .SUCCESS => { | ||
| 13102 | const n: usize = @intCast(rc); | ||
| 13103 | if (n == 0) return error.EndOfStream; | ||
| 13104 | i += n; | ||
| 13105 | continue; | ||
| 13106 | }, | ||
| 13107 | .INTR => continue, | ||
| 13108 | else => |err| return posix.unexpectedErrno(err), | ||
| 13109 | } | ||
| 13110 | } | ||
| 13111 | return @intCast(std.mem.readInt(u64, &buffer, .little)); | ||
| 13112 | } | ||
| 13113 | |||
| 13114 | const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); | ||
| 13115 | |||
| 13116 | fn destroyPipe(pipe: [2]posix.fd_t) void { | ||
| 13117 | if (pipe[0] != -1) posix.close(pipe[0]); | ||
| 13118 | if (pipe[0] != pipe[1]) posix.close(pipe[1]); | ||
| 13119 | } | ||
| 13120 | |||
| 13121 | fn setUpChildIo(stdio: process.SpawnOptions.StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { | ||
| 13122 | switch (stdio) { | ||
| 13123 | .pipe => try posix.dup2(pipe_fd, std_fileno), | ||
| 13124 | .close => posix.close(std_fileno), | ||
| 13125 | .inherit => {}, | ||
| 13126 | .ignore => try posix.dup2(dev_null_fd, std_fileno), | ||
| 13127 | .file => @panic("TODO implement setUpChildIo when file is used"), | ||
| 13128 | } | ||
| 13129 | } | ||
| 13130 | |||
| 13131 | fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.SpawnError!void { | ||
| 13132 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | ||
| 13133 | _ = t; | ||
| 13134 | |||
| 13135 | var saAttr: windows.SECURITY_ATTRIBUTES = .{ | ||
| 13136 | .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), | ||
| 13137 | .bInheritHandle = windows.TRUE, | ||
| 13138 | .lpSecurityDescriptor = null, | ||
| 13139 | }; | ||
| 13140 | |||
| 13141 | const any_ignore = | ||
| 13142 | child.stdin_behavior == .ignore or | ||
| 13143 | child.stdout_behavior == .ignore or | ||
| 13144 | child.stderr_behavior == .ignore; | ||
| 13145 | |||
| 13146 | const nul_handle = if (any_ignore) | ||
| 13147 | // "\Device\Null" or "\??\NUL" | ||
| 13148 | windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{ | ||
| 13149 | .access_mask = .{ | ||
| 13150 | .STANDARD = .{ .SYNCHRONIZE = true }, | ||
| 13151 | .GENERIC = .{ .WRITE = true, .READ = true }, | ||
| 13152 | }, | ||
| 13153 | .sa = &saAttr, | ||
| 13154 | .creation = .OPEN, | ||
| 13155 | }) catch |err| switch (err) { | ||
| 13156 | error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" | ||
| 13157 | error.PipeBusy => return error.Unexpected, // not possible for "NUL" | ||
| 13158 | error.NoDevice => return error.Unexpected, // not possible for "NUL" | ||
| 13159 | error.FileNotFound => return error.Unexpected, // not possible for "NUL" | ||
| 13160 | error.AccessDenied => return error.Unexpected, // not possible for "NUL" | ||
| 13161 | error.NameTooLong => return error.Unexpected, // not possible for "NUL" | ||
| 13162 | error.WouldBlock => return error.Unexpected, // not possible for "NUL" | ||
| 13163 | error.NetworkNotFound => return error.Unexpected, // not possible for "NUL" | ||
| 13164 | error.AntivirusInterference => return error.Unexpected, // not possible for "NUL" | ||
| 13165 | error.OperationCanceled => return error.Unexpected, // we're not canceling the operation | ||
| 13166 | else => |e| return e, | ||
| 13167 | } | ||
| 13168 | else | ||
| 13169 | undefined; | ||
| 13170 | defer { | ||
| 13171 | if (any_ignore) posix.close(nul_handle); | ||
| 13172 | } | ||
| 13173 | |||
| 13174 | var g_hChildStd_IN_Rd: ?windows.HANDLE = null; | ||
| 13175 | var g_hChildStd_IN_Wr: ?windows.HANDLE = null; | ||
| 13176 | switch (child.stdin_behavior) { | ||
| 13177 | .pipe => { | ||
| 13178 | try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); | ||
| 13179 | }, | ||
| 13180 | .ignore => { | ||
| 13181 | g_hChildStd_IN_Rd = nul_handle; | ||
| 13182 | }, | ||
| 13183 | .inherit => { | ||
| 13184 | g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; | ||
| 13185 | }, | ||
| 13186 | .close => { | ||
| 13187 | g_hChildStd_IN_Rd = null; | ||
| 13188 | }, | ||
| 13189 | } | ||
| 13190 | errdefer if (child.stdin_behavior == .pipe) { | ||
| 13191 | windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); | ||
| 13192 | }; | ||
| 13193 | |||
| 13194 | var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; | ||
| 13195 | var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; | ||
| 13196 | switch (child.stdout_behavior) { | ||
| 13197 | .pipe => { | ||
| 13198 | try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); | ||
| 13199 | }, | ||
| 13200 | .ignore => { | ||
| 13201 | g_hChildStd_OUT_Wr = nul_handle; | ||
| 13202 | }, | ||
| 13203 | .inherit => { | ||
| 13204 | g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; | ||
| 13205 | }, | ||
| 13206 | .close => { | ||
| 13207 | g_hChildStd_OUT_Wr = null; | ||
| 13208 | }, | ||
| 13209 | } | ||
| 13210 | errdefer if (child.stdout_behavior == .pipe) { | ||
| 13211 | windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); | ||
| 13212 | }; | ||
| 13213 | |||
| 13214 | var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; | ||
| 13215 | var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; | ||
| 13216 | switch (child.stderr_behavior) { | ||
| 13217 | .pipe => { | ||
| 13218 | try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); | ||
| 13219 | }, | ||
| 13220 | .ignore => { | ||
| 13221 | g_hChildStd_ERR_Wr = nul_handle; | ||
| 13222 | }, | ||
| 13223 | .inherit => { | ||
| 13224 | g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; | ||
| 13225 | }, | ||
| 13226 | .close => { | ||
| 13227 | g_hChildStd_ERR_Wr = null; | ||
| 13228 | }, | ||
| 13229 | } | ||
| 13230 | errdefer if (child.stderr_behavior == .pipe) { | ||
| 13231 | windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); | ||
| 13232 | }; | ||
| 13233 | |||
| 13234 | var siStartInfo = windows.STARTUPINFOW{ | ||
| 13235 | .cb = @sizeOf(windows.STARTUPINFOW), | ||
| 13236 | .hStdError = g_hChildStd_ERR_Wr, | ||
| 13237 | .hStdOutput = g_hChildStd_OUT_Wr, | ||
| 13238 | .hStdInput = g_hChildStd_IN_Rd, | ||
| 13239 | .dwFlags = windows.STARTF_USESTDHANDLES, | ||
| 13240 | |||
| 13241 | .lpReserved = null, | ||
| 13242 | .lpDesktop = null, | ||
| 13243 | .lpTitle = null, | ||
| 13244 | .dwX = 0, | ||
| 13245 | .dwY = 0, | ||
| 13246 | .dwXSize = 0, | ||
| 13247 | .dwYSize = 0, | ||
| 13248 | .dwXCountChars = 0, | ||
| 13249 | .dwYCountChars = 0, | ||
| 13250 | .dwFillAttribute = 0, | ||
| 13251 | .wShowWindow = 0, | ||
| 13252 | .cbReserved2 = 0, | ||
| 13253 | .lpReserved2 = null, | ||
| 13254 | }; | ||
| 13255 | var piProcInfo: windows.PROCESS_INFORMATION = undefined; | ||
| 13256 | |||
| 13257 | const cwd_w = if (child.cwd) |cwd| try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd) else null; | ||
| 13258 | defer if (cwd_w) |cwd| child.allocator.free(cwd); | ||
| 13259 | const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; | ||
| 13260 | |||
| 13261 | const maybe_envp_buf = if (child.env_map) |env_map| try process.createWindowsEnvBlock(child.allocator, env_map) else null; | ||
| 13262 | defer if (maybe_envp_buf) |envp_buf| child.allocator.free(envp_buf); | ||
| 13263 | const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; | ||
| 13264 | |||
| 13265 | const app_name_wtf8 = child.argv[0]; | ||
| 13266 | const app_name_is_absolute = Dir.path.isAbsolute(app_name_wtf8); | ||
| 13267 | |||
| 13268 | // the cwd set in Child is in effect when choosing the executable path | ||
| 13269 | // to match posix semantics | ||
| 13270 | var cwd_path_w_needs_free = false; | ||
| 13271 | const cwd_path_w = x: { | ||
| 13272 | // If the app name is absolute, then we need to use its dirname as the cwd | ||
| 13273 | if (app_name_is_absolute) { | ||
| 13274 | cwd_path_w_needs_free = true; | ||
| 13275 | const dir = Dir.path.dirname(app_name_wtf8).?; | ||
| 13276 | break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, dir); | ||
| 13277 | } else if (child.cwd) |cwd| { | ||
| 13278 | cwd_path_w_needs_free = true; | ||
| 13279 | break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd); | ||
| 13280 | } else { | ||
| 13281 | break :x &[_:0]u16{}; // empty for cwd | ||
| 13282 | } | ||
| 13283 | }; | ||
| 13284 | defer if (cwd_path_w_needs_free) child.allocator.free(cwd_path_w); | ||
| 13285 | |||
| 13286 | // If the app name has more than just a filename, then we need to separate that | ||
| 13287 | // into the basename and dirname and use the dirname as an addition to the cwd | ||
| 13288 | // path. This is because NtQueryDirectoryFile cannot accept FileName params with | ||
| 13289 | // path separators. | ||
| 13290 | const app_basename_wtf8 = Dir.path.basename(app_name_wtf8); | ||
| 13291 | // If the app name is absolute, then the cwd will already have the app's dirname in it, | ||
| 13292 | // so only populate app_dirname if app name is a relative path with > 0 path separators. | ||
| 13293 | const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) Dir.path.dirname(app_name_wtf8) else null; | ||
| 13294 | const app_dirname_w: ?[:0]u16 = x: { | ||
| 13295 | if (maybe_app_dirname_wtf8) |app_dirname_wtf8| { | ||
| 13296 | break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_dirname_wtf8); | ||
| 13297 | } | ||
| 13298 | break :x null; | ||
| 13299 | }; | ||
| 13300 | defer if (app_dirname_w != null) child.allocator.free(app_dirname_w.?); | ||
| 13301 | |||
| 13302 | const app_name_w = try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_basename_wtf8); | ||
| 13303 | defer child.allocator.free(app_name_w); | ||
| 13304 | |||
| 13305 | const flags: windows.CreateProcessFlags = .{ | ||
| 13306 | .create_suspended = child.start_suspended, | ||
| 13307 | .create_unicode_environment = true, | ||
| 13308 | .create_no_window = child.create_no_window, | ||
| 13309 | }; | ||
| 13310 | |||
| 13311 | run: { | ||
| 13312 | const PATH: [:0]const u16 = process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; | ||
| 13313 | const PATHEXT: [:0]const u16 = process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; | ||
| 13314 | |||
| 13315 | // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules | ||
| 13316 | // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously | ||
| 13317 | // constructed arguments. | ||
| 13318 | // | ||
| 13319 | // We'll need to wait until we're actually trying to run the command to know for sure | ||
| 13320 | // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually | ||
| 13321 | // serializing the command line until we determine how it should be serialized. | ||
| 13322 | var cmd_line_cache = WindowsCommandLineCache.init(child.allocator, child.argv); | ||
| 13323 | defer cmd_line_cache.deinit(); | ||
| 13324 | |||
| 13325 | var app_buf: std.ArrayList(u16) = .empty; | ||
| 13326 | defer app_buf.deinit(child.allocator); | ||
| 13327 | |||
| 13328 | try app_buf.appendSlice(child.allocator, app_name_w); | ||
| 13329 | |||
| 13330 | var dir_buf: std.ArrayList(u16) = .empty; | ||
| 13331 | defer dir_buf.deinit(child.allocator); | ||
| 13332 | |||
| 13333 | if (cwd_path_w.len > 0) { | ||
| 13334 | try dir_buf.appendSlice(child.allocator, cwd_path_w); | ||
| 13335 | } | ||
| 13336 | if (app_dirname_w) |app_dir| { | ||
| 13337 | if (dir_buf.items.len > 0) try dir_buf.append(child.allocator, Dir.path.sep); | ||
| 13338 | try dir_buf.appendSlice(child.allocator, app_dir); | ||
| 13339 | } | ||
| 13340 | |||
| 13341 | windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo) catch |no_path_err| { | ||
| 13342 | const original_err = switch (no_path_err) { | ||
| 13343 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | ||
| 13344 | error.InvalidArg0 => return error.FileNotFound, | ||
| 13345 | error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, | ||
| 13346 | error.UnrecoverableInvalidExe => return error.InvalidExe, | ||
| 13347 | else => |e| return e, | ||
| 13348 | }; | ||
| 13349 | |||
| 13350 | // If the app name had path separators, that disallows PATH searching, | ||
| 13351 | // and there's no need to search the PATH if the app name is absolute. | ||
| 13352 | // We still search the path if the cwd is absolute because of the | ||
| 13353 | // "cwd set in Child is in effect when choosing the executable path | ||
| 13354 | // to match posix semantics" behavior--we don't want to skip searching | ||
| 13355 | // the PATH just because we were trying to set the cwd of the child process. | ||
| 13356 | if (app_dirname_w != null or app_name_is_absolute) { | ||
| 13357 | return original_err; | ||
| 13358 | } | ||
| 13359 | |||
| 13360 | var it = std.mem.tokenizeScalar(u16, PATH, ';'); | ||
| 13361 | while (it.next()) |search_path| { | ||
| 13362 | dir_buf.clearRetainingCapacity(); | ||
| 13363 | try dir_buf.appendSlice(child.allocator, search_path); | ||
| 13364 | |||
| 13365 | if (windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo)) { | ||
| 13366 | break :run; | ||
| 13367 | } else |err| switch (err) { | ||
| 13368 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | ||
| 13369 | error.InvalidArg0 => return error.FileNotFound, | ||
| 13370 | error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, | ||
| 13371 | error.UnrecoverableInvalidExe => return error.InvalidExe, | ||
| 13372 | else => |e| return e, | ||
| 13373 | } | ||
| 13374 | } else { | ||
| 13375 | return original_err; | ||
| 13376 | } | ||
| 13377 | }; | ||
| 13378 | } | ||
| 13379 | |||
| 13380 | if (g_hChildStd_IN_Wr) |h| { | ||
| 13381 | child.stdin = File{ .handle = h }; | ||
| 13382 | } else { | ||
| 13383 | child.stdin = null; | ||
| 13384 | } | ||
| 13385 | if (g_hChildStd_OUT_Rd) |h| { | ||
| 13386 | child.stdout = File{ .handle = h }; | ||
| 13387 | } else { | ||
| 13388 | child.stdout = null; | ||
| 13389 | } | ||
| 13390 | if (g_hChildStd_ERR_Rd) |h| { | ||
| 13391 | child.stderr = File{ .handle = h }; | ||
| 13392 | } else { | ||
| 13393 | child.stderr = null; | ||
| 13394 | } | ||
| 13395 | |||
| 13396 | child.id = piProcInfo.hProcess; | ||
| 13397 | child.thread_handle = piProcInfo.hThread; | ||
| 13398 | child.term = null; | ||
| 13399 | |||
| 13400 | if (child.stdin_behavior == .pipe) { | ||
| 13401 | posix.close(g_hChildStd_IN_Rd.?); | ||
| 13402 | } | ||
| 13403 | if (child.stderr_behavior == .pipe) { | ||
| 13404 | posix.close(g_hChildStd_ERR_Wr.?); | ||
| 13405 | } | ||
| 13406 | if (child.stdout_behavior == .pipe) { | ||
| 13407 | posix.close(g_hChildStd_OUT_Wr.?); | ||
| 13408 | } | ||
| 13409 | } | ||
| 13410 | |||
| 13411 | /// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. | ||
| 13412 | /// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. | ||
| 13413 | /// Note: `app_buf` should not contain any leading path separators. | ||
| 13414 | /// Note: If the dir is the cwd, dir_buf should be empty (len = 0). | ||
| 13415 | fn windowsCreateProcessPathExt( | ||
| 13416 | allocator: Allocator, | ||
| 13417 | dir_buf: *std.ArrayList(u16), | ||
| 13418 | app_buf: *std.ArrayList(u16), | ||
| 13419 | pathext: [:0]const u16, | ||
| 13420 | cmd_line_cache: *WindowsCommandLineCache, | ||
| 13421 | envp_ptr: ?[*]u16, | ||
| 13422 | cwd_ptr: ?[*:0]u16, | ||
| 13423 | flags: windows.CreateProcessFlags, | ||
| 13424 | lpStartupInfo: *windows.STARTUPINFOW, | ||
| 13425 | lpProcessInformation: *windows.PROCESS_INFORMATION, | ||
| 13426 | ) !void { | ||
| 13427 | const app_name_len = app_buf.items.len; | ||
| 13428 | const dir_path_len = dir_buf.items.len; | ||
| 13429 | |||
| 13430 | if (app_name_len == 0) return error.FileNotFound; | ||
| 13431 | |||
| 13432 | defer app_buf.shrinkRetainingCapacity(app_name_len); | ||
| 13433 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | ||
| 13434 | |||
| 13435 | // The name of the game here is to avoid CreateProcessW calls at all costs, | ||
| 13436 | // and only ever try calling it when we have a real candidate for execution. | ||
| 13437 | // Secondarily, we want to minimize the number of syscalls used when checking | ||
| 13438 | // for each PATHEXT-appended version of the app name. | ||
| 13439 | // | ||
| 13440 | // An overview of the technique used: | ||
| 13441 | // - Open the search directory for iteration (either cwd or a path from PATH) | ||
| 13442 | // - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to | ||
| 13443 | // check if anything that could possibly match either the unappended version | ||
| 13444 | // of the app name or any of the versions with a PATHEXT value appended exists. | ||
| 13445 | // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early | ||
| 13446 | // without needing to use PATHEXT at all. | ||
| 13447 | // | ||
| 13448 | // This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence | ||
| 13449 | // for any directory that doesn't contain any possible matches, instead of having | ||
| 13450 | // to use a separate look up for each individual filename combination (unappended + | ||
| 13451 | // each PATHEXT appended). For directories where the wildcard *does* match something, | ||
| 13452 | // we iterate the matches and take note of any that are either the unappended version, | ||
| 13453 | // or a version with a supported PATHEXT appended. We then try calling CreateProcessW | ||
| 13454 | // with the found versions in the appropriate order. | ||
| 13455 | |||
| 13456 | // In the future, child process execution needs to move to Io implementation. | ||
| 13457 | // Under those conditions, here we will have access to lower level directory | ||
| 13458 | // opening function knowing which implementation we are in. Here, we imitate | ||
| 13459 | // that scenario. | ||
| 13460 | var dir = dir: { | ||
| 13461 | // needs to be null-terminated | ||
| 13462 | try dir_buf.append(allocator, 0); | ||
| 13463 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | ||
| 13464 | const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | ||
| 13465 | const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); | ||
| 13466 | break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{ | ||
| 13467 | .iterate = true, | ||
| 13468 | }) catch return error.FileNotFound; | ||
| 13469 | }; | ||
| 13470 | defer windows.CloseHandle(dir.handle); | ||
| 13471 | |||
| 13472 | // Add wildcard and null-terminator | ||
| 13473 | try app_buf.append(allocator, '*'); | ||
| 13474 | try app_buf.append(allocator, 0); | ||
| 13475 | const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; | ||
| 13476 | |||
| 13477 | // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries | ||
| 13478 | // returned per NtQueryDirectoryFile call. | ||
| 13479 | var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined; | ||
| 13480 | const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); | ||
| 13481 | if (file_information_buf.len < file_info_maximum_single_entry_size) { | ||
| 13482 | @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); | ||
| 13483 | } | ||
| 13484 | var io_status: windows.IO_STATUS_BLOCK = undefined; | ||
| 13485 | |||
| 13486 | const num_supported_pathext = @typeInfo(process.WindowsExtension).@"enum".fields.len; | ||
| 13487 | var pathext_seen = [_]bool{false} ** num_supported_pathext; | ||
| 13488 | var any_pathext_seen = false; | ||
| 13489 | var unappended_exists = false; | ||
| 13490 | |||
| 13491 | // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions | ||
| 13492 | // of the app_name we should try to spawn. | ||
| 13493 | // Note: This is necessary because the order of the files returned is filesystem-dependent: | ||
| 13494 | // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. | ||
| 13495 | // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. | ||
| 13496 | while (true) { | ||
| 13497 | const app_name_len_bytes = std.math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; | ||
| 13498 | var app_name_unicode_string = windows.UNICODE_STRING{ | ||
| 13499 | .Length = app_name_len_bytes, | ||
| 13500 | .MaximumLength = app_name_len_bytes, | ||
| 13501 | .Buffer = @constCast(app_name_wildcard.ptr), | ||
| 13502 | }; | ||
| 13503 | const rc = windows.ntdll.NtQueryDirectoryFile( | ||
| 13504 | dir.handle, | ||
| 13505 | null, | ||
| 13506 | null, | ||
| 13507 | null, | ||
| 13508 | &io_status, | ||
| 13509 | &file_information_buf, | ||
| 13510 | file_information_buf.len, | ||
| 13511 | .Directory, | ||
| 13512 | windows.FALSE, // single result | ||
| 13513 | &app_name_unicode_string, | ||
| 13514 | windows.FALSE, // restart iteration | ||
| 13515 | ); | ||
| 13516 | |||
| 13517 | // If we get nothing with the wildcard, then we can just bail out | ||
| 13518 | // as we know appending PATHEXT will not yield anything. | ||
| 13519 | switch (rc) { | ||
| 13520 | .SUCCESS => {}, | ||
| 13521 | .NO_SUCH_FILE => return error.FileNotFound, | ||
| 13522 | .NO_MORE_FILES => break, | ||
| 13523 | .ACCESS_DENIED => return error.AccessDenied, | ||
| 13524 | else => return windows.unexpectedStatus(rc), | ||
| 13525 | } | ||
| 13526 | |||
| 13527 | // According to the docs, this can only happen if there is not enough room in the | ||
| 13528 | // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. | ||
| 13529 | // Therefore, this condition should not be possible to hit with the buffer size we use. | ||
| 13530 | std.debug.assert(io_status.Information != 0); | ||
| 13531 | |||
| 13532 | var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; | ||
| 13533 | while (it.next()) |info| { | ||
| 13534 | // Skip directories | ||
| 13535 | if (info.FileAttributes.DIRECTORY) continue; | ||
| 13536 | const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; | ||
| 13537 | // Because all results start with the app_name since we're using the wildcard `app_name*`, | ||
| 13538 | // if the length is equal to app_name then this is an exact match | ||
| 13539 | if (filename.len == app_name_len) { | ||
| 13540 | // Note: We can't break early here because it's possible that the unappended version | ||
| 13541 | // fails to spawn, in which case we still want to try the PATHEXT appended versions. | ||
| 13542 | unappended_exists = true; | ||
| 13543 | } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { | ||
| 13544 | pathext_seen[@intFromEnum(pathext_ext)] = true; | ||
| 13545 | any_pathext_seen = true; | ||
| 13546 | } | ||
| 13547 | } | ||
| 13548 | } | ||
| 13549 | |||
| 13550 | const unappended_err = unappended: { | ||
| 13551 | if (unappended_exists) { | ||
| 13552 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | ||
| 13553 | '/', '\\' => {}, | ||
| 13554 | else => try dir_buf.append(allocator, Dir.path.sep), | ||
| 13555 | }; | ||
| 13556 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | ||
| 13557 | try dir_buf.append(allocator, 0); | ||
| 13558 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | ||
| 13559 | |||
| 13560 | const is_bat_or_cmd = bat_or_cmd: { | ||
| 13561 | const app_name = app_buf.items[0..app_name_len]; | ||
| 13562 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false; | ||
| 13563 | const ext = app_name[ext_start..]; | ||
| 13564 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false; | ||
| 13565 | switch (ext_enum) { | ||
| 13566 | .cmd, .bat => break :bat_or_cmd true, | ||
| 13567 | else => break :bat_or_cmd false, | ||
| 13568 | } | ||
| 13569 | }; | ||
| 13570 | const cmd_line_w = if (is_bat_or_cmd) | ||
| 13571 | try cmd_line_cache.scriptCommandLine(full_app_name) | ||
| 13572 | else | ||
| 13573 | try cmd_line_cache.commandLine(); | ||
| 13574 | const app_name_w = if (is_bat_or_cmd) | ||
| 13575 | try cmd_line_cache.cmdExePath() | ||
| 13576 | else | ||
| 13577 | full_app_name; | ||
| 13578 | |||
| 13579 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| { | ||
| 13580 | return; | ||
| 13581 | } else |err| switch (err) { | ||
| 13582 | error.FileNotFound, | ||
| 13583 | error.AccessDenied, | ||
| 13584 | => break :unappended err, | ||
| 13585 | error.InvalidExe => { | ||
| 13586 | // On InvalidExe, if the extension of the app name is .exe then | ||
| 13587 | // it's treated as an unrecoverable error. Otherwise, it'll be | ||
| 13588 | // skipped as normal. | ||
| 13589 | const app_name = app_buf.items[0..app_name_len]; | ||
| 13590 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; | ||
| 13591 | const ext = app_name[ext_start..]; | ||
| 13592 | if (windows.eqlIgnoreCaseWtf16(ext, std.unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | ||
| 13593 | return error.UnrecoverableInvalidExe; | ||
| 13594 | } | ||
| 13595 | break :unappended err; | ||
| 13596 | }, | ||
| 13597 | else => return err, | ||
| 13598 | } | ||
| 13599 | } | ||
| 13600 | break :unappended error.FileNotFound; | ||
| 13601 | }; | ||
| 13602 | |||
| 13603 | if (!any_pathext_seen) return unappended_err; | ||
| 13604 | |||
| 13605 | // Now try any PATHEXT appended versions that we've seen | ||
| 13606 | var ext_it = std.mem.tokenizeScalar(u16, pathext, ';'); | ||
| 13607 | while (ext_it.next()) |ext| { | ||
| 13608 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; | ||
| 13609 | if (!pathext_seen[@intFromEnum(ext_enum)]) continue; | ||
| 13610 | |||
| 13611 | dir_buf.shrinkRetainingCapacity(dir_path_len); | ||
| 13612 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | ||
| 13613 | '/', '\\' => {}, | ||
| 13614 | else => try dir_buf.append(allocator, Dir.path.sep), | ||
| 13615 | }; | ||
| 13616 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | ||
| 13617 | try dir_buf.appendSlice(allocator, ext); | ||
| 13618 | try dir_buf.append(allocator, 0); | ||
| 13619 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | ||
| 13620 | |||
| 13621 | const is_bat_or_cmd = switch (ext_enum) { | ||
| 13622 | .cmd, .bat => true, | ||
| 13623 | else => false, | ||
| 13624 | }; | ||
| 13625 | const cmd_line_w = if (is_bat_or_cmd) | ||
| 13626 | try cmd_line_cache.scriptCommandLine(full_app_name) | ||
| 13627 | else | ||
| 13628 | try cmd_line_cache.commandLine(); | ||
| 13629 | const app_name_w = if (is_bat_or_cmd) | ||
| 13630 | try cmd_line_cache.cmdExePath() | ||
| 13631 | else | ||
| 13632 | full_app_name; | ||
| 13633 | |||
| 13634 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| { | ||
| 13635 | return; | ||
| 13636 | } else |err| switch (err) { | ||
| 13637 | error.FileNotFound => continue, | ||
| 13638 | error.AccessDenied => continue, | ||
| 13639 | error.InvalidExe => { | ||
| 13640 | // On InvalidExe, if the extension of the app name is .exe then | ||
| 13641 | // it's treated as an unrecoverable error. Otherwise, it'll be | ||
| 13642 | // skipped as normal. | ||
| 13643 | if (windows.eqlIgnoreCaseWtf16(ext, std.unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | ||
| 13644 | return error.UnrecoverableInvalidExe; | ||
| 13645 | } | ||
| 13646 | continue; | ||
| 13647 | }, | ||
| 13648 | else => return err, | ||
| 13649 | } | ||
| 13650 | } | ||
| 13651 | |||
| 13652 | return unappended_err; | ||
| 13653 | } | ||
| 13654 | |||
| 13655 | fn windowsCreateProcess( | ||
| 13656 | app_name: [*:0]u16, | ||
| 13657 | cmd_line: [*:0]u16, | ||
| 13658 | envp_ptr: ?[*]u16, | ||
| 13659 | cwd_ptr: ?[*:0]u16, | ||
| 13660 | flags: windows.CreateProcessFlags, | ||
| 13661 | lpStartupInfo: *windows.STARTUPINFOW, | ||
| 13662 | lpProcessInformation: *windows.PROCESS_INFORMATION, | ||
| 13663 | ) !void { | ||
| 13664 | // TODO the docs for environment pointer say: | ||
| 13665 | // > A pointer to the environment block for the new process. If this parameter | ||
| 13666 | // > is NULL, the new process uses the environment of the calling process. | ||
| 13667 | // > ... | ||
| 13668 | // > An environment block can contain either Unicode or ANSI characters. If | ||
| 13669 | // > the environment block pointed to by lpEnvironment contains Unicode | ||
| 13670 | // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. | ||
| 13671 | // > If this parameter is NULL and the environment block of the parent process | ||
| 13672 | // > contains Unicode characters, you must also ensure that dwCreationFlags | ||
| 13673 | // > includes CREATE_UNICODE_ENVIRONMENT. | ||
| 13674 | // This seems to imply that we have to somehow know whether our process parent passed | ||
| 13675 | // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter. | ||
| 13676 | // Since we do not know this information that would imply that we must not pass NULL | ||
| 13677 | // for the parameter. | ||
| 13678 | // However this would imply that programs compiled with -DUNICODE could not pass | ||
| 13679 | // environment variables to programs that were not, which seems unlikely. | ||
| 13680 | // More investigation is needed. | ||
| 13681 | return windows.CreateProcessW( | ||
| 13682 | app_name, | ||
| 13683 | cmd_line, | ||
| 13684 | null, | ||
| 13685 | null, | ||
| 13686 | windows.TRUE, | ||
| 13687 | flags, | ||
| 13688 | @as(?*anyopaque, @ptrCast(envp_ptr)), | ||
| 13689 | cwd_ptr, | ||
| 13690 | lpStartupInfo, | ||
| 13691 | lpProcessInformation, | ||
| 13692 | ); | ||
| 13693 | } | ||
| 13694 | |||
| 13695 | /// Case-insensitive WTF-16 lookup | ||
| 13696 | fn windowsCreateProcessSupportsExtension(ext: []const u16) ?process.WindowsExtension { | ||
| 13697 | comptime { | ||
| 13698 | // Ensures keeping this function in sync with the enum. | ||
| 13699 | const fields = @typeInfo(process.WindowsExtension).@"enum".fields; | ||
| 13700 | assert(fields.len == 4); | ||
| 13701 | assert(@intFromEnum(process.WindowsExtension.bat) == 0); | ||
| 13702 | assert(@intFromEnum(process.WindowsExtension.cmd) == 1); | ||
| 13703 | assert(@intFromEnum(process.WindowsExtension.com) == 2); | ||
| 13704 | assert(@intFromEnum(process.WindowsExtension.exe) == 3); | ||
| 13705 | } | ||
| 13706 | |||
| 13707 | if (ext.len != 4) return null; | ||
| 13708 | const State = enum { | ||
| 13709 | start, | ||
| 13710 | dot, | ||
| 13711 | b, | ||
| 13712 | ba, | ||
| 13713 | c, | ||
| 13714 | cm, | ||
| 13715 | co, | ||
| 13716 | e, | ||
| 13717 | ex, | ||
| 13718 | }; | ||
| 13719 | var state: State = .start; | ||
| 13720 | for (ext) |c| switch (state) { | ||
| 13721 | .start => switch (c) { | ||
| 13722 | '.' => state = .dot, | ||
| 13723 | else => return null, | ||
| 13724 | }, | ||
| 13725 | .dot => switch (c) { | ||
| 13726 | 'b', 'B' => state = .b, | ||
| 13727 | 'c', 'C' => state = .c, | ||
| 13728 | 'e', 'E' => state = .e, | ||
| 13729 | else => return null, | ||
| 13730 | }, | ||
| 13731 | .b => switch (c) { | ||
| 13732 | 'a', 'A' => state = .ba, | ||
| 13733 | else => return null, | ||
| 13734 | }, | ||
| 13735 | .c => switch (c) { | ||
| 13736 | 'm', 'M' => state = .cm, | ||
| 13737 | 'o', 'O' => state = .co, | ||
| 13738 | else => return null, | ||
| 13739 | }, | ||
| 13740 | .e => switch (c) { | ||
| 13741 | 'x', 'X' => state = .ex, | ||
| 13742 | else => return null, | ||
| 13743 | }, | ||
| 13744 | .ba => switch (c) { | ||
| 13745 | 't', 'T' => return .bat, | ||
| 13746 | else => return null, | ||
| 13747 | }, | ||
| 13748 | .cm => switch (c) { | ||
| 13749 | 'd', 'D' => return .cmd, | ||
| 13750 | else => return null, | ||
| 13751 | }, | ||
| 13752 | .co => switch (c) { | ||
| 13753 | 'm', 'M' => return .com, | ||
| 13754 | else => return null, | ||
| 13755 | }, | ||
| 13756 | .ex => switch (c) { | ||
| 13757 | 'e', 'E' => return .exe, | ||
| 13758 | else => return null, | ||
| 13759 | }, | ||
| 13760 | }; | ||
| 13761 | return null; | ||
| 13762 | } | ||
| 13763 | |||
| 13764 | test windowsCreateProcessSupportsExtension { | ||
| 13765 | try std.testing.expectEqual(process.WindowsExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?); | ||
| 13766 | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); | ||
| 13767 | } | ||
| 13768 | |||
| 13769 | /// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW. | ||
| 13770 | /// | ||
| 13771 | /// Serialization is done on-demand and the result is cached in order to allow for: | ||
| 13772 | /// - Only serializing the particular type of command line needed (`.bat`/`.cmd` | ||
| 13773 | /// command line serialization is different from `.exe`/etc) | ||
| 13774 | /// - Reusing the serialized command lines if necessary (i.e. if the execution | ||
| 13775 | /// of a command fails and the PATH is going to be continued to be searched | ||
| 13776 | /// for more candidates) | ||
| 13777 | const WindowsCommandLineCache = struct { | ||
| 13778 | cmd_line: ?[:0]u16 = null, | ||
| 13779 | script_cmd_line: ?[:0]u16 = null, | ||
| 13780 | cmd_exe_path: ?[:0]u16 = null, | ||
| 13781 | argv: []const []const u8, | ||
| 13782 | allocator: Allocator, | ||
| 13783 | |||
| 13784 | fn init(allocator: Allocator, argv: []const []const u8) WindowsCommandLineCache { | ||
| 13785 | return .{ | ||
| 13786 | .allocator = allocator, | ||
| 13787 | .argv = argv, | ||
| 13788 | }; | ||
| 13789 | } | ||
| 13790 | |||
| 13791 | fn deinit(self: *WindowsCommandLineCache) void { | ||
| 13792 | if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line); | ||
| 13793 | if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line); | ||
| 13794 | if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path); | ||
| 13795 | } | ||
| 13796 | |||
| 13797 | fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 { | ||
| 13798 | if (self.cmd_line == null) { | ||
| 13799 | self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv); | ||
| 13800 | } | ||
| 13801 | return self.cmd_line.?; | ||
| 13802 | } | ||
| 13803 | |||
| 13804 | /// Not cached, since the path to the batch script will change during PATH searching. | ||
| 13805 | /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched, | ||
| 13806 | /// then script_path should include both the search path and the script filename | ||
| 13807 | /// (this allows avoiding cmd.exe having to search the PATH again). | ||
| 13808 | fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 { | ||
| 13809 | if (self.script_cmd_line) |v| self.allocator.free(v); | ||
| 13810 | self.script_cmd_line = try argvToScriptCommandLineWindows( | ||
| 13811 | self.allocator, | ||
| 13812 | script_path, | ||
| 13813 | self.argv[1..], | ||
| 13814 | ); | ||
| 13815 | return self.script_cmd_line.?; | ||
| 13816 | } | ||
| 13817 | |||
| 13818 | fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 { | ||
| 13819 | if (self.cmd_exe_path == null) { | ||
| 13820 | self.cmd_exe_path = try windowsCmdExePath(self.allocator); | ||
| 13821 | } | ||
| 13822 | return self.cmd_exe_path.?; | ||
| 13823 | } | ||
| 13824 | }; | ||
| 13825 | |||
| 13826 | /// Returns the absolute path of `cmd.exe` within the Windows system directory. | ||
| 13827 | /// The caller owns the returned slice. | ||
| 13828 | fn windowsCmdExePath(allocator: Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { | ||
| 13829 | var buf = try std.ArrayList(u16).initCapacity(allocator, 128); | ||
| 13830 | errdefer buf.deinit(allocator); | ||
| 13831 | while (true) { | ||
| 13832 | const unused_slice = buf.unusedCapacitySlice(); | ||
| 13833 | // TODO: Get the system directory from PEB.ReadOnlyStaticServerData | ||
| 13834 | const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len)); | ||
| 13835 | if (len == 0) { | ||
| 13836 | switch (windows.GetLastError()) { | ||
| 13837 | else => |err| return windows.unexpectedError(err), | ||
| 13838 | } | ||
| 13839 | } | ||
| 13840 | if (len > unused_slice.len) { | ||
| 13841 | try buf.ensureUnusedCapacity(allocator, len); | ||
| 13842 | } else { | ||
| 13843 | buf.items.len = len; | ||
| 13844 | break; | ||
| 13845 | } | ||
| 13846 | } | ||
| 13847 | switch (buf.items[buf.items.len - 1]) { | ||
| 13848 | '/', '\\' => {}, | ||
| 13849 | else => try buf.append(allocator, Dir.path.sep), | ||
| 13850 | } | ||
| 13851 | try buf.appendSlice(allocator, std.unicode.utf8ToUtf16LeStringLiteral("cmd.exe")); | ||
| 13852 | return try buf.toOwnedSliceSentinel(allocator, 0); | ||
| 13853 | } | ||
| 13854 | |||
| 13855 | const ArgvToScriptCommandLineError = error{ | ||
| 13856 | OutOfMemory, | ||
| 13857 | InvalidWtf8, | ||
| 13858 | /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | ||
| 13859 | /// within arguments when executing a `.bat`/`.cmd` script. | ||
| 13860 | /// - NUL/LF signifiies end of arguments, so anything afterwards | ||
| 13861 | /// would be lost after execution. | ||
| 13862 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | ||
| 13863 | /// would be lost after execution. | ||
| 13864 | InvalidBatchScriptArg, | ||
| 13865 | }; | ||
| 13866 | |||
| 13867 | /// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific | ||
| 13868 | /// escaping rules. The caller owns the returned slice. | ||
| 13869 | /// | ||
| 13870 | /// Escapes `argv` using the suggested mitigation against arbitrary command execution from: | ||
| 13871 | /// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | ||
| 13872 | /// | ||
| 13873 | /// The return of this function will look like | ||
| 13874 | /// `cmd.exe /d /e:ON /v:OFF /c "<escaped command line>"` | ||
| 13875 | /// and should be used as the `lpCommandLine` of `CreateProcessW`, while the | ||
| 13876 | /// return of `windowsCmdExePath` should be used as `lpApplicationName`. | ||
| 13877 | /// | ||
| 13878 | /// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise. | ||
| 13879 | /// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem. | ||
| 13880 | fn argvToScriptCommandLineWindows( | ||
| 13881 | allocator: Allocator, | ||
| 13882 | /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD. | ||
| 13883 | /// The script must have been verified to exist at this path before calling this function. | ||
| 13884 | script_path: []const u16, | ||
| 13885 | /// Arguments, not including the script name itself. Expected to be encoded as WTF-8. | ||
| 13886 | script_args: []const []const u8, | ||
| 13887 | ) ArgvToScriptCommandLineError![:0]u16 { | ||
| 13888 | var buf = try std.array_list.Managed(u8).initCapacity(allocator, 64); | ||
| 13889 | defer buf.deinit(); | ||
| 13890 | |||
| 13891 | // `/d` disables execution of AutoRun commands. | ||
| 13892 | // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation: | ||
| 13893 | // > If delayed expansion is enabled via the registry value DelayedExpansion, | ||
| 13894 | // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option. | ||
| 13895 | // > Escaping for % requires the command extension to be enabled. | ||
| 13896 | // > If it’s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option. | ||
| 13897 | // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | ||
| 13898 | buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \""); | ||
| 13899 | |||
| 13900 | // Always quote the path to the script arg | ||
| 13901 | buf.appendAssumeCapacity('"'); | ||
| 13902 | // We always want the path to the batch script to include a path separator in order to | ||
| 13903 | // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary | ||
| 13904 | // command execution mitigation, we just know exactly what script we want to execute | ||
| 13905 | // at this point, and potentially making cmd.exe re-find it is unnecessary. | ||
| 13906 | // | ||
| 13907 | // If the script path does not have a path separator, then we know its relative to CWD and | ||
| 13908 | // we can just put `.\` in the front. | ||
| 13909 | if (std.mem.findAny(u16, script_path, &[_]u16{ | ||
| 13910 | std.mem.nativeToLittle(u16, '\\'), std.mem.nativeToLittle(u16, '/'), | ||
| 13911 | }) == null) { | ||
| 13912 | try buf.appendSlice(".\\"); | ||
| 13913 | } | ||
| 13914 | // Note that we don't do any escaping/mitigations for this argument, since the relevant | ||
| 13915 | // characters (", %, etc) are illegal in file paths and this function should only be called | ||
| 13916 | // with script paths that have been verified to exist. | ||
| 13917 | try std.unicode.wtf16LeToWtf8ArrayList(&buf, script_path); | ||
| 13918 | buf.appendAssumeCapacity('"'); | ||
| 13919 | |||
| 13920 | for (script_args) |arg| { | ||
| 13921 | // Literal carriage returns get stripped when run through cmd.exe | ||
| 13922 | // and NUL/newlines act as 'end of command.' Because of this, it's basically | ||
| 13923 | // always a mistake to include these characters in argv, so it's | ||
| 13924 | // an error condition in order to ensure that the return of this | ||
| 13925 | // function can always roundtrip through cmd.exe. | ||
| 13926 | if (std.mem.findAny(u8, arg, "\x00\r\n") != null) { | ||
| 13927 | return error.InvalidBatchScriptArg; | ||
| 13928 | } | ||
| 13929 | |||
| 13930 | // Separate args with a space. | ||
| 13931 | try buf.append(' '); | ||
| 13932 | |||
| 13933 | // Need to quote if the argument is empty (otherwise the arg would just be lost) | ||
| 13934 | // or if the last character is a `\`, since then something like "%~2" in a .bat | ||
| 13935 | // script would cause the closing " to be escaped which we don't want. | ||
| 13936 | var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\'; | ||
| 13937 | if (!needs_quotes) { | ||
| 13938 | for (arg) |c| { | ||
| 13939 | switch (c) { | ||
| 13940 | // Known good characters that don't need to be quoted | ||
| 13941 | 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {}, | ||
| 13942 | // When in doubt, quote | ||
| 13943 | else => { | ||
| 13944 | needs_quotes = true; | ||
| 13945 | break; | ||
| 13946 | }, | ||
| 13947 | } | ||
| 13948 | } | ||
| 13949 | } | ||
| 13950 | if (needs_quotes) { | ||
| 13951 | try buf.append('"'); | ||
| 13952 | } | ||
| 13953 | var backslashes: usize = 0; | ||
| 13954 | for (arg) |c| { | ||
| 13955 | switch (c) { | ||
| 13956 | '\\' => { | ||
| 13957 | backslashes += 1; | ||
| 13958 | }, | ||
| 13959 | '"' => { | ||
| 13960 | try buf.appendNTimes('\\', backslashes); | ||
| 13961 | try buf.append('"'); | ||
| 13962 | backslashes = 0; | ||
| 13963 | }, | ||
| 13964 | // Replace `%` with `%%cd:~,%`. | ||
| 13965 | // | ||
| 13966 | // cmd.exe allows extracting a substring from an environment | ||
| 13967 | // variable with the syntax: `%foo:~<start_index>,<end_index>%`. | ||
| 13968 | // Therefore, `%cd:~,%` will always expand to an empty string | ||
| 13969 | // since both the start and end index are blank, and it is assumed | ||
| 13970 | // that `%cd%` is always available since it is a built-in variable | ||
| 13971 | // that corresponds to the current directory. | ||
| 13972 | // | ||
| 13973 | // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%` | ||
| 13974 | // will stop `%foo%` from being expanded and *after* expansion | ||
| 13975 | // we'll still be left with `%foo%` (the literal string). | ||
| 13976 | '%' => { | ||
| 13977 | // the trailing `%` is appended outside the switch | ||
| 13978 | try buf.appendSlice("%%cd:~,"); | ||
| 13979 | backslashes = 0; | ||
| 13980 | }, | ||
| 13981 | else => { | ||
| 13982 | backslashes = 0; | ||
| 13983 | }, | ||
| 13984 | } | ||
| 13985 | try buf.append(c); | ||
| 13986 | } | ||
| 13987 | if (needs_quotes) { | ||
| 13988 | try buf.appendNTimes('\\', backslashes); | ||
| 13989 | try buf.append('"'); | ||
| 13990 | } | ||
| 13991 | } | ||
| 13992 | |||
| 13993 | try buf.append('"'); | ||
| 13994 | |||
| 13995 | return try std.unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | ||
| 13996 | } | ||
| 13997 | |||
| 13998 | const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 }; | ||
| 13999 | |||
| 14000 | /// Serializes `argv` to a Windows command-line string suitable for passing to a child process and | ||
| 14001 | /// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice. | ||
| 14002 | /// | ||
| 14003 | /// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts. | ||
| 14004 | /// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | ||
| 14005 | /// | ||
| 14006 | /// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead. | ||
| 14007 | fn argvToCommandLineWindows( | ||
| 14008 | allocator: Allocator, | ||
| 14009 | argv: []const []const u8, | ||
| 14010 | ) ArgvToCommandLineError![:0]u16 { | ||
| 14011 | var buf = std.array_list.Managed(u8).init(allocator); | ||
| 14012 | defer buf.deinit(); | ||
| 14013 | |||
| 14014 | if (argv.len != 0) { | ||
| 14015 | const arg0 = argv[0]; | ||
| 14016 | |||
| 14017 | // The first argument must be quoted if it contains spaces or ASCII control characters | ||
| 14018 | // (excluding DEL). It also follows special quoting rules where backslashes have no special | ||
| 14019 | // interpretation, which makes it impossible to pass certain first arguments containing | ||
| 14020 | // double quotes to a child process without characters from the first argument leaking into | ||
| 14021 | // subsequent ones (which could have security implications). | ||
| 14022 | // | ||
| 14023 | // Empty arguments technically don't need quotes, but we quote them anyway for maximum | ||
| 14024 | // compatibility with different implementations of the 'CommandLineToArgvW' algorithm. | ||
| 14025 | // | ||
| 14026 | // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject | ||
| 14027 | // all first arguments containing double quotes, even ones that we could theoretically | ||
| 14028 | // serialize in unquoted form. | ||
| 14029 | var needs_quotes = arg0.len == 0; | ||
| 14030 | for (arg0) |c| { | ||
| 14031 | if (c <= ' ') { | ||
| 14032 | needs_quotes = true; | ||
| 14033 | } else if (c == '"') { | ||
| 14034 | return error.InvalidArg0; | ||
| 14035 | } | ||
| 14036 | } | ||
| 14037 | if (needs_quotes) { | ||
| 14038 | try buf.append('"'); | ||
| 14039 | try buf.appendSlice(arg0); | ||
| 14040 | try buf.append('"'); | ||
| 14041 | } else { | ||
| 14042 | try buf.appendSlice(arg0); | ||
| 14043 | } | ||
| 14044 | |||
| 14045 | for (argv[1..]) |arg| { | ||
| 14046 | try buf.append(' '); | ||
| 14047 | |||
| 14048 | // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes, | ||
| 14049 | // or if they are empty. For simplicity and for maximum compatibility with different | ||
| 14050 | // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII | ||
| 14051 | // control characters (again, excluding DEL). | ||
| 14052 | needs_quotes = for (arg) |c| { | ||
| 14053 | if (c <= ' ' or c == '"') { | ||
| 14054 | break true; | ||
| 14055 | } | ||
| 14056 | } else arg.len == 0; | ||
| 14057 | if (!needs_quotes) { | ||
| 14058 | try buf.appendSlice(arg); | ||
| 14059 | continue; | ||
| 14060 | } | ||
| 14061 | |||
| 14062 | try buf.append('"'); | ||
| 14063 | var backslash_count: usize = 0; | ||
| 14064 | for (arg) |byte| { | ||
| 14065 | switch (byte) { | ||
| 14066 | '\\' => { | ||
| 14067 | backslash_count += 1; | ||
| 14068 | }, | ||
| 14069 | '"' => { | ||
| 14070 | try buf.appendNTimes('\\', backslash_count * 2 + 1); | ||
| 14071 | try buf.append('"'); | ||
| 14072 | backslash_count = 0; | ||
| 14073 | }, | ||
| 14074 | else => { | ||
| 14075 | try buf.appendNTimes('\\', backslash_count); | ||
| 14076 | try buf.append(byte); | ||
| 14077 | backslash_count = 0; | ||
| 14078 | }, | ||
| 14079 | } | ||
| 14080 | } | ||
| 14081 | try buf.appendNTimes('\\', backslash_count * 2); | ||
| 14082 | try buf.append('"'); | ||
| 14083 | } | ||
| 14084 | } | ||
| 14085 | |||
| 14086 | return try std.unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | ||
| 14087 | } | ||
| 14088 | |||
| 14089 | test argvToCommandLineWindows { | ||
| 14090 | const t = testArgvToCommandLineWindows; | ||
| 14091 | |||
| 14092 | try t(&.{ | ||
| 14093 | \\C:\Program Files\zig\zig.exe | ||
| 14094 | , | ||
| 14095 | \\run | ||
| 14096 | , | ||
| 14097 | \\.\src\main.zig | ||
| 14098 | , | ||
| 14099 | \\-target | ||
| 14100 | , | ||
| 14101 | \\x86_64-windows-gnu | ||
| 14102 | , | ||
| 14103 | \\-O | ||
| 14104 | , | ||
| 14105 | \\ReleaseSafe | ||
| 14106 | , | ||
| 14107 | \\-- | ||
| 14108 | , | ||
| 14109 | \\--emoji=🗿 | ||
| 14110 | , | ||
| 14111 | \\--eval=new Regex("Dwayne \"The Rock\" Johnson") | ||
| 14112 | , | ||
| 14113 | }, | ||
| 14114 | \\"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\")" | ||
| 14115 | ); | ||
| 14116 | |||
| 14117 | try t(&.{}, ""); | ||
| 14118 | try t(&.{""}, "\"\""); | ||
| 14119 | try t(&.{" "}, "\" \""); | ||
| 14120 | try t(&.{"\t"}, "\"\t\""); | ||
| 14121 | try t(&.{"\x07"}, "\"\x07\""); | ||
| 14122 | try t(&.{"🦎"}, "🦎"); | ||
| 14123 | |||
| 14124 | try t( | ||
| 14125 | &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" }, | ||
| 14126 | "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee", | ||
| 14127 | ); | ||
| 14128 | |||
| 14129 | try t( | ||
| 14130 | &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" }, | ||
| 14131 | "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"", | ||
| 14132 | ); | ||
| 14133 | |||
| 14134 | try std.testing.expectError( | ||
| 14135 | error.InvalidArg0, | ||
| 14136 | argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}), | ||
| 14137 | ); | ||
| 14138 | try std.testing.expectError( | ||
| 14139 | error.InvalidArg0, | ||
| 14140 | argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}), | ||
| 14141 | ); | ||
| 14142 | try std.testing.expectError( | ||
| 14143 | error.InvalidArg0, | ||
| 14144 | argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}), | ||
| 14145 | ); | ||
| 14146 | } | ||
| 14147 | |||
| 14148 | fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void { | ||
| 14149 | const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); | ||
| 14150 | defer std.testing.allocator.free(cmd_line_w); | ||
| 14151 | |||
| 14152 | const cmd_line = try std.unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w); | ||
| 14153 | defer std.testing.allocator.free(cmd_line); | ||
| 14154 | |||
| 14155 | try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); | ||
| 14156 | } | ||
| 14157 | |||
| 14158 | /// Replaces the current process image with the executed process. If this | ||
| 14159 | /// function succeeds, it does not return. | ||
| 14160 | /// | ||
| 14161 | /// This operation is not available on all targets. `can_execv` | ||
| 14162 | /// | ||
| 14163 | /// This function also uses the PATH environment variable to get the full path to the executable. | ||
| 14164 | /// If `file` is an absolute path, this is the same as `execveZ`. | ||
| 14165 | /// | ||
| 14166 | /// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable, | ||
| 14167 | /// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall. | ||
| 14168 | /// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in. | ||
| 14169 | fn execvpeZ_expandArg0( | ||
| 14170 | arg0_expand: process.ArgExpansion, | ||
| 14171 | file: [*:0]const u8, | ||
| 14172 | child_argv: [*:null]?[*:0]const u8, | ||
| 14173 | envp: [*:null]const ?[*:0]const u8, | ||
| 14174 | PATH: []const u8, | ||
| 14175 | ) process.ReplaceError { | ||
| 14176 | const file_slice = std.mem.sliceTo(file, 0); | ||
| 14177 | if (std.mem.findScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp); | ||
| 14178 | |||
| 14179 | // Use of PATH_MAX here is valid as the path_buf will be passed | ||
| 14180 | // directly to the operating system in execveZ. | ||
| 14181 | var path_buf: [posix.PATH_MAX]u8 = undefined; | ||
| 14182 | var it = std.mem.tokenizeScalar(u8, PATH, ':'); | ||
| 14183 | var seen_eacces = false; | ||
| 14184 | var err: process.ReplaceError = error.FileNotFound; | ||
| 14185 | |||
| 14186 | // In case of expanding arg0 we must put it back if we return with an error. | ||
| 14187 | const prev_arg0 = child_argv[0]; | ||
| 14188 | defer switch (arg0_expand) { | ||
| 14189 | .expand => child_argv[0] = prev_arg0, | ||
| 14190 | .no_expand => {}, | ||
| 14191 | }; | ||
| 14192 | |||
| 14193 | while (it.next()) |search_path| { | ||
| 14194 | const path_len = search_path.len + file_slice.len + 1; | ||
| 14195 | if (path_buf.len < path_len + 1) return error.NameTooLong; | ||
| 14196 | @memcpy(path_buf[0..search_path.len], search_path); | ||
| 14197 | path_buf[search_path.len] = '/'; | ||
| 14198 | @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice); | ||
| 14199 | path_buf[path_len] = 0; | ||
| 14200 | const full_path = path_buf[0..path_len :0].ptr; | ||
| 14201 | switch (arg0_expand) { | ||
| 14202 | .expand => child_argv[0] = full_path, | ||
| 14203 | .no_expand => {}, | ||
| 14204 | } | ||
| 14205 | err = execveZ(full_path, child_argv, envp); | ||
| 14206 | switch (err) { | ||
| 14207 | error.AccessDenied => seen_eacces = true, | ||
| 14208 | error.FileNotFound, error.NotDir => {}, | ||
| 14209 | else => |e| return e, | ||
| 14210 | } | ||
| 14211 | } | ||
| 14212 | if (seen_eacces) return error.AccessDenied; | ||
| 14213 | return err; | ||
| 14214 | } | ||
| 14215 | |||
| 14216 | /// This function ignores PATH environment variable. See `execvpeZ` for that. | ||
| 14217 | pub fn execveZ( | ||
| 14218 | path: [*:0]const u8, | ||
| 14219 | child_argv: [*:null]const ?[*:0]const u8, | ||
| 14220 | envp: [*:null]const ?[*:0]const u8, | ||
| 14221 | ) process.ReplaceError { | ||
| 14222 | switch (posix.errno(posix.system.execve(path, child_argv, envp))) { | ||
| 14223 | .SUCCESS => unreachable, | ||
| 14224 | .FAULT => |err| return errnoBug(err), // Bad pointer parameter. | ||
| 14225 | .@"2BIG" => return error.SystemResources, | ||
| 14226 | .MFILE => return error.ProcessFdQuotaExceeded, | ||
| 14227 | .NAMETOOLONG => return error.NameTooLong, | ||
| 14228 | .NFILE => return error.SystemFdQuotaExceeded, | ||
| 14229 | .NOMEM => return error.SystemResources, | ||
| 14230 | .ACCES => return error.AccessDenied, | ||
| 14231 | .PERM => return error.PermissionDenied, | ||
| 14232 | .INVAL => return error.InvalidExe, | ||
| 14233 | .NOEXEC => return error.InvalidExe, | ||
| 14234 | .IO => return error.FileSystem, | ||
| 14235 | .LOOP => return error.FileSystem, | ||
| 14236 | .ISDIR => return error.IsDir, | ||
| 14237 | .NOENT => return error.FileNotFound, | ||
| 14238 | .NOTDIR => return error.NotDir, | ||
| 14239 | .TXTBSY => return error.FileBusy, | ||
| 14240 | else => |err| switch (native_os) { | ||
| 14241 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => switch (err) { | ||
| 14242 | .BADEXEC => return error.InvalidExe, | ||
| 14243 | .BADARCH => return error.InvalidExe, | ||
| 14244 | else => return posix.unexpectedErrno(err), | ||
| 14245 | }, | ||
| 14246 | .linux => switch (err) { | ||
| 14247 | .LIBBAD => return error.InvalidExe, | ||
| 14248 | else => return posix.unexpectedErrno(err), | ||
| 14249 | }, | ||
| 14250 | else => return posix.unexpectedErrno(err), | ||
| 14251 | }, | ||
| 14252 | } | ||
| 14253 | } | ||
| 14254 | |||
| 14255 | /// This function also uses the PATH environment variable to get the full path to the executable. | ||
| 14256 | /// If `file` is an absolute path, this is the same as `execveZ`. | ||
| 14257 | pub fn execvpeZ( | ||
| 14258 | file: [*:0]const u8, | ||
| 14259 | argv_ptr: [*:null]const ?[*:0]const u8, | ||
| 14260 | envp: [*:null]const ?[*:0]const u8, | ||
| 14261 | optional_PATH: ?[]const u8, | ||
| 14262 | ) process.ReplaceError { | ||
| 14263 | return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp, optional_PATH); | ||
| 14264 | } | ||
| 14265 | |||
| 14266 | fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | ||
| 14267 | var rd_h: windows.HANDLE = undefined; | ||
| 14268 | var wr_h: windows.HANDLE = undefined; | ||
| 14269 | try windows.CreatePipe(&rd_h, &wr_h, sattr); | ||
| 14270 | errdefer windowsDestroyPipe(rd_h, wr_h); | ||
| 14271 | try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); | ||
| 14272 | rd.* = rd_h; | ||
| 14273 | wr.* = wr_h; | ||
| 14274 | } | ||
| 14275 | |||
| 14276 | fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { | ||
| 14277 | if (rd) |h| posix.close(h); | ||
| 14278 | if (wr) |h| posix.close(h); | ||
| 14279 | } | ||
| 14280 | |||
| 14281 | fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | ||
| 14282 | var tmp_bufw: [128]u16 = undefined; | ||
| 14283 | |||
| 14284 | // Anonymous pipes are built upon Named pipes. | ||
| 14285 | // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe | ||
| 14286 | // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. | ||
| 14287 | // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations | ||
| 14288 | const pipe_path = blk: { | ||
| 14289 | var tmp_buf: [128]u8 = undefined; | ||
| 14290 | // Forge a random path for the pipe. | ||
| 14291 | const pipe_path = std.fmt.bufPrintSentinel( | ||
| 14292 | &tmp_buf, | ||
| 14293 | "\\\\.\\pipe\\zig-childprocess-{d}-{d}", | ||
| 14294 | .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, | ||
| 14295 | 0, | ||
| 14296 | ) catch unreachable; | ||
| 14297 | const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable; | ||
| 14298 | tmp_bufw[len] = 0; | ||
| 14299 | break :blk tmp_bufw[0..len :0]; | ||
| 14300 | }; | ||
| 14301 | |||
| 14302 | // Create the read handle that can be used with overlapped IO ops. | ||
| 14303 | const read_handle = windows.kernel32.CreateNamedPipeW( | ||
| 14304 | pipe_path.ptr, | ||
| 14305 | windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, | ||
| 14306 | windows.PIPE_TYPE_BYTE, | ||
| 14307 | 1, | ||
| 14308 | 4096, | ||
| 14309 | 4096, | ||
| 14310 | 0, | ||
| 14311 | sattr, | ||
| 14312 | ); | ||
| 14313 | if (read_handle == windows.INVALID_HANDLE_VALUE) { | ||
| 14314 | switch (windows.GetLastError()) { | ||
| 14315 | else => |err| return windows.unexpectedError(err), | ||
| 14316 | } | ||
| 14317 | } | ||
| 14318 | errdefer posix.close(read_handle); | ||
| 14319 | |||
| 14320 | var sattr_copy = sattr.*; | ||
| 14321 | const write_handle = windows.kernel32.CreateFileW( | ||
| 14322 | pipe_path.ptr, | ||
| 14323 | .{ .GENERIC = .{ .WRITE = true } }, | ||
| 14324 | 0, | ||
| 14325 | &sattr_copy, | ||
| 14326 | windows.OPEN_EXISTING, | ||
| 14327 | @bitCast(windows.FILE.ATTRIBUTE{ .NORMAL = true }), | ||
| 14328 | null, | ||
| 14329 | ); | ||
| 14330 | if (write_handle == windows.INVALID_HANDLE_VALUE) { | ||
| 14331 | switch (windows.GetLastError()) { | ||
| 14332 | else => |err| return windows.unexpectedError(err), | ||
| 14333 | } | ||
| 14334 | } | ||
| 14335 | errdefer posix.close(write_handle); | ||
| 14336 | |||
| 14337 | try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); | ||
| 14338 | |||
| 14339 | rd.* = read_handle; | ||
| 14340 | wr.* = write_handle; | ||
| 14341 | } | ||
| 14342 | |||
| 14343 | var pipe_name_counter = std.atomic.Value(u32).init(1); | ||
| 14344 | |||
| 12693 | test { | 14345 | test { |
| 12694 | _ = @import("Threaded/test.zig"); | 14346 | _ = @import("Threaded/test.zig"); |
| 12695 | } | 14347 | } |
lib/std/c.zig+22-22| ... | @@ -3764,8 +3764,8 @@ pub const W = switch (native_os) { | ... | @@ -3764,8 +3764,8 @@ pub const W = switch (native_os) { |
| 3764 | pub fn EXITSTATUS(x: u32) u8 { | 3764 | pub fn EXITSTATUS(x: u32) u8 { |
| 3765 | return @as(u8, @intCast(x >> 8)); | 3765 | return @as(u8, @intCast(x >> 8)); |
| 3766 | } | 3766 | } |
| 3767 | pub fn TERMSIG(x: u32) u32 { | 3767 | pub fn TERMSIG(x: u32) SIG { |
| 3768 | return status(x); | 3768 | return @enumFromInt(status(x)); |
| 3769 | } | 3769 | } |
| 3770 | pub fn STOPSIG(x: u32) u32 { | 3770 | pub fn STOPSIG(x: u32) u32 { |
| 3771 | return x >> 8; | 3771 | return x >> 8; |
| ... | @@ -3797,14 +3797,14 @@ pub const W = switch (native_os) { | ... | @@ -3797,14 +3797,14 @@ pub const W = switch (native_os) { |
| 3797 | pub fn EXITSTATUS(s: u32) u8 { | 3797 | pub fn EXITSTATUS(s: u32) u8 { |
| 3798 | return @as(u8, @intCast((s & 0xff00) >> 8)); | 3798 | return @as(u8, @intCast((s & 0xff00) >> 8)); |
| 3799 | } | 3799 | } |
| 3800 | pub fn TERMSIG(s: u32) u32 { | 3800 | pub fn TERMSIG(s: u32) SIG { |
| 3801 | return s & 0x7f; | 3801 | return @enumFromInt(s & 0x7f); |
| 3802 | } | 3802 | } |
| 3803 | pub fn STOPSIG(s: u32) u32 { | 3803 | pub fn STOPSIG(s: u32) u32 { |
| 3804 | return EXITSTATUS(s); | 3804 | return EXITSTATUS(s); |
| 3805 | } | 3805 | } |
| 3806 | pub fn IFEXITED(s: u32) bool { | 3806 | pub fn IFEXITED(s: u32) bool { |
| 3807 | return TERMSIG(s) == 0; | 3807 | return (s & 0x7f) == 0; |
| 3808 | } | 3808 | } |
| 3809 | pub fn IFSTOPPED(s: u32) bool { | 3809 | pub fn IFSTOPPED(s: u32) bool { |
| 3810 | return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00; | 3810 | return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00; |
| ... | @@ -3825,14 +3825,14 @@ pub const W = switch (native_os) { | ... | @@ -3825,14 +3825,14 @@ pub const W = switch (native_os) { |
| 3825 | pub fn EXITSTATUS(s: u32) u8 { | 3825 | pub fn EXITSTATUS(s: u32) u8 { |
| 3826 | return @as(u8, @intCast((s >> 8) & 0xff)); | 3826 | return @as(u8, @intCast((s >> 8) & 0xff)); |
| 3827 | } | 3827 | } |
| 3828 | pub fn TERMSIG(s: u32) u32 { | 3828 | pub fn TERMSIG(s: u32) SIG { |
| 3829 | return s & 0x7f; | 3829 | return @enumFromInt(s & 0x7f); |
| 3830 | } | 3830 | } |
| 3831 | pub fn STOPSIG(s: u32) u32 { | 3831 | pub fn STOPSIG(s: u32) u32 { |
| 3832 | return EXITSTATUS(s); | 3832 | return EXITSTATUS(s); |
| 3833 | } | 3833 | } |
| 3834 | pub fn IFEXITED(s: u32) bool { | 3834 | pub fn IFEXITED(s: u32) bool { |
| 3835 | return TERMSIG(s) == 0; | 3835 | return (s & 0x7f) == 0; |
| 3836 | } | 3836 | } |
| 3837 | 3837 | ||
| 3838 | pub fn IFCONTINUED(s: u32) bool { | 3838 | pub fn IFCONTINUED(s: u32) bool { |
| ... | @@ -3859,14 +3859,14 @@ pub const W = switch (native_os) { | ... | @@ -3859,14 +3859,14 @@ pub const W = switch (native_os) { |
| 3859 | pub fn EXITSTATUS(s: u32) u8 { | 3859 | pub fn EXITSTATUS(s: u32) u8 { |
| 3860 | return @as(u8, @intCast((s >> 8) & 0xff)); | 3860 | return @as(u8, @intCast((s >> 8) & 0xff)); |
| 3861 | } | 3861 | } |
| 3862 | pub fn TERMSIG(s: u32) u32 { | 3862 | pub fn TERMSIG(s: u32) SIG { |
| 3863 | return s & 0x7f; | 3863 | return @enumFromInt(s & 0x7f); |
| 3864 | } | 3864 | } |
| 3865 | pub fn STOPSIG(s: u32) u32 { | 3865 | pub fn STOPSIG(s: u32) u32 { |
| 3866 | return EXITSTATUS(s); | 3866 | return EXITSTATUS(s); |
| 3867 | } | 3867 | } |
| 3868 | pub fn IFEXITED(s: u32) bool { | 3868 | pub fn IFEXITED(s: u32) bool { |
| 3869 | return TERMSIG(s) == 0; | 3869 | return (s & 0x7f) == 0; |
| 3870 | } | 3870 | } |
| 3871 | 3871 | ||
| 3872 | pub fn IFCONTINUED(s: u32) bool { | 3872 | pub fn IFCONTINUED(s: u32) bool { |
| ... | @@ -3893,14 +3893,14 @@ pub const W = switch (native_os) { | ... | @@ -3893,14 +3893,14 @@ pub const W = switch (native_os) { |
| 3893 | pub fn EXITSTATUS(s: u32) u8 { | 3893 | pub fn EXITSTATUS(s: u32) u8 { |
| 3894 | return @as(u8, @intCast((s & 0xff00) >> 8)); | 3894 | return @as(u8, @intCast((s & 0xff00) >> 8)); |
| 3895 | } | 3895 | } |
| 3896 | pub fn TERMSIG(s: u32) u32 { | 3896 | pub fn TERMSIG(s: u32) SIG { |
| 3897 | return s & 0x7f; | 3897 | return @enumFromInt(s & 0x7f); |
| 3898 | } | 3898 | } |
| 3899 | pub fn STOPSIG(s: u32) u32 { | 3899 | pub fn STOPSIG(s: u32) u32 { |
| 3900 | return EXITSTATUS(s); | 3900 | return EXITSTATUS(s); |
| 3901 | } | 3901 | } |
| 3902 | pub fn IFEXITED(s: u32) bool { | 3902 | pub fn IFEXITED(s: u32) bool { |
| 3903 | return TERMSIG(s) == 0; | 3903 | return (s & 0x7f) == 0; |
| 3904 | } | 3904 | } |
| 3905 | pub fn IFSTOPPED(s: u32) bool { | 3905 | pub fn IFSTOPPED(s: u32) bool { |
| 3906 | return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00; | 3906 | return @as(u16, @truncate((((s & 0xffff) *% 0x10001) >> 8))) > 0x7f00; |
| ... | @@ -3921,8 +3921,8 @@ pub const W = switch (native_os) { | ... | @@ -3921,8 +3921,8 @@ pub const W = switch (native_os) { |
| 3921 | return @as(u8, @intCast(s & 0xff)); | 3921 | return @as(u8, @intCast(s & 0xff)); |
| 3922 | } | 3922 | } |
| 3923 | 3923 | ||
| 3924 | pub fn TERMSIG(s: u32) u32 { | 3924 | pub fn TERMSIG(s: u32) SIG { |
| 3925 | return (s >> 8) & 0xff; | 3925 | return @enumFromInt((s >> 8) & 0xff); |
| 3926 | } | 3926 | } |
| 3927 | 3927 | ||
| 3928 | pub fn STOPSIG(s: u32) u32 { | 3928 | pub fn STOPSIG(s: u32) u32 { |
| ... | @@ -3949,14 +3949,14 @@ pub const W = switch (native_os) { | ... | @@ -3949,14 +3949,14 @@ pub const W = switch (native_os) { |
| 3949 | pub fn EXITSTATUS(s: u32) u8 { | 3949 | pub fn EXITSTATUS(s: u32) u8 { |
| 3950 | return @as(u8, @intCast((s >> 8) & 0xff)); | 3950 | return @as(u8, @intCast((s >> 8) & 0xff)); |
| 3951 | } | 3951 | } |
| 3952 | pub fn TERMSIG(s: u32) u32 { | 3952 | pub fn TERMSIG(s: u32) SIG { |
| 3953 | return (s & 0x7f); | 3953 | return @enumFromInt(s & 0x7f); |
| 3954 | } | 3954 | } |
| 3955 | pub fn STOPSIG(s: u32) u32 { | 3955 | pub fn STOPSIG(s: u32) u32 { |
| 3956 | return EXITSTATUS(s); | 3956 | return EXITSTATUS(s); |
| 3957 | } | 3957 | } |
| 3958 | pub fn IFEXITED(s: u32) bool { | 3958 | pub fn IFEXITED(s: u32) bool { |
| 3959 | return TERMSIG(s) == 0; | 3959 | return (s & 0x7f) == 0; |
| 3960 | } | 3960 | } |
| 3961 | 3961 | ||
| 3962 | pub fn IFCONTINUED(s: u32) bool { | 3962 | pub fn IFCONTINUED(s: u32) bool { |
| ... | @@ -3988,12 +3988,12 @@ pub const W = switch (native_os) { | ... | @@ -3988,12 +3988,12 @@ pub const W = switch (native_os) { |
| 3988 | return EXITSTATUS(s); | 3988 | return EXITSTATUS(s); |
| 3989 | } | 3989 | } |
| 3990 | 3990 | ||
| 3991 | pub fn TERMSIG(s: u32) u32 { | 3991 | pub fn TERMSIG(s: u32) SIG { |
| 3992 | return s & 0x7f; | 3992 | return @enumFromInt(s & 0x7f); |
| 3993 | } | 3993 | } |
| 3994 | 3994 | ||
| 3995 | pub fn IFEXITED(s: u32) bool { | 3995 | pub fn IFEXITED(s: u32) bool { |
| 3996 | return TERMSIG(s) == 0; | 3996 | return (s & 0x7f) == 0; |
| 3997 | } | 3997 | } |
| 3998 | 3998 | ||
| 3999 | pub fn IFSTOPPED(s: u32) bool { | 3999 | pub fn IFSTOPPED(s: u32) bool { |
lib/std/os/emscripten.zig+3-3| ... | @@ -224,14 +224,14 @@ pub const W = struct { | ... | @@ -224,14 +224,14 @@ pub const W = struct { |
| 224 | pub fn EXITSTATUS(s: u32) u8 { | 224 | pub fn EXITSTATUS(s: u32) u8 { |
| 225 | return @as(u8, @intCast((s & 0xff00) >> 8)); | 225 | return @as(u8, @intCast((s & 0xff00) >> 8)); |
| 226 | } | 226 | } |
| 227 | pub fn TERMSIG(s: u32) u32 { | 227 | pub fn TERMSIG(s: u32) SIG { |
| 228 | return s & 0x7f; | 228 | return @enumFromInt(s & 0x7f); |
| 229 | } | 229 | } |
| 230 | pub fn STOPSIG(s: u32) u32 { | 230 | pub fn STOPSIG(s: u32) u32 { |
| 231 | return EXITSTATUS(s); | 231 | return EXITSTATUS(s); |
| 232 | } | 232 | } |
| 233 | pub fn IFEXITED(s: u32) bool { | 233 | pub fn IFEXITED(s: u32) bool { |
| 234 | return TERMSIG(s) == 0; | 234 | return (s & 0x7f) == 0; |
| 235 | } | 235 | } |
| 236 | pub fn IFSTOPPED(s: u32) bool { | 236 | pub fn IFSTOPPED(s: u32) bool { |
| 237 | return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00; | 237 | return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00; |
lib/std/os/linux.zig+3-3| ... | @@ -3616,14 +3616,14 @@ pub const W = struct { | ... | @@ -3616,14 +3616,14 @@ pub const W = struct { |
| 3616 | pub fn EXITSTATUS(s: u32) u8 { | 3616 | pub fn EXITSTATUS(s: u32) u8 { |
| 3617 | return @as(u8, @intCast((s & 0xff00) >> 8)); | 3617 | return @as(u8, @intCast((s & 0xff00) >> 8)); |
| 3618 | } | 3618 | } |
| 3619 | pub fn TERMSIG(s: u32) u32 { | 3619 | pub fn TERMSIG(s: u32) SIG { |
| 3620 | return s & 0x7f; | 3620 | return @enumFromInt(s & 0x7f); |
| 3621 | } | 3621 | } |
| 3622 | pub fn STOPSIG(s: u32) u32 { | 3622 | pub fn STOPSIG(s: u32) u32 { |
| 3623 | return EXITSTATUS(s); | 3623 | return EXITSTATUS(s); |
| 3624 | } | 3624 | } |
| 3625 | pub fn IFEXITED(s: u32) bool { | 3625 | pub fn IFEXITED(s: u32) bool { |
| 3626 | return TERMSIG(s) == 0; | 3626 | return (s & 0x7f) == 0; |
| 3627 | } | 3627 | } |
| 3628 | pub fn IFSTOPPED(s: u32) bool { | 3628 | pub fn IFSTOPPED(s: u32) bool { |
| 3629 | return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00; | 3629 | return @as(u16, @truncate(((s & 0xffff) *% 0x10001) >> 8)) > 0x7f00; |
lib/std/posix.zig-97| ... | @@ -795,79 +795,10 @@ pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void { | ... | @@ -795,79 +795,10 @@ pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void { |
| 795 | } | 795 | } |
| 796 | } | 796 | } |
| 797 | 797 | ||
| 798 | pub fn getpid() pid_t { | ||
| 799 | return system.getpid(); | ||
| 800 | } | ||
| 801 | |||
| 802 | pub fn getppid() pid_t { | 798 | pub fn getppid() pid_t { |
| 803 | return system.getppid(); | 799 | return system.getppid(); |
| 804 | } | 800 | } |
| 805 | 801 | ||
| 806 | pub const ExecveError = error{ | ||
| 807 | SystemResources, | ||
| 808 | AccessDenied, | ||
| 809 | PermissionDenied, | ||
| 810 | InvalidExe, | ||
| 811 | FileSystem, | ||
| 812 | IsDir, | ||
| 813 | FileNotFound, | ||
| 814 | NotDir, | ||
| 815 | FileBusy, | ||
| 816 | ProcessFdQuotaExceeded, | ||
| 817 | SystemFdQuotaExceeded, | ||
| 818 | NameTooLong, | ||
| 819 | } || UnexpectedError; | ||
| 820 | |||
| 821 | /// This function ignores PATH environment variable. See `execvpeZ` for that. | ||
| 822 | pub fn execveZ( | ||
| 823 | path: [*:0]const u8, | ||
| 824 | child_argv: [*:null]const ?[*:0]const u8, | ||
| 825 | envp: [*:null]const ?[*:0]const u8, | ||
| 826 | ) ExecveError { | ||
| 827 | switch (errno(system.execve(path, child_argv, envp))) { | ||
| 828 | .SUCCESS => unreachable, | ||
| 829 | .FAULT => unreachable, | ||
| 830 | .@"2BIG" => return error.SystemResources, | ||
| 831 | .MFILE => return error.ProcessFdQuotaExceeded, | ||
| 832 | .NAMETOOLONG => return error.NameTooLong, | ||
| 833 | .NFILE => return error.SystemFdQuotaExceeded, | ||
| 834 | .NOMEM => return error.SystemResources, | ||
| 835 | .ACCES => return error.AccessDenied, | ||
| 836 | .PERM => return error.PermissionDenied, | ||
| 837 | .INVAL => return error.InvalidExe, | ||
| 838 | .NOEXEC => return error.InvalidExe, | ||
| 839 | .IO => return error.FileSystem, | ||
| 840 | .LOOP => return error.FileSystem, | ||
| 841 | .ISDIR => return error.IsDir, | ||
| 842 | .NOENT => return error.FileNotFound, | ||
| 843 | .NOTDIR => return error.NotDir, | ||
| 844 | .TXTBSY => return error.FileBusy, | ||
| 845 | else => |err| switch (native_os) { | ||
| 846 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => switch (err) { | ||
| 847 | .BADEXEC => return error.InvalidExe, | ||
| 848 | .BADARCH => return error.InvalidExe, | ||
| 849 | else => return unexpectedErrno(err), | ||
| 850 | }, | ||
| 851 | .linux => switch (err) { | ||
| 852 | .LIBBAD => return error.InvalidExe, | ||
| 853 | else => return unexpectedErrno(err), | ||
| 854 | }, | ||
| 855 | else => return unexpectedErrno(err), | ||
| 856 | }, | ||
| 857 | } | ||
| 858 | } | ||
| 859 | |||
| 860 | /// This function also uses the PATH environment variable to get the full path to the executable. | ||
| 861 | /// If `file` is an absolute path, this is the same as `execveZ`. | ||
| 862 | pub fn execvpeZ( | ||
| 863 | file: [*:0]const u8, | ||
| 864 | argv_ptr: [*:null]const ?[*:0]const u8, | ||
| 865 | envp: [*:null]const ?[*:0]const u8, | ||
| 866 | optional_PATH: ?[]const u8, | ||
| 867 | ) ExecveError { | ||
| 868 | return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp, optional_PATH); | ||
| 869 | } | ||
| 870 | |||
| 871 | pub const GetCwdError = error{ | 802 | pub const GetCwdError = error{ |
| 872 | NameTooLong, | 803 | NameTooLong, |
| 873 | CurrentWorkingDirectoryUnlinked, | 804 | CurrentWorkingDirectoryUnlinked, |
| ... | @@ -1119,16 +1050,6 @@ pub fn seteuid(uid: uid_t) SetEidError!void { | ... | @@ -1119,16 +1050,6 @@ pub fn seteuid(uid: uid_t) SetEidError!void { |
| 1119 | } | 1050 | } |
| 1120 | } | 1051 | } |
| 1121 | 1052 | ||
| 1122 | pub fn setreuid(ruid: uid_t, euid: uid_t) SetIdError!void { | ||
| 1123 | switch (errno(system.setreuid(ruid, euid))) { | ||
| 1124 | .SUCCESS => return, | ||
| 1125 | .AGAIN => return error.ResourceLimitReached, | ||
| 1126 | .INVAL => return error.InvalidUserId, | ||
| 1127 | .PERM => return error.PermissionDenied, | ||
| 1128 | else => |err| return unexpectedErrno(err), | ||
| 1129 | } | ||
| 1130 | } | ||
| 1131 | |||
| 1132 | pub fn setgid(gid: gid_t) SetIdError!void { | 1053 | pub fn setgid(gid: gid_t) SetIdError!void { |
| 1133 | switch (errno(system.setgid(gid))) { | 1054 | switch (errno(system.setgid(gid))) { |
| 1134 | .SUCCESS => return, | 1055 | .SUCCESS => return, |
| ... | @@ -1158,24 +1079,6 @@ pub fn setregid(rgid: gid_t, egid: gid_t) SetIdError!void { | ... | @@ -1158,24 +1079,6 @@ pub fn setregid(rgid: gid_t, egid: gid_t) SetIdError!void { |
| 1158 | } | 1079 | } |
| 1159 | } | 1080 | } |
| 1160 | 1081 | ||
| 1161 | pub const SetPgidError = error{ | ||
| 1162 | ProcessAlreadyExec, | ||
| 1163 | InvalidProcessGroupId, | ||
| 1164 | PermissionDenied, | ||
| 1165 | ProcessNotFound, | ||
| 1166 | } || UnexpectedError; | ||
| 1167 | |||
| 1168 | pub fn setpgid(pid: pid_t, pgid: pid_t) SetPgidError!void { | ||
| 1169 | switch (errno(system.setpgid(pid, pgid))) { | ||
| 1170 | .SUCCESS => return, | ||
| 1171 | .ACCES => return error.ProcessAlreadyExec, | ||
| 1172 | .INVAL => return error.InvalidProcessGroupId, | ||
| 1173 | .PERM => return error.PermissionDenied, | ||
| 1174 | .SRCH => return error.ProcessNotFound, | ||
| 1175 | else => |err| return unexpectedErrno(err), | ||
| 1176 | } | ||
| 1177 | } | ||
| 1178 | |||
| 1179 | pub fn getuid() uid_t { | 1082 | pub fn getuid() uid_t { |
| 1180 | return system.getuid(); | 1083 | return system.getuid(); |
| 1181 | } | 1084 | } |
lib/std/process.zig+201-104| ... | @@ -259,20 +259,44 @@ pub fn getBaseAddress() usize { | ... | @@ -259,20 +259,44 @@ pub fn getBaseAddress() usize { |
| 259 | } | 259 | } |
| 260 | } | 260 | } |
| 261 | 261 | ||
| 262 | /// Deprecated in favor of `Child.can_spawn`. | ||
| 263 | pub const can_spawn = Child.can_spawn; | ||
| 264 | /// Deprecated in favor of `can_replace`. | ||
| 265 | pub const can_execv = can_replace; | ||
| 266 | |||
| 267 | /// Tells whether the target operating system supports replacing the current | 262 | /// Tells whether the target operating system supports replacing the current |
| 268 | /// process image. If this is `false` then calling `execv` or `replace` | 263 | /// process image. If this is `false` then calling `replace` or `replaceFile` |
| 269 | /// functions will cause compilation to fail. | 264 | /// functions will return `error.OperationUnsupported`. |
| 270 | pub const can_replace = switch (native_os) { | 265 | pub const can_replace = switch (native_os) { |
| 271 | .windows, .haiku, .wasi => false, | 266 | .windows, .haiku, .wasi => false, |
| 272 | else => true, | 267 | else => true, |
| 273 | }; | 268 | }; |
| 274 | 269 | ||
| 275 | pub const ReplaceError = std.posix.ExecveError || error{OutOfMemory}; | 270 | /// Tells whether spawning child processes is supported. |
| 271 | pub const can_spawn = switch (native_os) { | ||
| 272 | .wasi, .ios, .tvos, .visionos, .watchos => false, | ||
| 273 | else => true, | ||
| 274 | }; | ||
| 275 | |||
| 276 | pub const ReplaceError = error{ | ||
| 277 | /// The target operating system cannot replace the process image with a new | ||
| 278 | /// one. | ||
| 279 | OperationUnsupported, | ||
| 280 | SystemResources, | ||
| 281 | AccessDenied, | ||
| 282 | PermissionDenied, | ||
| 283 | InvalidExe, | ||
| 284 | FileSystem, | ||
| 285 | IsDir, | ||
| 286 | FileNotFound, | ||
| 287 | NotDir, | ||
| 288 | FileBusy, | ||
| 289 | ProcessFdQuotaExceeded, | ||
| 290 | SystemFdQuotaExceeded, | ||
| 291 | } || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; | ||
| 292 | |||
| 293 | pub const ReplaceOptions = struct { | ||
| 294 | argv: []const []const u8, | ||
| 295 | arg0_expand: ArgExpansion = .no_expand, | ||
| 296 | /// Replaces the environment when provided. The PATH value from here is | ||
| 297 | /// never used to resolve `argv[0]`. | ||
| 298 | env_map: ?*const Environ.Map = null, | ||
| 299 | }; | ||
| 276 | 300 | ||
| 277 | /// Replaces the current process image with the executed process. If this | 301 | /// Replaces the current process image with the executed process. If this |
| 278 | /// function succeeds, it does not return. | 302 | /// function succeeds, it does not return. |
| ... | @@ -281,25 +305,9 @@ pub const ReplaceError = std.posix.ExecveError || error{OutOfMemory}; | ... | @@ -281,25 +305,9 @@ pub const ReplaceError = std.posix.ExecveError || error{OutOfMemory}; |
| 281 | /// is not already a file path (i.e. it contains '/'), it is resolved into a | 305 | /// is not already a file path (i.e. it contains '/'), it is resolved into a |
| 282 | /// file path based on PATH from the parent environment. | 306 | /// file path based on PATH from the parent environment. |
| 283 | /// | 307 | /// |
| 284 | /// This operation is not available on targets for which `can_replace` is | 308 | /// It is illegal to call this function in a fork() child. |
| 285 | /// `false`. | 309 | pub fn replace(io: Io, options: ReplaceOptions) ReplaceError { |
| 286 | /// | 310 | return io.vtable.processReplace(io.userdata, options); |
| 287 | /// This function must allocate memory to add a null terminating bytes on path | ||
| 288 | /// and each arg. | ||
| 289 | /// | ||
| 290 | /// Due to the heap allocation, it is illegal to call this function in a fork() | ||
| 291 | /// child. | ||
| 292 | pub fn replace(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Block) ReplaceError { | ||
| 293 | if (!can_replace) @compileError("unsupported operation: replace"); | ||
| 294 | |||
| 295 | var arena_allocator = std.heap.ArenaAllocator.init(gpa); | ||
| 296 | defer arena_allocator.deinit(); | ||
| 297 | const arena = arena_allocator.allocator(); | ||
| 298 | |||
| 299 | const argv_buf = try arena.allocSentinel(?[*:0]const u8, argv.len, null); | ||
| 300 | for (argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; | ||
| 301 | |||
| 302 | return posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, env); | ||
| 303 | } | 311 | } |
| 304 | 312 | ||
| 305 | /// Replaces the current process image with the executed process. If this | 313 | /// 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 | ... | @@ -309,92 +317,181 @@ pub fn replace(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Bl |
| 309 | /// relative to `dir`. It is *always* treated as a file path, even if it does | 317 | /// relative to `dir`. It is *always* treated as a file path, even if it does |
| 310 | /// not contain '/'. | 318 | /// not contain '/'. |
| 311 | /// | 319 | /// |
| 312 | /// This operation is not available on targets for which `can_replace` is | 320 | /// It is illegal to call this function in a fork() child. |
| 313 | /// `false`. | 321 | pub fn replacePath(io: Io, dir: Io.Dir, options: ReplaceOptions) ReplaceError { |
| 322 | return io.vtable.processReplacePath(io.userdata, dir, options); | ||
| 323 | } | ||
| 324 | |||
| 325 | pub const ArgExpansion = enum { expand, no_expand }; | ||
| 326 | |||
| 327 | /// File name extensions supported natively by `CreateProcess()` on Windows. | ||
| 328 | pub const WindowsExtension = enum { bat, cmd, com, exe }; | ||
| 329 | |||
| 330 | pub const SpawnError = error{ | ||
| 331 | OutOfMemory, | ||
| 332 | /// POSIX-only. `StdIo.ignore` was selected and opening `/dev/null` returned ENODEV. | ||
| 333 | NoDevice, | ||
| 334 | /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8. | ||
| 335 | /// https://wtf-8.codeberg.page/ | ||
| 336 | InvalidWtf8, | ||
| 337 | /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process. | ||
| 338 | CurrentWorkingDirectoryUnlinked, | ||
| 339 | /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | ||
| 340 | /// within arguments when executing a `.bat`/`.cmd` script. | ||
| 341 | /// - NUL/LF signifiies end of arguments, so anything afterwards | ||
| 342 | /// would be lost after execution. | ||
| 343 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | ||
| 344 | /// would be lost after execution. | ||
| 345 | InvalidBatchScriptArg, | ||
| 346 | SystemResources, | ||
| 347 | AccessDenied, | ||
| 348 | PermissionDenied, | ||
| 349 | InvalidExe, | ||
| 350 | FileSystem, | ||
| 351 | IsDir, | ||
| 352 | FileNotFound, | ||
| 353 | NotDir, | ||
| 354 | FileBusy, | ||
| 355 | ProcessFdQuotaExceeded, | ||
| 356 | SystemFdQuotaExceeded, | ||
| 357 | ResourceLimitReached, | ||
| 358 | InvalidUserId, | ||
| 359 | InvalidProcessGroupId, | ||
| 360 | SymLinkLoop, | ||
| 361 | InvalidName, | ||
| 362 | /// An attempt was made to change the process group ID of one of the | ||
| 363 | /// children of the calling process and the child had already performed an | ||
| 364 | /// image replacement. | ||
| 365 | ProcessAlreadyExec, | ||
| 366 | } || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; | ||
| 367 | |||
| 368 | pub const SpawnOptions = struct { | ||
| 369 | argv: []const []const u8, | ||
| 370 | |||
| 371 | /// Set to change the current working directory when spawning the child process. | ||
| 372 | cwd: ?[]const u8 = null, | ||
| 373 | /// Set to change the current working directory when spawning the child process. | ||
| 374 | /// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190 | ||
| 375 | /// Once that is done, `cwd` will be deprecated in favor of this field. | ||
| 376 | cwd_dir: ?Io.Dir = null, | ||
| 377 | /// Replaces the child environment when provided. The PATH value from here | ||
| 378 | /// is not used to resolve `argv[0]`; that resolution always uses parent | ||
| 379 | /// environment. | ||
| 380 | env_map: ?*const Environ.Map = null, | ||
| 381 | expand_arg0: ArgExpansion = .no_expand, | ||
| 382 | /// When populated, a pipe will be created for the child process to | ||
| 383 | /// communicate progress back to the parent. The file descriptor of the | ||
| 384 | /// write end of the pipe will be specified in the `ZIG_PROGRESS` | ||
| 385 | /// environment variable inside the child process. The progress reported by | ||
| 386 | /// the child will be attached to this progress node in the parent process. | ||
| 387 | /// | ||
| 388 | /// The child's progress tree will be grafted into the parent's progress tree, | ||
| 389 | /// by substituting this node with the child's root node. | ||
| 390 | progress_node: std.Progress.Node = std.Progress.Node.none, | ||
| 391 | |||
| 392 | stdin: StdIo = .inherit, | ||
| 393 | stdout: StdIo = .inherit, | ||
| 394 | stderr: StdIo = .inherit, | ||
| 395 | |||
| 396 | /// Set to true to obtain rusage information for the child process. | ||
| 397 | /// Depending on the target platform and implementation status, the | ||
| 398 | /// requested statistics may or may not be available. If they are | ||
| 399 | /// available, then the `resource_usage_statistics` field will be populated | ||
| 400 | /// after calling `wait`. | ||
| 401 | /// On Linux and Darwin, this obtains rusage statistics from wait4(). | ||
| 402 | request_resource_usage_statistics: bool = false, | ||
| 403 | |||
| 404 | /// Set to change the user id when spawning the child process. | ||
| 405 | uid: ?posix.uid_t = null, | ||
| 406 | /// Set to change the group id when spawning the child process. | ||
| 407 | gid: ?posix.gid_t = null, | ||
| 408 | /// Set to change the process group id when spawning the child process. | ||
| 409 | pgid: ?posix.pid_t = null, | ||
| 410 | |||
| 411 | /// Start child process in suspended state. | ||
| 412 | /// For Posix systems it's started as if SIGSTOP was sent. | ||
| 413 | start_suspended: bool = false, | ||
| 414 | /// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess. | ||
| 415 | create_no_window: bool = false, | ||
| 416 | /// Darwin-only. Disable ASLR for the child process. | ||
| 417 | disable_aslr: bool = false, | ||
| 418 | |||
| 419 | /// Behavior of the child process's standard input, output, and error streams. | ||
| 420 | pub const StdIo = union(enum) { | ||
| 421 | /// Inherit the corresponding stream from the parent process. | ||
| 422 | inherit, | ||
| 423 | /// Pass an already open file from the parent to the child. | ||
| 424 | file: File, | ||
| 425 | /// Pass a null stream to the child process by opening "/dev/null" on POSIX | ||
| 426 | /// and "NUL" on Windows. | ||
| 427 | ignore, | ||
| 428 | /// Create a new pipe for the stream. | ||
| 429 | /// | ||
| 430 | /// The corresponding field (`stdout`, `stderr`, or `stdin`) will be | ||
| 431 | /// assigned a `File` object that can be used to read from or write to the | ||
| 432 | /// pipe. | ||
| 433 | pipe, | ||
| 434 | /// Spawn the child process with the corresponding stream missing. This | ||
| 435 | /// will likely result in the child encountering EBADF if it tries to use | ||
| 436 | /// stdin, stdout, or stderr, or if only one stream is closed, it will | ||
| 437 | /// result in them getting mixed up. Generally, this option is for advanced | ||
| 438 | /// use cases only. | ||
| 439 | close, | ||
| 440 | }; | ||
| 441 | }; | ||
| 442 | |||
| 443 | /// Creates a child process. | ||
| 314 | /// | 444 | /// |
| 315 | /// This function must allocate memory to add a null terminating bytes on path | 445 | /// `argv[0]` is the name of the program to execute. If it is not already a |
| 316 | /// and each arg. | 446 | /// file path (i.e. it contains '/'), it is resolved into a file path based on |
| 447 | /// PATH from the parent environment. | ||
| 448 | pub fn spawn(io: Io, options: SpawnOptions) SpawnError!Child { | ||
| 449 | return io.vtable.processSpawn(io.userdata, options); | ||
| 450 | } | ||
| 451 | |||
| 452 | /// Creates a child process. | ||
| 317 | /// | 453 | /// |
| 318 | /// Due to the heap allocation, it is illegal to call this | 454 | /// `argv[0]` is the file path of the program to execute, relative to `dir`. It |
| 319 | /// function in a fork() child. For that use case, use the `std.posix` | 455 | /// is *always* treated as a file path, even if it does not contain '/'. |
| 320 | /// functions directly. | 456 | pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child { |
| 321 | pub fn replaceFile(io: Io, gpa: Allocator, argv: []const []const u8, env: Environ.Block) ReplaceError { | 457 | return io.vtable.processSpawnPath(io.userdata, dir, options); |
| 322 | if (!can_replace) @compileError("unsupported operation: replaceFile"); | 458 | } |
| 323 | 459 | ||
| 324 | var arena_allocator = std.heap.ArenaAllocator.init(gpa); | 460 | pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{ |
| 325 | defer arena_allocator.deinit(); | 461 | StdoutStreamTooLong, |
| 326 | const arena = arena_allocator.allocator(); | 462 | StderrStreamTooLong, |
| 463 | }; | ||
| 327 | 464 | ||
| 328 | const argv_buf = try arena.allocSentinel(?[*:0]const u8, argv.len, null); | 465 | pub const RunOptions = struct { |
| 329 | for (argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; | 466 | spawn_options: SpawnOptions, |
| 467 | max_output_bytes: usize = 50 * 1024, | ||
| 468 | }; | ||
| 330 | 469 | ||
| 331 | return posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, env); | 470 | pub const RunResult = struct { |
| 332 | } | 471 | term: Child.Term, |
| 472 | stdout: []u8, | ||
| 473 | stderr: []u8, | ||
| 474 | }; | ||
| 333 | 475 | ||
| 334 | pub const Arg0Expand = enum { expand, no_expand }; | 476 | /// Spawns a child process, waits for it, collecting stdout and stderr, and then returns. |
| 477 | /// If it succeeds, the caller owns result.stdout and result.stderr memory. | ||
| 478 | pub fn run(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult { | ||
| 479 | var child = try spawn(io, options.spawn_options); | ||
| 480 | defer child.kill(io); | ||
| 335 | 481 | ||
| 336 | /// Replaces the current process image with the executed process. If this | 482 | var stdout: std.ArrayList(u8) = .empty; |
| 337 | /// function succeeds, it does not return. | 483 | defer stdout.deinit(gpa); |
| 338 | /// | 484 | var stderr: std.ArrayList(u8) = .empty; |
| 339 | /// This operation is not available on all targets. `can_execv` | 485 | defer stderr.deinit(gpa); |
| 340 | /// | ||
| 341 | /// This function also uses the PATH environment variable to get the full path to the executable. | ||
| 342 | /// If `file` is an absolute path, this is the same as `execveZ`. | ||
| 343 | /// | ||
| 344 | /// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable, | ||
| 345 | /// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall. | ||
| 346 | /// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in. | ||
| 347 | pub fn replace( | ||
| 348 | comptime arg0_expand: Arg0Expand, | ||
| 349 | file: [*:0]const u8, | ||
| 350 | child_argv: switch (arg0_expand) { | ||
| 351 | .expand => [*:null]?[*:0]const u8, | ||
| 352 | .no_expand => [*:null]const ?[*:0]const u8, | ||
| 353 | }, | ||
| 354 | envp: [*:null]const ?[*:0]const u8, | ||
| 355 | optional_PATH: ?[]const u8, | ||
| 356 | ) ExecveError { | ||
| 357 | const file_slice = mem.sliceTo(file, 0); | ||
| 358 | if (mem.findScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp); | ||
| 359 | |||
| 360 | const PATH = optional_PATH orelse "/usr/local/bin:/bin/:/usr/bin"; | ||
| 361 | // Use of PATH_MAX here is valid as the path_buf will be passed | ||
| 362 | // directly to the operating system in execveZ. | ||
| 363 | var path_buf: [PATH_MAX]u8 = undefined; | ||
| 364 | var it = mem.tokenizeScalar(u8, PATH, ':'); | ||
| 365 | var seen_eacces = false; | ||
| 366 | var err: ExecveError = error.FileNotFound; | ||
| 367 | |||
| 368 | // In case of expanding arg0 we must put it back if we return with an error. | ||
| 369 | const prev_arg0 = child_argv[0]; | ||
| 370 | defer switch (arg0_expand) { | ||
| 371 | .expand => child_argv[0] = prev_arg0, | ||
| 372 | .no_expand => {}, | ||
| 373 | }; | ||
| 374 | 486 | ||
| 375 | while (it.next()) |search_path| { | 487 | try child.collectOutput(gpa, &stdout, &stderr, options.max_output_bytes); |
| 376 | const path_len = search_path.len + file_slice.len + 1; | ||
| 377 | if (path_buf.len < path_len + 1) return error.NameTooLong; | ||
| 378 | @memcpy(path_buf[0..search_path.len], search_path); | ||
| 379 | path_buf[search_path.len] = '/'; | ||
| 380 | @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice); | ||
| 381 | path_buf[path_len] = 0; | ||
| 382 | const full_path = path_buf[0..path_len :0].ptr; | ||
| 383 | switch (arg0_expand) { | ||
| 384 | .expand => child_argv[0] = full_path, | ||
| 385 | .no_expand => {}, | ||
| 386 | } | ||
| 387 | err = execveZ(full_path, child_argv, envp); | ||
| 388 | switch (err) { | ||
| 389 | error.AccessDenied => seen_eacces = true, | ||
| 390 | error.FileNotFound, error.NotDir => {}, | ||
| 391 | else => |e| return e, | ||
| 392 | } | ||
| 393 | } | ||
| 394 | if (seen_eacces) return error.AccessDenied; | ||
| 395 | return err; | ||
| 396 | } | ||
| 397 | 488 | ||
| 489 | return .{ | ||
| 490 | .stdout = try stdout.toOwnedSlice(gpa), | ||
| 491 | .stderr = try stderr.toOwnedSlice(gpa), | ||
| 492 | .term = try child.wait(io), | ||
| 493 | }; | ||
| 494 | } | ||
| 398 | 495 | ||
| 399 | pub const TotalSystemMemoryError = error{ | 496 | pub const TotalSystemMemoryError = error{ |
| 400 | UnknownTotalSystemMemory, | 497 | UnknownTotalSystemMemory, |
lib/std/process/Child.zig+39-1760| ... | @@ -5,120 +5,38 @@ const native_os = builtin.os.tag; | ... | @@ -5,120 +5,38 @@ const native_os = builtin.os.tag; |
| 5 | 5 | ||
| 6 | const std = @import("../std.zig"); | 6 | const std = @import("../std.zig"); |
| 7 | const Io = std.Io; | 7 | const Io = std.Io; |
| 8 | const unicode = std.unicode; | ||
| 9 | const fs = std.fs; | ||
| 10 | const process = std.process; | 8 | const process = std.process; |
| 11 | const File = std.Io.File; | 9 | const File = std.Io.File; |
| 12 | const windows = std.os.windows; | ||
| 13 | const linux = std.os.linux; | ||
| 14 | const posix = std.posix; | ||
| 15 | const mem = std.mem; | ||
| 16 | const maxInt = std.math.maxInt; | ||
| 17 | const assert = std.debug.assert; | 10 | const assert = std.debug.assert; |
| 18 | const Allocator = std.mem.Allocator; | 11 | const Allocator = std.mem.Allocator; |
| 19 | const ArrayList = std.ArrayList; | 12 | const ArrayList = std.ArrayList; |
| 20 | 13 | ||
| 21 | /// Tells whether spawning child processes is supported. | ||
| 22 | pub const can_spawn = switch (native_os) { | ||
| 23 | .wasi, .ios, .tvos, .visionos, .watchos => false, | ||
| 24 | else => true, | ||
| 25 | }; | ||
| 26 | |||
| 27 | pub const Id = switch (native_os) { | 14 | pub const Id = switch (native_os) { |
| 28 | .windows => windows.HANDLE, | 15 | .windows => std.os.windows.HANDLE, |
| 29 | .wasi => void, | 16 | .wasi => void, |
| 30 | else => posix.pid_t, | 17 | else => std.posix.pid_t, |
| 31 | }; | 18 | }; |
| 32 | 19 | ||
| 33 | /// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`. | 20 | /// After `wait` or `kill` is called, this becomes `null`. |
| 34 | /// On Windows this is the hProcess. | 21 | /// On Windows this is the hProcess. |
| 35 | /// On POSIX this is the pid. | 22 | /// On POSIX this is the pid. |
| 36 | id: Id, | 23 | id: ?Id, |
| 37 | thread_handle: if (native_os == .windows) windows.HANDLE else void, | 24 | thread_handle: if (native_os == .windows) std.os.windows.HANDLE else void = {}, |
| 38 | |||
| 39 | allocator: Allocator, | ||
| 40 | |||
| 41 | /// The writing end of the child process's standard input pipe. | 25 | /// The writing end of the child process's standard input pipe. |
| 42 | /// Usage requires `stdin_behavior == StdIo.Pipe`. | 26 | /// Usage requires `process.SpawnOptions.StdIo.pipe`. |
| 43 | /// Available after calling `spawn()`. | ||
| 44 | stdin: ?File, | 27 | stdin: ?File, |
| 45 | |||
| 46 | /// The reading end of the child process's standard output pipe. | 28 | /// The reading end of the child process's standard output pipe. |
| 47 | /// Usage requires `stdout_behavior == StdIo.Pipe`. | 29 | /// Usage requires `process.SpawnOptions.StdIo.pipe`. |
| 48 | /// Available after calling `spawn()`. | ||
| 49 | stdout: ?File, | 30 | stdout: ?File, |
| 50 | |||
| 51 | /// The reading end of the child process's standard error pipe. | 31 | /// The reading end of the child process's standard error pipe. |
| 52 | /// Usage requires `stderr_behavior == StdIo.Pipe`. | 32 | /// Usage requires `process.SpawnOptions.StdIo.pipe`. |
| 53 | /// Available after calling `spawn()`. | ||
| 54 | stderr: ?File, | 33 | stderr: ?File, |
| 55 | |||
| 56 | /// Terminated state of the child process. | ||
| 57 | /// Available after calling `wait()`. | ||
| 58 | term: ?(SpawnError!Term), | ||
| 59 | |||
| 60 | argv: []const []const u8, | ||
| 61 | |||
| 62 | parent_environ: process.Environ, | ||
| 63 | /// `null` means to use `parent_environ` also for the spawned process. | ||
| 64 | env_map: ?*const EnvMap, | ||
| 65 | |||
| 66 | stdin_behavior: StdIo, | ||
| 67 | stdout_behavior: StdIo, | ||
| 68 | stderr_behavior: StdIo, | ||
| 69 | |||
| 70 | /// Set to change the user id when spawning the child process. | ||
| 71 | uid: if (native_os == .windows or native_os == .wasi) void else ?posix.uid_t, | ||
| 72 | |||
| 73 | /// Set to change the group id when spawning the child process. | ||
| 74 | gid: if (native_os == .windows or native_os == .wasi) void else ?posix.gid_t, | ||
| 75 | |||
| 76 | /// Set to change the process group id when spawning the child process. | ||
| 77 | pgid: if (native_os == .windows or native_os == .wasi) void else ?posix.pid_t, | ||
| 78 | |||
| 79 | /// Set to change the current working directory when spawning the child process. | ||
| 80 | cwd: ?[]const u8, | ||
| 81 | /// Set to change the current working directory when spawning the child process. | ||
| 82 | /// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190 | ||
| 83 | /// Once that is done, `cwd` will be deprecated in favor of this field. | ||
| 84 | cwd_dir: ?Io.Dir = null, | ||
| 85 | |||
| 86 | err_pipe: if (native_os == .windows) void else ?posix.fd_t, | ||
| 87 | |||
| 88 | expand_arg0: Arg0Expand, | ||
| 89 | |||
| 90 | /// Darwin-only. Disable ASLR for the child process. | ||
| 91 | disable_aslr: bool = false, | ||
| 92 | |||
| 93 | /// Start child process in suspended state. | ||
| 94 | /// For Posix systems it's started as if SIGSTOP was sent. | ||
| 95 | start_suspended: bool = false, | ||
| 96 | |||
| 97 | /// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess. | ||
| 98 | create_no_window: bool = false, | ||
| 99 | |||
| 100 | /// Set to true to obtain rusage information for the child process. | ||
| 101 | /// Depending on the target platform and implementation status, the | ||
| 102 | /// requested statistics may or may not be available. If they are | ||
| 103 | /// available, then the `resource_usage_statistics` field will be populated | ||
| 104 | /// after calling `wait`. | ||
| 105 | /// On Linux and Darwin, this obtains rusage statistics from wait4(). | ||
| 106 | request_resource_usage_statistics: bool = false, | ||
| 107 | |||
| 108 | /// This is available after calling wait if | 34 | /// This is available after calling wait if |
| 109 | /// `request_resource_usage_statistics` was set to `true` before calling | 35 | /// `request_resource_usage_statistics` was set to `true` before calling |
| 110 | /// `spawn`. | 36 | /// `spawn`. |
| 37 | /// TODO move this data into `Term` | ||
| 111 | resource_usage_statistics: ResourceUsageStatistics = .{}, | 38 | resource_usage_statistics: ResourceUsageStatistics = .{}, |
| 112 | 39 | request_resource_usage_statistics: bool, | |
| 113 | /// When populated, a pipe will be created for the child process to | ||
| 114 | /// communicate progress back to the parent. The file descriptor of the | ||
| 115 | /// write end of the pipe will be specified in the `ZIG_PROGRESS` | ||
| 116 | /// environment variable inside the child process. The progress reported by | ||
| 117 | /// the child will be attached to this progress node in the parent process. | ||
| 118 | /// | ||
| 119 | /// The child's progress tree will be grafted into the parent's progress tree, | ||
| 120 | /// by substituting this node with the child's root node. | ||
| 121 | progress_node: std.Progress.Node = std.Progress.Node.none, | ||
| 122 | 40 | ||
| 123 | pub const ResourceUsageStatistics = struct { | 41 | pub const ResourceUsageStatistics = struct { |
| 124 | rusage: @TypeOf(rusage_init) = rusage_init, | 42 | rusage: @TypeOf(rusage_init) = rusage_init, |
| ... | @@ -168,233 +86,60 @@ pub const ResourceUsageStatistics = struct { | ... | @@ -168,233 +86,60 @@ pub const ResourceUsageStatistics = struct { |
| 168 | .tvos, | 86 | .tvos, |
| 169 | .visionos, | 87 | .visionos, |
| 170 | .watchos, | 88 | .watchos, |
| 171 | => @as(?posix.rusage, null), | 89 | => @as(?std.posix.rusage, null), |
| 172 | .windows => @as(?windows.VM_COUNTERS, null), | 90 | .windows => @as(?std.os.windows.VM_COUNTERS, null), |
| 173 | else => {}, | 91 | else => {}, |
| 174 | }; | 92 | }; |
| 175 | }; | 93 | }; |
| 176 | 94 | ||
| 177 | pub const Arg0Expand = posix.Arg0Expand; | ||
| 178 | |||
| 179 | pub const SpawnError = error{ | ||
| 180 | OutOfMemory, | ||
| 181 | |||
| 182 | /// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV. | ||
| 183 | NoDevice, | ||
| 184 | |||
| 185 | /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8. | ||
| 186 | /// https://wtf-8.codeberg.page/ | ||
| 187 | InvalidWtf8, | ||
| 188 | |||
| 189 | /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process. | ||
| 190 | CurrentWorkingDirectoryUnlinked, | ||
| 191 | |||
| 192 | /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | ||
| 193 | /// within arguments when executing a `.bat`/`.cmd` script. | ||
| 194 | /// - NUL/LF signifiies end of arguments, so anything afterwards | ||
| 195 | /// would be lost after execution. | ||
| 196 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | ||
| 197 | /// would be lost after execution. | ||
| 198 | InvalidBatchScriptArg, | ||
| 199 | } || | ||
| 200 | posix.ExecveError || | ||
| 201 | posix.SetIdError || | ||
| 202 | posix.SetPgidError || | ||
| 203 | posix.ChangeCurDirError || | ||
| 204 | windows.CreateProcessError || | ||
| 205 | windows.GetProcessMemoryInfoError || | ||
| 206 | windows.WaitForSingleObjectError; | ||
| 207 | |||
| 208 | pub const Term = union(enum) { | 95 | pub const Term = union(enum) { |
| 209 | Exited: u8, | 96 | exited: u8, |
| 210 | Signal: u32, | 97 | signal: std.posix.SIG, |
| 211 | Stopped: u32, | 98 | stopped: u32, |
| 212 | Unknown: u32, | 99 | unknown: u32, |
| 213 | }; | ||
| 214 | |||
| 215 | /// Behavior of the child process's standard input, output, and error | ||
| 216 | /// streams. | ||
| 217 | pub const StdIo = enum { | ||
| 218 | /// Inherit the stream from the parent process. | ||
| 219 | Inherit, | ||
| 220 | |||
| 221 | /// Pass a null stream to the child process. | ||
| 222 | /// This is /dev/null on POSIX and NUL on Windows. | ||
| 223 | Ignore, | ||
| 224 | |||
| 225 | /// Create a pipe for the stream. | ||
| 226 | /// The corresponding field (`stdout`, `stderr`, or `stdin`) | ||
| 227 | /// will be assigned a `File` object that can be used | ||
| 228 | /// to read from or write to the pipe. | ||
| 229 | Pipe, | ||
| 230 | |||
| 231 | /// Close the stream after the child process spawns. | ||
| 232 | Close, | ||
| 233 | }; | 100 | }; |
| 234 | 101 | ||
| 235 | /// First argument in argv is the executable. | 102 | /// Requests for the operating system to forcibly terminate the child process, |
| 236 | pub fn init(gpa: Allocator, argv: []const []const u8, environ: Environ) Child { | 103 | /// then blocks until it terminates, then cleans up all resources. |
| 237 | return .{ | 104 | /// |
| 238 | .allocator = gpa, | 105 | /// Idempotent and does nothing after `wait` returns. |
| 239 | .argv = argv, | 106 | /// |
| 240 | .environ = environ, | 107 | /// Uncancelable. Ignores unexpected errors from the operating system. |
| 241 | .id = undefined, | 108 | pub fn kill(child: *Child, io: Io) void { |
| 242 | .thread_handle = undefined, | 109 | if (child.id != null) { |
| 243 | .err_pipe = if (native_os == .windows) {} else null, | 110 | assert(child.stdin == null); |
| 244 | .term = null, | 111 | assert(child.stdout == null); |
| 245 | .cwd = null, | 112 | assert(child.stderr == null); |
| 246 | .uid = if (native_os == .windows or native_os == .wasi) {} else null, | ||
| 247 | .gid = if (native_os == .windows or native_os == .wasi) {} else null, | ||
| 248 | .pgid = if (native_os == .windows or native_os == .wasi) {} else null, | ||
| 249 | .stdin = null, | ||
| 250 | .stdout = null, | ||
| 251 | .stderr = null, | ||
| 252 | .stdin_behavior = .Inherit, | ||
| 253 | .stdout_behavior = .Inherit, | ||
| 254 | .stderr_behavior = .Inherit, | ||
| 255 | .expand_arg0 = .no_expand, | ||
| 256 | }; | ||
| 257 | } | ||
| 258 | |||
| 259 | pub fn setUserName(self: *Child, name: []const u8) !void { | ||
| 260 | const user_info = try process.getUserInfo(name); | ||
| 261 | self.uid = user_info.uid; | ||
| 262 | self.gid = user_info.gid; | ||
| 263 | } | ||
| 264 | |||
| 265 | /// On success must call `kill` or `wait`. | ||
| 266 | /// After spawning the `id` is available. | ||
| 267 | pub fn spawn(self: *Child, io: Io) SpawnError!void { | ||
| 268 | if (!process.can_spawn) { | ||
| 269 | @compileError("the target operating system cannot spawn processes"); | ||
| 270 | } | ||
| 271 | |||
| 272 | if (native_os == .windows) { | ||
| 273 | return self.spawnWindows(io); | ||
| 274 | } else { | ||
| 275 | return self.spawnPosix(io); | ||
| 276 | } | ||
| 277 | } | ||
| 278 | |||
| 279 | pub fn spawnAndWait(child: *Child, io: Io) SpawnError!Term { | ||
| 280 | try child.spawn(io); | ||
| 281 | return child.wait(io); | ||
| 282 | } | ||
| 283 | |||
| 284 | /// Forcibly terminates child process and then cleans up all resources. | ||
| 285 | pub fn kill(self: *Child, io: Io) !Term { | ||
| 286 | if (native_os == .windows) { | ||
| 287 | return self.killWindows(io, 1); | ||
| 288 | } else { | ||
| 289 | return self.killPosix(io); | ||
| 290 | } | ||
| 291 | } | ||
| 292 | |||
| 293 | pub fn killWindows(self: *Child, io: Io, exit_code: windows.UINT) !Term { | ||
| 294 | if (self.term) |term| { | ||
| 295 | self.cleanupStreams(io); | ||
| 296 | return term; | ||
| 297 | } | ||
| 298 | |||
| 299 | windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) { | ||
| 300 | error.AccessDenied => { | ||
| 301 | // Usually when TerminateProcess triggers a ACCESS_DENIED error, it | ||
| 302 | // indicates that the process has already exited, but there may be | ||
| 303 | // some rare edge cases where our process handle no longer has the | ||
| 304 | // PROCESS_TERMINATE access right, so let's do another check to make | ||
| 305 | // sure the process is really no longer running: | ||
| 306 | windows.WaitForSingleObjectEx(self.id, 0, false) catch return err; | ||
| 307 | return error.AlreadyTerminated; | ||
| 308 | }, | ||
| 309 | else => return err, | ||
| 310 | }; | ||
| 311 | try self.waitUnwrappedWindows(io); | ||
| 312 | return self.term.?; | ||
| 313 | } | ||
| 314 | |||
| 315 | pub fn killPosix(self: *Child, io: Io) !Term { | ||
| 316 | if (self.term) |term| { | ||
| 317 | self.cleanupStreams(io); | ||
| 318 | return term; | ||
| 319 | } | ||
| 320 | posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { | ||
| 321 | error.ProcessNotFound => return error.AlreadyTerminated, | ||
| 322 | else => return err, | ||
| 323 | }; | ||
| 324 | self.waitUnwrappedPosix(io); | ||
| 325 | return self.term.?; | ||
| 326 | } | ||
| 327 | |||
| 328 | pub const WaitError = SpawnError || std.os.windows.GetProcessMemoryInfoError; | ||
| 329 | |||
| 330 | /// On some targets, `spawn` may not report all spawn errors, such as `error.InvalidExe`. | ||
| 331 | /// This function will block until any spawn errors can be reported, and return them. | ||
| 332 | pub fn waitForSpawn(self: *Child) SpawnError!void { | ||
| 333 | if (native_os == .windows) return; // `spawn` reports everything | ||
| 334 | if (self.term) |term| { | ||
| 335 | _ = term catch |spawn_err| return spawn_err; | ||
| 336 | return; | 113 | return; |
| 337 | } | 114 | } |
| 338 | 115 | io.vtable.childKill(io.userdata, child); | |
| 339 | const err_pipe = self.err_pipe orelse return; | 116 | assert(child.id == null); |
| 340 | self.err_pipe = null; | ||
| 341 | // Wait for the child to report any errors in or before `execvpe`. | ||
| 342 | const report = readIntFd(err_pipe); | ||
| 343 | posix.close(err_pipe); | ||
| 344 | if (report) |child_err_int| { | ||
| 345 | const child_err: SpawnError = @errorCast(@errorFromInt(child_err_int)); | ||
| 346 | self.term = child_err; | ||
| 347 | return child_err; | ||
| 348 | } else |read_err| switch (read_err) { | ||
| 349 | error.EndOfStream => { | ||
| 350 | // Write end closed by CLOEXEC at the time of the `execvpe` call, | ||
| 351 | // indicating success. | ||
| 352 | }, | ||
| 353 | else => { | ||
| 354 | // Problem reading the error from the error reporting pipe. We | ||
| 355 | // don't know if the child is alive or dead. Better to assume it is | ||
| 356 | // alive so the resource does not risk being leaked. | ||
| 357 | }, | ||
| 358 | } | ||
| 359 | } | 117 | } |
| 360 | 118 | ||
| 119 | pub const WaitError = error{ | ||
| 120 | AccessDenied, | ||
| 121 | } || Io.Cancelable || Io.UnexpectedError; | ||
| 122 | |||
| 361 | /// Blocks until child process terminates and then cleans up all resources. | 123 | /// Blocks until child process terminates and then cleans up all resources. |
| 362 | pub fn wait(self: *Child, io: Io) WaitError!Term { | 124 | pub fn wait(child: *Child, io: Io) WaitError!Term { |
| 363 | try self.waitForSpawn(); // report spawn errors | 125 | assert(child.id != null); |
| 364 | if (self.term) |term| { | 126 | return io.vtable.childWait(io.userdata, child); |
| 365 | self.cleanupStreams(io); | ||
| 366 | return term; | ||
| 367 | } | ||
| 368 | switch (native_os) { | ||
| 369 | .windows => try self.waitUnwrappedWindows(io), | ||
| 370 | else => self.waitUnwrappedPosix(io), | ||
| 371 | } | ||
| 372 | self.id = undefined; | ||
| 373 | return self.term.?; | ||
| 374 | } | 127 | } |
| 375 | 128 | ||
| 376 | pub const RunResult = struct { | ||
| 377 | term: Term, | ||
| 378 | stdout: []u8, | ||
| 379 | stderr: []u8, | ||
| 380 | }; | ||
| 381 | |||
| 382 | /// Collect the output from the process's stdout and stderr. Will return once all output | 129 | /// Collect the output from the process's stdout and stderr. Will return once all output |
| 383 | /// has been collected. This does not mean that the process has ended. `wait` should still | 130 | /// has been collected. This does not mean that the process has ended. `wait` should still |
| 384 | /// be called to wait for and clean up the process. | 131 | /// be called to wait for and clean up the process. |
| 385 | /// | 132 | /// |
| 386 | /// The process must be started with stdout_behavior and stderr_behavior == .Pipe | 133 | /// The process must have been started with stdout and stderr set to |
| 134 | /// `process.SpawnOptions.StdIo.pipe`. | ||
| 387 | pub fn collectOutput( | 135 | pub fn collectOutput( |
| 388 | child: Child, | 136 | child: *const Child, |
| 389 | /// Used for `stdout` and `stderr`. | 137 | /// Used for `stdout` and `stderr`. |
| 390 | allocator: Allocator, | 138 | allocator: Allocator, |
| 391 | stdout: *ArrayList(u8), | 139 | stdout: *ArrayList(u8), |
| 392 | stderr: *ArrayList(u8), | 140 | stderr: *ArrayList(u8), |
| 393 | max_output_bytes: usize, | 141 | max_output_bytes: usize, |
| 394 | ) !void { | 142 | ) !void { |
| 395 | assert(child.stdout_behavior == .Pipe); | ||
| 396 | assert(child.stderr_behavior == .Pipe); | ||
| 397 | |||
| 398 | var poller = std.Io.poll(allocator, enum { stdout, stderr }, .{ | 143 | var poller = std.Io.poll(allocator, enum { stdout, stderr }, .{ |
| 399 | .stdout = child.stdout.?, | 144 | .stdout = child.stdout.?, |
| 400 | .stderr = child.stderr.?, | 145 | .stderr = child.stderr.?, |
| ... | @@ -431,1469 +176,3 @@ pub fn collectOutput( | ... | @@ -431,1469 +176,3 @@ pub fn collectOutput( |
| 431 | return error.StderrStreamTooLong; | 176 | return error.StderrStreamTooLong; |
| 432 | } | 177 | } |
| 433 | } | 178 | } |
| 434 | |||
| 435 | pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{ | ||
| 436 | StdoutStreamTooLong, | ||
| 437 | StderrStreamTooLong, | ||
| 438 | }; | ||
| 439 | |||
| 440 | /// Spawns a child process, waits for it, collecting stdout and stderr, and then returns. | ||
| 441 | /// If it succeeds, the caller owns result.stdout and result.stderr memory. | ||
| 442 | pub fn run(gpa: Allocator, io: Io, args: struct { | ||
| 443 | argv: []const []const u8, | ||
| 444 | environ: Environ, | ||
| 445 | cwd: ?[]const u8 = null, | ||
| 446 | cwd_dir: ?Io.Dir = null, | ||
| 447 | max_output_bytes: usize = 50 * 1024, | ||
| 448 | expand_arg0: Arg0Expand = .no_expand, | ||
| 449 | progress_node: std.Progress.Node = std.Progress.Node.none, | ||
| 450 | }) RunError!RunResult { | ||
| 451 | var child = Child.init(gpa, args.argv, args.environ); | ||
| 452 | child.stdin_behavior = .Ignore; | ||
| 453 | child.stdout_behavior = .Pipe; | ||
| 454 | child.stderr_behavior = .Pipe; | ||
| 455 | child.cwd = args.cwd; | ||
| 456 | child.cwd_dir = args.cwd_dir; | ||
| 457 | child.expand_arg0 = args.expand_arg0; | ||
| 458 | child.progress_node = args.progress_node; | ||
| 459 | |||
| 460 | var stdout: ArrayList(u8) = .empty; | ||
| 461 | defer stdout.deinit(gpa); | ||
| 462 | var stderr: ArrayList(u8) = .empty; | ||
| 463 | defer stderr.deinit(gpa); | ||
| 464 | |||
| 465 | try child.spawn(io); | ||
| 466 | errdefer { | ||
| 467 | _ = child.kill(io) catch {}; | ||
| 468 | } | ||
| 469 | try child.collectOutput(gpa, &stdout, &stderr, args.max_output_bytes); | ||
| 470 | |||
| 471 | return .{ | ||
| 472 | .stdout = try stdout.toOwnedSlice(gpa), | ||
| 473 | .stderr = try stderr.toOwnedSlice(gpa), | ||
| 474 | .term = try child.wait(io), | ||
| 475 | }; | ||
| 476 | } | ||
| 477 | |||
| 478 | fn waitUnwrappedWindows(self: *Child, io: Io) WaitError!void { | ||
| 479 | const result = windows.WaitForSingleObjectEx(self.id, windows.INFINITE, false); | ||
| 480 | |||
| 481 | self.term = @as(SpawnError!Term, x: { | ||
| 482 | var exit_code: windows.DWORD = undefined; | ||
| 483 | if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) { | ||
| 484 | break :x Term{ .Unknown = 0 }; | ||
| 485 | } else { | ||
| 486 | break :x Term{ .Exited = @as(u8, @truncate(exit_code)) }; | ||
| 487 | } | ||
| 488 | }); | ||
| 489 | |||
| 490 | if (self.request_resource_usage_statistics) { | ||
| 491 | self.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(self.id); | ||
| 492 | } | ||
| 493 | |||
| 494 | posix.close(self.id); | ||
| 495 | posix.close(self.thread_handle); | ||
| 496 | self.cleanupStreams(io); | ||
| 497 | return result; | ||
| 498 | } | ||
| 499 | |||
| 500 | fn waitUnwrappedPosix(self: *Child, io: Io) void { | ||
| 501 | const res: posix.WaitPidResult = res: { | ||
| 502 | if (self.request_resource_usage_statistics) { | ||
| 503 | switch (native_os) { | ||
| 504 | .dragonfly, | ||
| 505 | .freebsd, | ||
| 506 | .netbsd, | ||
| 507 | .openbsd, | ||
| 508 | .illumos, | ||
| 509 | .linux, | ||
| 510 | .serenity, | ||
| 511 | .driverkit, | ||
| 512 | .ios, | ||
| 513 | .maccatalyst, | ||
| 514 | .macos, | ||
| 515 | .tvos, | ||
| 516 | .visionos, | ||
| 517 | .watchos, | ||
| 518 | => { | ||
| 519 | var ru: posix.rusage = undefined; | ||
| 520 | const res = posix.wait4(self.id, 0, &ru); | ||
| 521 | self.resource_usage_statistics.rusage = ru; | ||
| 522 | break :res res; | ||
| 523 | }, | ||
| 524 | else => {}, | ||
| 525 | } | ||
| 526 | } | ||
| 527 | |||
| 528 | break :res posix.waitpid(self.id, 0); | ||
| 529 | }; | ||
| 530 | const status = res.status; | ||
| 531 | self.cleanupStreams(io); | ||
| 532 | self.handleWaitResult(status); | ||
| 533 | } | ||
| 534 | |||
| 535 | fn handleWaitResult(self: *Child, status: u32) void { | ||
| 536 | self.term = statusToTerm(status); | ||
| 537 | } | ||
| 538 | |||
| 539 | fn cleanupStreams(self: *Child, io: Io) void { | ||
| 540 | if (self.stdin) |*stdin| { | ||
| 541 | stdin.close(io); | ||
| 542 | self.stdin = null; | ||
| 543 | } | ||
| 544 | if (self.stdout) |*stdout| { | ||
| 545 | stdout.close(io); | ||
| 546 | self.stdout = null; | ||
| 547 | } | ||
| 548 | if (self.stderr) |*stderr| { | ||
| 549 | stderr.close(io); | ||
| 550 | self.stderr = null; | ||
| 551 | } | ||
| 552 | } | ||
| 553 | |||
| 554 | fn statusToTerm(status: u32) Term { | ||
| 555 | return if (posix.W.IFEXITED(status)) | ||
| 556 | Term{ .Exited = posix.W.EXITSTATUS(status) } | ||
| 557 | else if (posix.W.IFSIGNALED(status)) | ||
| 558 | Term{ .Signal = posix.W.TERMSIG(status) } | ||
| 559 | else if (posix.W.IFSTOPPED(status)) | ||
| 560 | Term{ .Stopped = posix.W.STOPSIG(status) } | ||
| 561 | else | ||
| 562 | Term{ .Unknown = status }; | ||
| 563 | } | ||
| 564 | |||
| 565 | fn spawnPosix(self: *Child, io: Io) SpawnError!void { | ||
| 566 | // The child process does need to access (one end of) these pipes. However, | ||
| 567 | // we must initially set CLOEXEC to avoid a race condition. If another thread | ||
| 568 | // is racing to spawn a different child process, we don't want it to inherit | ||
| 569 | // these FDs in any scenario; that would mean that, for instance, calls to | ||
| 570 | // `poll` from the parent would not report the child's stdout as closing when | ||
| 571 | // expected, since the other child may retain a reference to the write end of | ||
| 572 | // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we | ||
| 573 | // need to do something in the new child to make sure we preserve the reference | ||
| 574 | // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it | ||
| 575 | // turns out, we `dup2` everything anyway, so there's no need! | ||
| 576 | const pipe_flags: posix.O = .{ .CLOEXEC = true }; | ||
| 577 | |||
| 578 | const stdin_pipe = if (self.stdin_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined; | ||
| 579 | errdefer if (self.stdin_behavior == .Pipe) { | ||
| 580 | destroyPipe(stdin_pipe); | ||
| 581 | }; | ||
| 582 | |||
| 583 | const stdout_pipe = if (self.stdout_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined; | ||
| 584 | errdefer if (self.stdout_behavior == .Pipe) { | ||
| 585 | destroyPipe(stdout_pipe); | ||
| 586 | }; | ||
| 587 | |||
| 588 | const stderr_pipe = if (self.stderr_behavior == .Pipe) try posix.pipe2(pipe_flags) else undefined; | ||
| 589 | errdefer if (self.stderr_behavior == .Pipe) { | ||
| 590 | destroyPipe(stderr_pipe); | ||
| 591 | }; | ||
| 592 | |||
| 593 | const any_ignore = (self.stdin_behavior == .Ignore or self.stdout_behavior == .Ignore or self.stderr_behavior == .Ignore); | ||
| 594 | const dev_null_fd = if (any_ignore) | ||
| 595 | posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) { | ||
| 596 | error.PathAlreadyExists => unreachable, | ||
| 597 | error.NoSpaceLeft => unreachable, | ||
| 598 | error.FileTooBig => unreachable, | ||
| 599 | error.DeviceBusy => unreachable, | ||
| 600 | error.FileLocksUnsupported => unreachable, | ||
| 601 | error.BadPathName => unreachable, // Windows-only | ||
| 602 | error.WouldBlock => unreachable, | ||
| 603 | error.NetworkNotFound => unreachable, // Windows-only | ||
| 604 | error.Canceled => unreachable, // temporarily in the posix error set | ||
| 605 | error.SharingViolation => unreachable, // Windows-only | ||
| 606 | error.PipeBusy => unreachable, // not a pipe | ||
| 607 | error.AntivirusInterference => unreachable, // Windows-only | ||
| 608 | else => |e| return e, | ||
| 609 | } | ||
| 610 | else | ||
| 611 | undefined; | ||
| 612 | defer { | ||
| 613 | if (any_ignore) posix.close(dev_null_fd); | ||
| 614 | } | ||
| 615 | |||
| 616 | const prog_pipe: [2]posix.fd_t = p: { | ||
| 617 | if (self.progress_node.index == .none) { | ||
| 618 | break :p .{ -1, -1 }; | ||
| 619 | } else { | ||
| 620 | // We use CLOEXEC for the same reason as in `pipe_flags`. | ||
| 621 | break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); | ||
| 622 | } | ||
| 623 | }; | ||
| 624 | errdefer destroyPipe(prog_pipe); | ||
| 625 | |||
| 626 | var arena_allocator = std.heap.ArenaAllocator.init(self.allocator); | ||
| 627 | defer arena_allocator.deinit(); | ||
| 628 | const arena = arena_allocator.allocator(); | ||
| 629 | |||
| 630 | // The POSIX standard does not allow malloc() between fork() and execve(), | ||
| 631 | // and `self.allocator` may be a libc allocator. | ||
| 632 | // I have personally observed the child process deadlocking when it tries | ||
| 633 | // to call malloc() due to a heap allocation between fork() and execve(), | ||
| 634 | // in musl v1.1.24. | ||
| 635 | // Additionally, we want to reduce the number of possible ways things | ||
| 636 | // can fail between fork() and execve(). | ||
| 637 | // Therefore, we do all the allocation for the execve() before the fork(). | ||
| 638 | // This means we must do the null-termination of argv and env vars here. | ||
| 639 | const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null); | ||
| 640 | for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; | ||
| 641 | |||
| 642 | const prog_fileno = 3; | ||
| 643 | comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno); | ||
| 644 | |||
| 645 | const envp: [*:null]const ?[*:0]const u8 = m: { | ||
| 646 | const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno; | ||
| 647 | switch (self.environ) { | ||
| 648 | .empty => break :m (try process.Environ.createBlock(.{ .block = &.{} }, arena, .{ | ||
| 649 | .zig_progress_fd = prog_fd, | ||
| 650 | })).ptr, | ||
| 651 | .inherit => |b| break :m (try b.createBlock(arena, .{ | ||
| 652 | .zig_progress_fd = prog_fd, | ||
| 653 | })).ptr, | ||
| 654 | .map => |m| break :m (try m.createBlock(arena, .{ | ||
| 655 | .zig_progress_fd = prog_fd, | ||
| 656 | })).ptr, | ||
| 657 | } | ||
| 658 | }; | ||
| 659 | |||
| 660 | // This pipe communicates to the parent errors in the child between `fork` and `execvpe`. | ||
| 661 | // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds. | ||
| 662 | const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true }); | ||
| 663 | errdefer destroyPipe(err_pipe); | ||
| 664 | |||
| 665 | const pid_result = try posix.fork(); | ||
| 666 | if (pid_result == 0) { | ||
| 667 | // we are the child | ||
| 668 | setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 669 | setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 670 | setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 671 | |||
| 672 | if (self.cwd_dir) |cwd| { | ||
| 673 | posix.fchdir(cwd.handle) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 674 | } else if (self.cwd) |cwd| { | ||
| 675 | posix.chdir(cwd) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 676 | } | ||
| 677 | |||
| 678 | // Must happen after fchdir above, the cwd file descriptor might be | ||
| 679 | // equal to prog_fileno and be clobbered by this dup2 call. | ||
| 680 | if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 681 | |||
| 682 | if (self.gid) |gid| { | ||
| 683 | posix.setregid(gid, gid) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 684 | } | ||
| 685 | |||
| 686 | if (self.uid) |uid| { | ||
| 687 | posix.setreuid(uid, uid) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 688 | } | ||
| 689 | |||
| 690 | if (self.pgid) |pid| { | ||
| 691 | posix.setpgid(0, pid) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 692 | } | ||
| 693 | |||
| 694 | if (self.start_suspended) { | ||
| 695 | posix.kill(posix.getpid(), .STOP) catch |err| forkChildErrReport(io, err_pipe[1], err); | ||
| 696 | } | ||
| 697 | |||
| 698 | const parent_PATH: ?[]const u8 = switch(self.environ) { | ||
| 699 | .empty => null, | ||
| 700 | .inherit => | ||
| 701 | .map => |m| m.get("PATH"), | ||
| 702 | }; | ||
| 703 | |||
| 704 | const err = switch (self.expand_arg0) { | ||
| 705 | .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp, parent_PATH), | ||
| 706 | .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp, parent_PATH), | ||
| 707 | }; | ||
| 708 | forkChildErrReport(io, err_pipe[1], err); | ||
| 709 | } | ||
| 710 | |||
| 711 | // we are the parent | ||
| 712 | errdefer comptime unreachable; // The child is forked; we must not error from now on | ||
| 713 | |||
| 714 | posix.close(err_pipe[1]); // make sure only the child holds the write end open | ||
| 715 | self.err_pipe = err_pipe[0]; | ||
| 716 | |||
| 717 | const pid: i32 = @intCast(pid_result); | ||
| 718 | if (self.stdin_behavior == .Pipe) { | ||
| 719 | self.stdin = .{ .handle = stdin_pipe[1] }; | ||
| 720 | } else { | ||
| 721 | self.stdin = null; | ||
| 722 | } | ||
| 723 | if (self.stdout_behavior == .Pipe) { | ||
| 724 | self.stdout = .{ .handle = stdout_pipe[0] }; | ||
| 725 | } else { | ||
| 726 | self.stdout = null; | ||
| 727 | } | ||
| 728 | if (self.stderr_behavior == .Pipe) { | ||
| 729 | self.stderr = .{ .handle = stderr_pipe[0] }; | ||
| 730 | } else { | ||
| 731 | self.stderr = null; | ||
| 732 | } | ||
| 733 | |||
| 734 | self.id = pid; | ||
| 735 | self.term = null; | ||
| 736 | |||
| 737 | if (self.stdin_behavior == .Pipe) { | ||
| 738 | posix.close(stdin_pipe[0]); | ||
| 739 | } | ||
| 740 | if (self.stdout_behavior == .Pipe) { | ||
| 741 | posix.close(stdout_pipe[1]); | ||
| 742 | } | ||
| 743 | if (self.stderr_behavior == .Pipe) { | ||
| 744 | posix.close(stderr_pipe[1]); | ||
| 745 | } | ||
| 746 | |||
| 747 | if (prog_pipe[1] != -1) { | ||
| 748 | posix.close(prog_pipe[1]); | ||
| 749 | } | ||
| 750 | self.progress_node.setIpcFd(prog_pipe[0]); | ||
| 751 | } | ||
| 752 | |||
| 753 | fn spawnWindows(self: *Child, io: Io) SpawnError!void { | ||
| 754 | var saAttr = windows.SECURITY_ATTRIBUTES{ | ||
| 755 | .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), | ||
| 756 | .bInheritHandle = windows.TRUE, | ||
| 757 | .lpSecurityDescriptor = null, | ||
| 758 | }; | ||
| 759 | |||
| 760 | const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); | ||
| 761 | |||
| 762 | const nul_handle = if (any_ignore) | ||
| 763 | // "\Device\Null" or "\??\NUL" | ||
| 764 | windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{ | ||
| 765 | .access_mask = .{ | ||
| 766 | .STANDARD = .{ .SYNCHRONIZE = true }, | ||
| 767 | .GENERIC = .{ .WRITE = true, .READ = true }, | ||
| 768 | }, | ||
| 769 | .sa = &saAttr, | ||
| 770 | .creation = .OPEN, | ||
| 771 | }) catch |err| switch (err) { | ||
| 772 | error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" | ||
| 773 | error.PipeBusy => return error.Unexpected, // not possible for "NUL" | ||
| 774 | error.NoDevice => return error.Unexpected, // not possible for "NUL" | ||
| 775 | error.FileNotFound => return error.Unexpected, // not possible for "NUL" | ||
| 776 | error.AccessDenied => return error.Unexpected, // not possible for "NUL" | ||
| 777 | error.NameTooLong => return error.Unexpected, // not possible for "NUL" | ||
| 778 | error.WouldBlock => return error.Unexpected, // not possible for "NUL" | ||
| 779 | error.NetworkNotFound => return error.Unexpected, // not possible for "NUL" | ||
| 780 | error.AntivirusInterference => return error.Unexpected, // not possible for "NUL" | ||
| 781 | error.OperationCanceled => return error.Unexpected, // we're not canceling the operation | ||
| 782 | else => |e| return e, | ||
| 783 | } | ||
| 784 | else | ||
| 785 | undefined; | ||
| 786 | defer { | ||
| 787 | if (any_ignore) posix.close(nul_handle); | ||
| 788 | } | ||
| 789 | |||
| 790 | var g_hChildStd_IN_Rd: ?windows.HANDLE = null; | ||
| 791 | var g_hChildStd_IN_Wr: ?windows.HANDLE = null; | ||
| 792 | switch (self.stdin_behavior) { | ||
| 793 | StdIo.Pipe => { | ||
| 794 | try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); | ||
| 795 | }, | ||
| 796 | StdIo.Ignore => { | ||
| 797 | g_hChildStd_IN_Rd = nul_handle; | ||
| 798 | }, | ||
| 799 | StdIo.Inherit => { | ||
| 800 | g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; | ||
| 801 | }, | ||
| 802 | StdIo.Close => { | ||
| 803 | g_hChildStd_IN_Rd = null; | ||
| 804 | }, | ||
| 805 | } | ||
| 806 | errdefer if (self.stdin_behavior == StdIo.Pipe) { | ||
| 807 | windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); | ||
| 808 | }; | ||
| 809 | |||
| 810 | var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; | ||
| 811 | var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; | ||
| 812 | switch (self.stdout_behavior) { | ||
| 813 | StdIo.Pipe => { | ||
| 814 | try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); | ||
| 815 | }, | ||
| 816 | StdIo.Ignore => { | ||
| 817 | g_hChildStd_OUT_Wr = nul_handle; | ||
| 818 | }, | ||
| 819 | StdIo.Inherit => { | ||
| 820 | g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; | ||
| 821 | }, | ||
| 822 | StdIo.Close => { | ||
| 823 | g_hChildStd_OUT_Wr = null; | ||
| 824 | }, | ||
| 825 | } | ||
| 826 | errdefer if (self.stdout_behavior == StdIo.Pipe) { | ||
| 827 | windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); | ||
| 828 | }; | ||
| 829 | |||
| 830 | var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; | ||
| 831 | var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; | ||
| 832 | switch (self.stderr_behavior) { | ||
| 833 | StdIo.Pipe => { | ||
| 834 | try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); | ||
| 835 | }, | ||
| 836 | StdIo.Ignore => { | ||
| 837 | g_hChildStd_ERR_Wr = nul_handle; | ||
| 838 | }, | ||
| 839 | StdIo.Inherit => { | ||
| 840 | g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; | ||
| 841 | }, | ||
| 842 | StdIo.Close => { | ||
| 843 | g_hChildStd_ERR_Wr = null; | ||
| 844 | }, | ||
| 845 | } | ||
| 846 | errdefer if (self.stderr_behavior == StdIo.Pipe) { | ||
| 847 | windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); | ||
| 848 | }; | ||
| 849 | |||
| 850 | var siStartInfo = windows.STARTUPINFOW{ | ||
| 851 | .cb = @sizeOf(windows.STARTUPINFOW), | ||
| 852 | .hStdError = g_hChildStd_ERR_Wr, | ||
| 853 | .hStdOutput = g_hChildStd_OUT_Wr, | ||
| 854 | .hStdInput = g_hChildStd_IN_Rd, | ||
| 855 | .dwFlags = windows.STARTF_USESTDHANDLES, | ||
| 856 | |||
| 857 | .lpReserved = null, | ||
| 858 | .lpDesktop = null, | ||
| 859 | .lpTitle = null, | ||
| 860 | .dwX = 0, | ||
| 861 | .dwY = 0, | ||
| 862 | .dwXSize = 0, | ||
| 863 | .dwYSize = 0, | ||
| 864 | .dwXCountChars = 0, | ||
| 865 | .dwYCountChars = 0, | ||
| 866 | .dwFillAttribute = 0, | ||
| 867 | .wShowWindow = 0, | ||
| 868 | .cbReserved2 = 0, | ||
| 869 | .lpReserved2 = null, | ||
| 870 | }; | ||
| 871 | var piProcInfo: windows.PROCESS_INFORMATION = undefined; | ||
| 872 | |||
| 873 | const cwd_w = if (self.cwd) |cwd| try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd) else null; | ||
| 874 | defer if (cwd_w) |cwd| self.allocator.free(cwd); | ||
| 875 | const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; | ||
| 876 | |||
| 877 | const maybe_envp_buf = if (self.env_map) |env_map| try process.createWindowsEnvBlock(self.allocator, env_map) else null; | ||
| 878 | defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf); | ||
| 879 | const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; | ||
| 880 | |||
| 881 | const app_name_wtf8 = self.argv[0]; | ||
| 882 | const app_name_is_absolute = fs.path.isAbsolute(app_name_wtf8); | ||
| 883 | |||
| 884 | // the cwd set in Child is in effect when choosing the executable path | ||
| 885 | // to match posix semantics | ||
| 886 | var cwd_path_w_needs_free = false; | ||
| 887 | const cwd_path_w = x: { | ||
| 888 | // If the app name is absolute, then we need to use its dirname as the cwd | ||
| 889 | if (app_name_is_absolute) { | ||
| 890 | cwd_path_w_needs_free = true; | ||
| 891 | const dir = fs.path.dirname(app_name_wtf8).?; | ||
| 892 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, dir); | ||
| 893 | } else if (self.cwd) |cwd| { | ||
| 894 | cwd_path_w_needs_free = true; | ||
| 895 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd); | ||
| 896 | } else { | ||
| 897 | break :x &[_:0]u16{}; // empty for cwd | ||
| 898 | } | ||
| 899 | }; | ||
| 900 | defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); | ||
| 901 | |||
| 902 | // If the app name has more than just a filename, then we need to separate that | ||
| 903 | // into the basename and dirname and use the dirname as an addition to the cwd | ||
| 904 | // path. This is because NtQueryDirectoryFile cannot accept FileName params with | ||
| 905 | // path separators. | ||
| 906 | const app_basename_wtf8 = fs.path.basename(app_name_wtf8); | ||
| 907 | // If the app name is absolute, then the cwd will already have the app's dirname in it, | ||
| 908 | // so only populate app_dirname if app name is a relative path with > 0 path separators. | ||
| 909 | const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_wtf8) else null; | ||
| 910 | const app_dirname_w: ?[:0]u16 = x: { | ||
| 911 | if (maybe_app_dirname_wtf8) |app_dirname_wtf8| { | ||
| 912 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_dirname_wtf8); | ||
| 913 | } | ||
| 914 | break :x null; | ||
| 915 | }; | ||
| 916 | defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); | ||
| 917 | |||
| 918 | const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8); | ||
| 919 | defer self.allocator.free(app_name_w); | ||
| 920 | |||
| 921 | const flags: windows.CreateProcessFlags = .{ | ||
| 922 | .create_suspended = self.start_suspended, | ||
| 923 | .create_unicode_environment = true, | ||
| 924 | .create_no_window = self.create_no_window, | ||
| 925 | }; | ||
| 926 | |||
| 927 | run: { | ||
| 928 | const PATH: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; | ||
| 929 | const PATHEXT: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; | ||
| 930 | |||
| 931 | // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules | ||
| 932 | // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously | ||
| 933 | // constructed arguments. | ||
| 934 | // | ||
| 935 | // We'll need to wait until we're actually trying to run the command to know for sure | ||
| 936 | // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually | ||
| 937 | // serializing the command line until we determine how it should be serialized. | ||
| 938 | var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv); | ||
| 939 | defer cmd_line_cache.deinit(); | ||
| 940 | |||
| 941 | var app_buf: ArrayList(u16) = .empty; | ||
| 942 | defer app_buf.deinit(self.allocator); | ||
| 943 | |||
| 944 | try app_buf.appendSlice(self.allocator, app_name_w); | ||
| 945 | |||
| 946 | var dir_buf: ArrayList(u16) = .empty; | ||
| 947 | defer dir_buf.deinit(self.allocator); | ||
| 948 | |||
| 949 | if (cwd_path_w.len > 0) { | ||
| 950 | try dir_buf.appendSlice(self.allocator, cwd_path_w); | ||
| 951 | } | ||
| 952 | if (app_dirname_w) |app_dir| { | ||
| 953 | if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); | ||
| 954 | try dir_buf.appendSlice(self.allocator, app_dir); | ||
| 955 | } | ||
| 956 | |||
| 957 | windowsCreateProcessPathExt(self.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo) catch |no_path_err| { | ||
| 958 | const original_err = switch (no_path_err) { | ||
| 959 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | ||
| 960 | error.InvalidArg0 => return error.FileNotFound, | ||
| 961 | error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, | ||
| 962 | error.UnrecoverableInvalidExe => return error.InvalidExe, | ||
| 963 | else => |e| return e, | ||
| 964 | }; | ||
| 965 | |||
| 966 | // If the app name had path separators, that disallows PATH searching, | ||
| 967 | // and there's no need to search the PATH if the app name is absolute. | ||
| 968 | // We still search the path if the cwd is absolute because of the | ||
| 969 | // "cwd set in Child is in effect when choosing the executable path | ||
| 970 | // to match posix semantics" behavior--we don't want to skip searching | ||
| 971 | // the PATH just because we were trying to set the cwd of the child process. | ||
| 972 | if (app_dirname_w != null or app_name_is_absolute) { | ||
| 973 | return original_err; | ||
| 974 | } | ||
| 975 | |||
| 976 | var it = mem.tokenizeScalar(u16, PATH, ';'); | ||
| 977 | while (it.next()) |search_path| { | ||
| 978 | dir_buf.clearRetainingCapacity(); | ||
| 979 | try dir_buf.appendSlice(self.allocator, search_path); | ||
| 980 | |||
| 981 | if (windowsCreateProcessPathExt(self.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo)) { | ||
| 982 | break :run; | ||
| 983 | } else |err| switch (err) { | ||
| 984 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | ||
| 985 | error.InvalidArg0 => return error.FileNotFound, | ||
| 986 | error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, | ||
| 987 | error.UnrecoverableInvalidExe => return error.InvalidExe, | ||
| 988 | else => |e| return e, | ||
| 989 | } | ||
| 990 | } else { | ||
| 991 | return original_err; | ||
| 992 | } | ||
| 993 | }; | ||
| 994 | } | ||
| 995 | |||
| 996 | if (g_hChildStd_IN_Wr) |h| { | ||
| 997 | self.stdin = File{ .handle = h }; | ||
| 998 | } else { | ||
| 999 | self.stdin = null; | ||
| 1000 | } | ||
| 1001 | if (g_hChildStd_OUT_Rd) |h| { | ||
| 1002 | self.stdout = File{ .handle = h }; | ||
| 1003 | } else { | ||
| 1004 | self.stdout = null; | ||
| 1005 | } | ||
| 1006 | if (g_hChildStd_ERR_Rd) |h| { | ||
| 1007 | self.stderr = File{ .handle = h }; | ||
| 1008 | } else { | ||
| 1009 | self.stderr = null; | ||
| 1010 | } | ||
| 1011 | |||
| 1012 | self.id = piProcInfo.hProcess; | ||
| 1013 | self.thread_handle = piProcInfo.hThread; | ||
| 1014 | self.term = null; | ||
| 1015 | |||
| 1016 | if (self.stdin_behavior == StdIo.Pipe) { | ||
| 1017 | posix.close(g_hChildStd_IN_Rd.?); | ||
| 1018 | } | ||
| 1019 | if (self.stderr_behavior == StdIo.Pipe) { | ||
| 1020 | posix.close(g_hChildStd_ERR_Wr.?); | ||
| 1021 | } | ||
| 1022 | if (self.stdout_behavior == StdIo.Pipe) { | ||
| 1023 | posix.close(g_hChildStd_OUT_Wr.?); | ||
| 1024 | } | ||
| 1025 | } | ||
| 1026 | |||
| 1027 | fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { | ||
| 1028 | switch (stdio) { | ||
| 1029 | .Pipe => try posix.dup2(pipe_fd, std_fileno), | ||
| 1030 | .Close => posix.close(std_fileno), | ||
| 1031 | .Inherit => {}, | ||
| 1032 | .Ignore => try posix.dup2(dev_null_fd, std_fileno), | ||
| 1033 | } | ||
| 1034 | } | ||
| 1035 | |||
| 1036 | fn destroyPipe(pipe: [2]posix.fd_t) void { | ||
| 1037 | if (pipe[0] != -1) posix.close(pipe[0]); | ||
| 1038 | if (pipe[0] != pipe[1]) posix.close(pipe[1]); | ||
| 1039 | } | ||
| 1040 | |||
| 1041 | // Child of fork calls this to report an error to the fork parent. | ||
| 1042 | // Then the child exits. | ||
| 1043 | fn forkChildErrReport(io: Io, fd: i32, err: Child.SpawnError) noreturn { | ||
| 1044 | writeIntFd(io, fd, @as(ErrInt, @intFromError(err))) catch {}; | ||
| 1045 | // If we're linking libc, some naughty applications may have registered atexit handlers | ||
| 1046 | // which we really do not want to run in the fork child. I caught LLVM doing this and | ||
| 1047 | // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne, | ||
| 1048 | // "Why'd you have to go and make things so complicated?" | ||
| 1049 | if (builtin.link_libc) { | ||
| 1050 | // The _exit(2) function does nothing but make the exit syscall, unlike exit(3) | ||
| 1051 | std.c._exit(1); | ||
| 1052 | } | ||
| 1053 | posix.system.exit(1); | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | fn writeIntFd(io: Io, fd: i32, value: ErrInt) !void { | ||
| 1057 | var buffer: [8]u8 = undefined; | ||
| 1058 | var fw: File.Writer = .initStreaming(.{ .handle = fd }, io, &buffer); | ||
| 1059 | fw.interface.writeInt(u64, value, .little) catch unreachable; | ||
| 1060 | fw.interface.flush() catch return error.SystemResources; | ||
| 1061 | } | ||
| 1062 | |||
| 1063 | fn readIntFd(fd: i32) !ErrInt { | ||
| 1064 | var buffer: [8]u8 = undefined; | ||
| 1065 | var i: usize = 0; | ||
| 1066 | while (i < buffer.len) { | ||
| 1067 | const n = try std.posix.read(fd, buffer[i..]); | ||
| 1068 | if (n == 0) return error.EndOfStream; | ||
| 1069 | i += n; | ||
| 1070 | } | ||
| 1071 | const int = mem.readInt(u64, &buffer, .little); | ||
| 1072 | return @intCast(int); | ||
| 1073 | } | ||
| 1074 | |||
| 1075 | const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); | ||
| 1076 | |||
| 1077 | /// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. | ||
| 1078 | /// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. | ||
| 1079 | /// Note: `app_buf` should not contain any leading path separators. | ||
| 1080 | /// Note: If the dir is the cwd, dir_buf should be empty (len = 0). | ||
| 1081 | fn windowsCreateProcessPathExt( | ||
| 1082 | allocator: Allocator, | ||
| 1083 | io: Io, | ||
| 1084 | dir_buf: *ArrayList(u16), | ||
| 1085 | app_buf: *ArrayList(u16), | ||
| 1086 | pathext: [:0]const u16, | ||
| 1087 | cmd_line_cache: *WindowsCommandLineCache, | ||
| 1088 | envp_ptr: ?[*]u16, | ||
| 1089 | cwd_ptr: ?[*:0]u16, | ||
| 1090 | flags: windows.CreateProcessFlags, | ||
| 1091 | lpStartupInfo: *windows.STARTUPINFOW, | ||
| 1092 | lpProcessInformation: *windows.PROCESS_INFORMATION, | ||
| 1093 | ) !void { | ||
| 1094 | const app_name_len = app_buf.items.len; | ||
| 1095 | const dir_path_len = dir_buf.items.len; | ||
| 1096 | |||
| 1097 | if (app_name_len == 0) return error.FileNotFound; | ||
| 1098 | |||
| 1099 | defer app_buf.shrinkRetainingCapacity(app_name_len); | ||
| 1100 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | ||
| 1101 | |||
| 1102 | // The name of the game here is to avoid CreateProcessW calls at all costs, | ||
| 1103 | // and only ever try calling it when we have a real candidate for execution. | ||
| 1104 | // Secondarily, we want to minimize the number of syscalls used when checking | ||
| 1105 | // for each PATHEXT-appended version of the app name. | ||
| 1106 | // | ||
| 1107 | // An overview of the technique used: | ||
| 1108 | // - Open the search directory for iteration (either cwd or a path from PATH) | ||
| 1109 | // - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to | ||
| 1110 | // check if anything that could possibly match either the unappended version | ||
| 1111 | // of the app name or any of the versions with a PATHEXT value appended exists. | ||
| 1112 | // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early | ||
| 1113 | // without needing to use PATHEXT at all. | ||
| 1114 | // | ||
| 1115 | // This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence | ||
| 1116 | // for any directory that doesn't contain any possible matches, instead of having | ||
| 1117 | // to use a separate look up for each individual filename combination (unappended + | ||
| 1118 | // each PATHEXT appended). For directories where the wildcard *does* match something, | ||
| 1119 | // we iterate the matches and take note of any that are either the unappended version, | ||
| 1120 | // or a version with a supported PATHEXT appended. We then try calling CreateProcessW | ||
| 1121 | // with the found versions in the appropriate order. | ||
| 1122 | |||
| 1123 | // In the future, child process execution needs to move to Io implementation. | ||
| 1124 | // Under those conditions, here we will have access to lower level directory | ||
| 1125 | // opening function knowing which implementation we are in. Here, we imitate | ||
| 1126 | // that scenario. | ||
| 1127 | var dir = dir: { | ||
| 1128 | // needs to be null-terminated | ||
| 1129 | try dir_buf.append(allocator, 0); | ||
| 1130 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | ||
| 1131 | const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | ||
| 1132 | const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); | ||
| 1133 | break :dir Io.Threaded.dirOpenDirWindows(.cwd(), prefixed_path.span(), .{ | ||
| 1134 | .iterate = true, | ||
| 1135 | }) catch return error.FileNotFound; | ||
| 1136 | }; | ||
| 1137 | defer dir.close(io); | ||
| 1138 | |||
| 1139 | // Add wildcard and null-terminator | ||
| 1140 | try app_buf.append(allocator, '*'); | ||
| 1141 | try app_buf.append(allocator, 0); | ||
| 1142 | const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; | ||
| 1143 | |||
| 1144 | // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries | ||
| 1145 | // returned per NtQueryDirectoryFile call. | ||
| 1146 | var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined; | ||
| 1147 | const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); | ||
| 1148 | if (file_information_buf.len < file_info_maximum_single_entry_size) { | ||
| 1149 | @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); | ||
| 1150 | } | ||
| 1151 | var io_status: windows.IO_STATUS_BLOCK = undefined; | ||
| 1152 | |||
| 1153 | const num_supported_pathext = @typeInfo(WindowsExtension).@"enum".fields.len; | ||
| 1154 | var pathext_seen = [_]bool{false} ** num_supported_pathext; | ||
| 1155 | var any_pathext_seen = false; | ||
| 1156 | var unappended_exists = false; | ||
| 1157 | |||
| 1158 | // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions | ||
| 1159 | // of the app_name we should try to spawn. | ||
| 1160 | // Note: This is necessary because the order of the files returned is filesystem-dependent: | ||
| 1161 | // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. | ||
| 1162 | // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. | ||
| 1163 | while (true) { | ||
| 1164 | const app_name_len_bytes = std.math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; | ||
| 1165 | var app_name_unicode_string = windows.UNICODE_STRING{ | ||
| 1166 | .Length = app_name_len_bytes, | ||
| 1167 | .MaximumLength = app_name_len_bytes, | ||
| 1168 | .Buffer = @constCast(app_name_wildcard.ptr), | ||
| 1169 | }; | ||
| 1170 | const rc = windows.ntdll.NtQueryDirectoryFile( | ||
| 1171 | dir.handle, | ||
| 1172 | null, | ||
| 1173 | null, | ||
| 1174 | null, | ||
| 1175 | &io_status, | ||
| 1176 | &file_information_buf, | ||
| 1177 | file_information_buf.len, | ||
| 1178 | .Directory, | ||
| 1179 | windows.FALSE, // single result | ||
| 1180 | &app_name_unicode_string, | ||
| 1181 | windows.FALSE, // restart iteration | ||
| 1182 | ); | ||
| 1183 | |||
| 1184 | // If we get nothing with the wildcard, then we can just bail out | ||
| 1185 | // as we know appending PATHEXT will not yield anything. | ||
| 1186 | switch (rc) { | ||
| 1187 | .SUCCESS => {}, | ||
| 1188 | .NO_SUCH_FILE => return error.FileNotFound, | ||
| 1189 | .NO_MORE_FILES => break, | ||
| 1190 | .ACCESS_DENIED => return error.AccessDenied, | ||
| 1191 | else => return windows.unexpectedStatus(rc), | ||
| 1192 | } | ||
| 1193 | |||
| 1194 | // According to the docs, this can only happen if there is not enough room in the | ||
| 1195 | // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. | ||
| 1196 | // Therefore, this condition should not be possible to hit with the buffer size we use. | ||
| 1197 | std.debug.assert(io_status.Information != 0); | ||
| 1198 | |||
| 1199 | var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; | ||
| 1200 | while (it.next()) |info| { | ||
| 1201 | // Skip directories | ||
| 1202 | if (info.FileAttributes.DIRECTORY) continue; | ||
| 1203 | const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; | ||
| 1204 | // Because all results start with the app_name since we're using the wildcard `app_name*`, | ||
| 1205 | // if the length is equal to app_name then this is an exact match | ||
| 1206 | if (filename.len == app_name_len) { | ||
| 1207 | // Note: We can't break early here because it's possible that the unappended version | ||
| 1208 | // fails to spawn, in which case we still want to try the PATHEXT appended versions. | ||
| 1209 | unappended_exists = true; | ||
| 1210 | } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { | ||
| 1211 | pathext_seen[@intFromEnum(pathext_ext)] = true; | ||
| 1212 | any_pathext_seen = true; | ||
| 1213 | } | ||
| 1214 | } | ||
| 1215 | } | ||
| 1216 | |||
| 1217 | const unappended_err = unappended: { | ||
| 1218 | if (unappended_exists) { | ||
| 1219 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | ||
| 1220 | '/', '\\' => {}, | ||
| 1221 | else => try dir_buf.append(allocator, fs.path.sep), | ||
| 1222 | }; | ||
| 1223 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | ||
| 1224 | try dir_buf.append(allocator, 0); | ||
| 1225 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | ||
| 1226 | |||
| 1227 | const is_bat_or_cmd = bat_or_cmd: { | ||
| 1228 | const app_name = app_buf.items[0..app_name_len]; | ||
| 1229 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false; | ||
| 1230 | const ext = app_name[ext_start..]; | ||
| 1231 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false; | ||
| 1232 | switch (ext_enum) { | ||
| 1233 | .cmd, .bat => break :bat_or_cmd true, | ||
| 1234 | else => break :bat_or_cmd false, | ||
| 1235 | } | ||
| 1236 | }; | ||
| 1237 | const cmd_line_w = if (is_bat_or_cmd) | ||
| 1238 | try cmd_line_cache.scriptCommandLine(full_app_name) | ||
| 1239 | else | ||
| 1240 | try cmd_line_cache.commandLine(); | ||
| 1241 | const app_name_w = if (is_bat_or_cmd) | ||
| 1242 | try cmd_line_cache.cmdExePath() | ||
| 1243 | else | ||
| 1244 | full_app_name; | ||
| 1245 | |||
| 1246 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| { | ||
| 1247 | return; | ||
| 1248 | } else |err| switch (err) { | ||
| 1249 | error.FileNotFound, | ||
| 1250 | error.AccessDenied, | ||
| 1251 | => break :unappended err, | ||
| 1252 | error.InvalidExe => { | ||
| 1253 | // On InvalidExe, if the extension of the app name is .exe then | ||
| 1254 | // it's treated as an unrecoverable error. Otherwise, it'll be | ||
| 1255 | // skipped as normal. | ||
| 1256 | const app_name = app_buf.items[0..app_name_len]; | ||
| 1257 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; | ||
| 1258 | const ext = app_name[ext_start..]; | ||
| 1259 | if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | ||
| 1260 | return error.UnrecoverableInvalidExe; | ||
| 1261 | } | ||
| 1262 | break :unappended err; | ||
| 1263 | }, | ||
| 1264 | else => return err, | ||
| 1265 | } | ||
| 1266 | } | ||
| 1267 | break :unappended error.FileNotFound; | ||
| 1268 | }; | ||
| 1269 | |||
| 1270 | if (!any_pathext_seen) return unappended_err; | ||
| 1271 | |||
| 1272 | // Now try any PATHEXT appended versions that we've seen | ||
| 1273 | var ext_it = mem.tokenizeScalar(u16, pathext, ';'); | ||
| 1274 | while (ext_it.next()) |ext| { | ||
| 1275 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; | ||
| 1276 | if (!pathext_seen[@intFromEnum(ext_enum)]) continue; | ||
| 1277 | |||
| 1278 | dir_buf.shrinkRetainingCapacity(dir_path_len); | ||
| 1279 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | ||
| 1280 | '/', '\\' => {}, | ||
| 1281 | else => try dir_buf.append(allocator, fs.path.sep), | ||
| 1282 | }; | ||
| 1283 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | ||
| 1284 | try dir_buf.appendSlice(allocator, ext); | ||
| 1285 | try dir_buf.append(allocator, 0); | ||
| 1286 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | ||
| 1287 | |||
| 1288 | const is_bat_or_cmd = switch (ext_enum) { | ||
| 1289 | .cmd, .bat => true, | ||
| 1290 | else => false, | ||
| 1291 | }; | ||
| 1292 | const cmd_line_w = if (is_bat_or_cmd) | ||
| 1293 | try cmd_line_cache.scriptCommandLine(full_app_name) | ||
| 1294 | else | ||
| 1295 | try cmd_line_cache.commandLine(); | ||
| 1296 | const app_name_w = if (is_bat_or_cmd) | ||
| 1297 | try cmd_line_cache.cmdExePath() | ||
| 1298 | else | ||
| 1299 | full_app_name; | ||
| 1300 | |||
| 1301 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| { | ||
| 1302 | return; | ||
| 1303 | } else |err| switch (err) { | ||
| 1304 | error.FileNotFound => continue, | ||
| 1305 | error.AccessDenied => continue, | ||
| 1306 | error.InvalidExe => { | ||
| 1307 | // On InvalidExe, if the extension of the app name is .exe then | ||
| 1308 | // it's treated as an unrecoverable error. Otherwise, it'll be | ||
| 1309 | // skipped as normal. | ||
| 1310 | if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | ||
| 1311 | return error.UnrecoverableInvalidExe; | ||
| 1312 | } | ||
| 1313 | continue; | ||
| 1314 | }, | ||
| 1315 | else => return err, | ||
| 1316 | } | ||
| 1317 | } | ||
| 1318 | |||
| 1319 | return unappended_err; | ||
| 1320 | } | ||
| 1321 | |||
| 1322 | fn windowsCreateProcess( | ||
| 1323 | app_name: [*:0]u16, | ||
| 1324 | cmd_line: [*:0]u16, | ||
| 1325 | envp_ptr: ?[*]u16, | ||
| 1326 | cwd_ptr: ?[*:0]u16, | ||
| 1327 | flags: windows.CreateProcessFlags, | ||
| 1328 | lpStartupInfo: *windows.STARTUPINFOW, | ||
| 1329 | lpProcessInformation: *windows.PROCESS_INFORMATION, | ||
| 1330 | ) !void { | ||
| 1331 | // TODO the docs for environment pointer say: | ||
| 1332 | // > A pointer to the environment block for the new process. If this parameter | ||
| 1333 | // > is NULL, the new process uses the environment of the calling process. | ||
| 1334 | // > ... | ||
| 1335 | // > An environment block can contain either Unicode or ANSI characters. If | ||
| 1336 | // > the environment block pointed to by lpEnvironment contains Unicode | ||
| 1337 | // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. | ||
| 1338 | // > If this parameter is NULL and the environment block of the parent process | ||
| 1339 | // > contains Unicode characters, you must also ensure that dwCreationFlags | ||
| 1340 | // > includes CREATE_UNICODE_ENVIRONMENT. | ||
| 1341 | // This seems to imply that we have to somehow know whether our process parent passed | ||
| 1342 | // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter. | ||
| 1343 | // Since we do not know this information that would imply that we must not pass NULL | ||
| 1344 | // for the parameter. | ||
| 1345 | // However this would imply that programs compiled with -DUNICODE could not pass | ||
| 1346 | // environment variables to programs that were not, which seems unlikely. | ||
| 1347 | // More investigation is needed. | ||
| 1348 | return windows.CreateProcessW( | ||
| 1349 | app_name, | ||
| 1350 | cmd_line, | ||
| 1351 | null, | ||
| 1352 | null, | ||
| 1353 | windows.TRUE, | ||
| 1354 | flags, | ||
| 1355 | @as(?*anyopaque, @ptrCast(envp_ptr)), | ||
| 1356 | cwd_ptr, | ||
| 1357 | lpStartupInfo, | ||
| 1358 | lpProcessInformation, | ||
| 1359 | ); | ||
| 1360 | } | ||
| 1361 | |||
| 1362 | fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | ||
| 1363 | var rd_h: windows.HANDLE = undefined; | ||
| 1364 | var wr_h: windows.HANDLE = undefined; | ||
| 1365 | try windows.CreatePipe(&rd_h, &wr_h, sattr); | ||
| 1366 | errdefer windowsDestroyPipe(rd_h, wr_h); | ||
| 1367 | try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); | ||
| 1368 | rd.* = rd_h; | ||
| 1369 | wr.* = wr_h; | ||
| 1370 | } | ||
| 1371 | |||
| 1372 | fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { | ||
| 1373 | if (rd) |h| posix.close(h); | ||
| 1374 | if (wr) |h| posix.close(h); | ||
| 1375 | } | ||
| 1376 | |||
| 1377 | fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | ||
| 1378 | var tmp_bufw: [128]u16 = undefined; | ||
| 1379 | |||
| 1380 | // Anonymous pipes are built upon Named pipes. | ||
| 1381 | // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe | ||
| 1382 | // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. | ||
| 1383 | // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations | ||
| 1384 | const pipe_path = blk: { | ||
| 1385 | var tmp_buf: [128]u8 = undefined; | ||
| 1386 | // Forge a random path for the pipe. | ||
| 1387 | const pipe_path = std.fmt.bufPrintSentinel( | ||
| 1388 | &tmp_buf, | ||
| 1389 | "\\\\.\\pipe\\zig-childprocess-{d}-{d}", | ||
| 1390 | .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, | ||
| 1391 | 0, | ||
| 1392 | ) catch unreachable; | ||
| 1393 | const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable; | ||
| 1394 | tmp_bufw[len] = 0; | ||
| 1395 | break :blk tmp_bufw[0..len :0]; | ||
| 1396 | }; | ||
| 1397 | |||
| 1398 | // Create the read handle that can be used with overlapped IO ops. | ||
| 1399 | const read_handle = windows.kernel32.CreateNamedPipeW( | ||
| 1400 | pipe_path.ptr, | ||
| 1401 | windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, | ||
| 1402 | windows.PIPE_TYPE_BYTE, | ||
| 1403 | 1, | ||
| 1404 | 4096, | ||
| 1405 | 4096, | ||
| 1406 | 0, | ||
| 1407 | sattr, | ||
| 1408 | ); | ||
| 1409 | if (read_handle == windows.INVALID_HANDLE_VALUE) { | ||
| 1410 | switch (windows.GetLastError()) { | ||
| 1411 | else => |err| return windows.unexpectedError(err), | ||
| 1412 | } | ||
| 1413 | } | ||
| 1414 | errdefer posix.close(read_handle); | ||
| 1415 | |||
| 1416 | var sattr_copy = sattr.*; | ||
| 1417 | const write_handle = windows.kernel32.CreateFileW( | ||
| 1418 | pipe_path.ptr, | ||
| 1419 | .{ .GENERIC = .{ .WRITE = true } }, | ||
| 1420 | 0, | ||
| 1421 | &sattr_copy, | ||
| 1422 | windows.OPEN_EXISTING, | ||
| 1423 | @bitCast(windows.FILE.ATTRIBUTE{ .NORMAL = true }), | ||
| 1424 | null, | ||
| 1425 | ); | ||
| 1426 | if (write_handle == windows.INVALID_HANDLE_VALUE) { | ||
| 1427 | switch (windows.GetLastError()) { | ||
| 1428 | else => |err| return windows.unexpectedError(err), | ||
| 1429 | } | ||
| 1430 | } | ||
| 1431 | errdefer posix.close(write_handle); | ||
| 1432 | |||
| 1433 | try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); | ||
| 1434 | |||
| 1435 | rd.* = read_handle; | ||
| 1436 | wr.* = write_handle; | ||
| 1437 | } | ||
| 1438 | |||
| 1439 | var pipe_name_counter = std.atomic.Value(u32).init(1); | ||
| 1440 | |||
| 1441 | /// File name extensions supported natively by `CreateProcess()` on Windows. | ||
| 1442 | // Should be kept in sync with `windowsCreateProcessSupportsExtension`. | ||
| 1443 | pub const WindowsExtension = enum { | ||
| 1444 | bat, | ||
| 1445 | cmd, | ||
| 1446 | com, | ||
| 1447 | exe, | ||
| 1448 | }; | ||
| 1449 | |||
| 1450 | /// Case-insensitive WTF-16 lookup | ||
| 1451 | fn windowsCreateProcessSupportsExtension(ext: []const u16) ?WindowsExtension { | ||
| 1452 | if (ext.len != 4) return null; | ||
| 1453 | const State = enum { | ||
| 1454 | start, | ||
| 1455 | dot, | ||
| 1456 | b, | ||
| 1457 | ba, | ||
| 1458 | c, | ||
| 1459 | cm, | ||
| 1460 | co, | ||
| 1461 | e, | ||
| 1462 | ex, | ||
| 1463 | }; | ||
| 1464 | var state: State = .start; | ||
| 1465 | for (ext) |c| switch (state) { | ||
| 1466 | .start => switch (c) { | ||
| 1467 | '.' => state = .dot, | ||
| 1468 | else => return null, | ||
| 1469 | }, | ||
| 1470 | .dot => switch (c) { | ||
| 1471 | 'b', 'B' => state = .b, | ||
| 1472 | 'c', 'C' => state = .c, | ||
| 1473 | 'e', 'E' => state = .e, | ||
| 1474 | else => return null, | ||
| 1475 | }, | ||
| 1476 | .b => switch (c) { | ||
| 1477 | 'a', 'A' => state = .ba, | ||
| 1478 | else => return null, | ||
| 1479 | }, | ||
| 1480 | .c => switch (c) { | ||
| 1481 | 'm', 'M' => state = .cm, | ||
| 1482 | 'o', 'O' => state = .co, | ||
| 1483 | else => return null, | ||
| 1484 | }, | ||
| 1485 | .e => switch (c) { | ||
| 1486 | 'x', 'X' => state = .ex, | ||
| 1487 | else => return null, | ||
| 1488 | }, | ||
| 1489 | .ba => switch (c) { | ||
| 1490 | 't', 'T' => return .bat, | ||
| 1491 | else => return null, | ||
| 1492 | }, | ||
| 1493 | .cm => switch (c) { | ||
| 1494 | 'd', 'D' => return .cmd, | ||
| 1495 | else => return null, | ||
| 1496 | }, | ||
| 1497 | .co => switch (c) { | ||
| 1498 | 'm', 'M' => return .com, | ||
| 1499 | else => return null, | ||
| 1500 | }, | ||
| 1501 | .ex => switch (c) { | ||
| 1502 | 'e', 'E' => return .exe, | ||
| 1503 | else => return null, | ||
| 1504 | }, | ||
| 1505 | }; | ||
| 1506 | return null; | ||
| 1507 | } | ||
| 1508 | |||
| 1509 | test windowsCreateProcessSupportsExtension { | ||
| 1510 | try std.testing.expectEqual(WindowsExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?); | ||
| 1511 | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); | ||
| 1512 | } | ||
| 1513 | |||
| 1514 | /// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW. | ||
| 1515 | /// | ||
| 1516 | /// Serialization is done on-demand and the result is cached in order to allow for: | ||
| 1517 | /// - Only serializing the particular type of command line needed (`.bat`/`.cmd` | ||
| 1518 | /// command line serialization is different from `.exe`/etc) | ||
| 1519 | /// - Reusing the serialized command lines if necessary (i.e. if the execution | ||
| 1520 | /// of a command fails and the PATH is going to be continued to be searched | ||
| 1521 | /// for more candidates) | ||
| 1522 | const WindowsCommandLineCache = struct { | ||
| 1523 | cmd_line: ?[:0]u16 = null, | ||
| 1524 | script_cmd_line: ?[:0]u16 = null, | ||
| 1525 | cmd_exe_path: ?[:0]u16 = null, | ||
| 1526 | argv: []const []const u8, | ||
| 1527 | allocator: Allocator, | ||
| 1528 | |||
| 1529 | fn init(allocator: Allocator, argv: []const []const u8) WindowsCommandLineCache { | ||
| 1530 | return .{ | ||
| 1531 | .allocator = allocator, | ||
| 1532 | .argv = argv, | ||
| 1533 | }; | ||
| 1534 | } | ||
| 1535 | |||
| 1536 | fn deinit(self: *WindowsCommandLineCache) void { | ||
| 1537 | if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line); | ||
| 1538 | if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line); | ||
| 1539 | if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path); | ||
| 1540 | } | ||
| 1541 | |||
| 1542 | fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 { | ||
| 1543 | if (self.cmd_line == null) { | ||
| 1544 | self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv); | ||
| 1545 | } | ||
| 1546 | return self.cmd_line.?; | ||
| 1547 | } | ||
| 1548 | |||
| 1549 | /// Not cached, since the path to the batch script will change during PATH searching. | ||
| 1550 | /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched, | ||
| 1551 | /// then script_path should include both the search path and the script filename | ||
| 1552 | /// (this allows avoiding cmd.exe having to search the PATH again). | ||
| 1553 | fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 { | ||
| 1554 | if (self.script_cmd_line) |v| self.allocator.free(v); | ||
| 1555 | self.script_cmd_line = try argvToScriptCommandLineWindows( | ||
| 1556 | self.allocator, | ||
| 1557 | script_path, | ||
| 1558 | self.argv[1..], | ||
| 1559 | ); | ||
| 1560 | return self.script_cmd_line.?; | ||
| 1561 | } | ||
| 1562 | |||
| 1563 | fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 { | ||
| 1564 | if (self.cmd_exe_path == null) { | ||
| 1565 | self.cmd_exe_path = try windowsCmdExePath(self.allocator); | ||
| 1566 | } | ||
| 1567 | return self.cmd_exe_path.?; | ||
| 1568 | } | ||
| 1569 | }; | ||
| 1570 | |||
| 1571 | /// Returns the absolute path of `cmd.exe` within the Windows system directory. | ||
| 1572 | /// The caller owns the returned slice. | ||
| 1573 | fn windowsCmdExePath(allocator: Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { | ||
| 1574 | var buf = try ArrayList(u16).initCapacity(allocator, 128); | ||
| 1575 | errdefer buf.deinit(allocator); | ||
| 1576 | while (true) { | ||
| 1577 | const unused_slice = buf.unusedCapacitySlice(); | ||
| 1578 | // TODO: Get the system directory from PEB.ReadOnlyStaticServerData | ||
| 1579 | const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len)); | ||
| 1580 | if (len == 0) { | ||
| 1581 | switch (windows.GetLastError()) { | ||
| 1582 | else => |err| return windows.unexpectedError(err), | ||
| 1583 | } | ||
| 1584 | } | ||
| 1585 | if (len > unused_slice.len) { | ||
| 1586 | try buf.ensureUnusedCapacity(allocator, len); | ||
| 1587 | } else { | ||
| 1588 | buf.items.len = len; | ||
| 1589 | break; | ||
| 1590 | } | ||
| 1591 | } | ||
| 1592 | switch (buf.items[buf.items.len - 1]) { | ||
| 1593 | '/', '\\' => {}, | ||
| 1594 | else => try buf.append(allocator, fs.path.sep), | ||
| 1595 | } | ||
| 1596 | try buf.appendSlice(allocator, unicode.utf8ToUtf16LeStringLiteral("cmd.exe")); | ||
| 1597 | return try buf.toOwnedSliceSentinel(allocator, 0); | ||
| 1598 | } | ||
| 1599 | |||
| 1600 | const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 }; | ||
| 1601 | |||
| 1602 | /// Serializes `argv` to a Windows command-line string suitable for passing to a child process and | ||
| 1603 | /// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice. | ||
| 1604 | /// | ||
| 1605 | /// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts. | ||
| 1606 | /// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | ||
| 1607 | /// | ||
| 1608 | /// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead. | ||
| 1609 | fn argvToCommandLineWindows( | ||
| 1610 | allocator: Allocator, | ||
| 1611 | argv: []const []const u8, | ||
| 1612 | ) ArgvToCommandLineError![:0]u16 { | ||
| 1613 | var buf = std.array_list.Managed(u8).init(allocator); | ||
| 1614 | defer buf.deinit(); | ||
| 1615 | |||
| 1616 | if (argv.len != 0) { | ||
| 1617 | const arg0 = argv[0]; | ||
| 1618 | |||
| 1619 | // The first argument must be quoted if it contains spaces or ASCII control characters | ||
| 1620 | // (excluding DEL). It also follows special quoting rules where backslashes have no special | ||
| 1621 | // interpretation, which makes it impossible to pass certain first arguments containing | ||
| 1622 | // double quotes to a child process without characters from the first argument leaking into | ||
| 1623 | // subsequent ones (which could have security implications). | ||
| 1624 | // | ||
| 1625 | // Empty arguments technically don't need quotes, but we quote them anyway for maximum | ||
| 1626 | // compatibility with different implementations of the 'CommandLineToArgvW' algorithm. | ||
| 1627 | // | ||
| 1628 | // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject | ||
| 1629 | // all first arguments containing double quotes, even ones that we could theoretically | ||
| 1630 | // serialize in unquoted form. | ||
| 1631 | var needs_quotes = arg0.len == 0; | ||
| 1632 | for (arg0) |c| { | ||
| 1633 | if (c <= ' ') { | ||
| 1634 | needs_quotes = true; | ||
| 1635 | } else if (c == '"') { | ||
| 1636 | return error.InvalidArg0; | ||
| 1637 | } | ||
| 1638 | } | ||
| 1639 | if (needs_quotes) { | ||
| 1640 | try buf.append('"'); | ||
| 1641 | try buf.appendSlice(arg0); | ||
| 1642 | try buf.append('"'); | ||
| 1643 | } else { | ||
| 1644 | try buf.appendSlice(arg0); | ||
| 1645 | } | ||
| 1646 | |||
| 1647 | for (argv[1..]) |arg| { | ||
| 1648 | try buf.append(' '); | ||
| 1649 | |||
| 1650 | // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes, | ||
| 1651 | // or if they are empty. For simplicity and for maximum compatibility with different | ||
| 1652 | // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII | ||
| 1653 | // control characters (again, excluding DEL). | ||
| 1654 | needs_quotes = for (arg) |c| { | ||
| 1655 | if (c <= ' ' or c == '"') { | ||
| 1656 | break true; | ||
| 1657 | } | ||
| 1658 | } else arg.len == 0; | ||
| 1659 | if (!needs_quotes) { | ||
| 1660 | try buf.appendSlice(arg); | ||
| 1661 | continue; | ||
| 1662 | } | ||
| 1663 | |||
| 1664 | try buf.append('"'); | ||
| 1665 | var backslash_count: usize = 0; | ||
| 1666 | for (arg) |byte| { | ||
| 1667 | switch (byte) { | ||
| 1668 | '\\' => { | ||
| 1669 | backslash_count += 1; | ||
| 1670 | }, | ||
| 1671 | '"' => { | ||
| 1672 | try buf.appendNTimes('\\', backslash_count * 2 + 1); | ||
| 1673 | try buf.append('"'); | ||
| 1674 | backslash_count = 0; | ||
| 1675 | }, | ||
| 1676 | else => { | ||
| 1677 | try buf.appendNTimes('\\', backslash_count); | ||
| 1678 | try buf.append(byte); | ||
| 1679 | backslash_count = 0; | ||
| 1680 | }, | ||
| 1681 | } | ||
| 1682 | } | ||
| 1683 | try buf.appendNTimes('\\', backslash_count * 2); | ||
| 1684 | try buf.append('"'); | ||
| 1685 | } | ||
| 1686 | } | ||
| 1687 | |||
| 1688 | return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | ||
| 1689 | } | ||
| 1690 | |||
| 1691 | test argvToCommandLineWindows { | ||
| 1692 | const t = testArgvToCommandLineWindows; | ||
| 1693 | |||
| 1694 | try t(&.{ | ||
| 1695 | \\C:\Program Files\zig\zig.exe | ||
| 1696 | , | ||
| 1697 | \\run | ||
| 1698 | , | ||
| 1699 | \\.\src\main.zig | ||
| 1700 | , | ||
| 1701 | \\-target | ||
| 1702 | , | ||
| 1703 | \\x86_64-windows-gnu | ||
| 1704 | , | ||
| 1705 | \\-O | ||
| 1706 | , | ||
| 1707 | \\ReleaseSafe | ||
| 1708 | , | ||
| 1709 | \\-- | ||
| 1710 | , | ||
| 1711 | \\--emoji=🗿 | ||
| 1712 | , | ||
| 1713 | \\--eval=new Regex("Dwayne \"The Rock\" Johnson") | ||
| 1714 | , | ||
| 1715 | }, | ||
| 1716 | \\"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\")" | ||
| 1717 | ); | ||
| 1718 | |||
| 1719 | try t(&.{}, ""); | ||
| 1720 | try t(&.{""}, "\"\""); | ||
| 1721 | try t(&.{" "}, "\" \""); | ||
| 1722 | try t(&.{"\t"}, "\"\t\""); | ||
| 1723 | try t(&.{"\x07"}, "\"\x07\""); | ||
| 1724 | try t(&.{"🦎"}, "🦎"); | ||
| 1725 | |||
| 1726 | try t( | ||
| 1727 | &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" }, | ||
| 1728 | "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee", | ||
| 1729 | ); | ||
| 1730 | |||
| 1731 | try t( | ||
| 1732 | &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" }, | ||
| 1733 | "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"", | ||
| 1734 | ); | ||
| 1735 | |||
| 1736 | try std.testing.expectError( | ||
| 1737 | error.InvalidArg0, | ||
| 1738 | argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}), | ||
| 1739 | ); | ||
| 1740 | try std.testing.expectError( | ||
| 1741 | error.InvalidArg0, | ||
| 1742 | argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}), | ||
| 1743 | ); | ||
| 1744 | try std.testing.expectError( | ||
| 1745 | error.InvalidArg0, | ||
| 1746 | argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}), | ||
| 1747 | ); | ||
| 1748 | } | ||
| 1749 | |||
| 1750 | fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void { | ||
| 1751 | const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); | ||
| 1752 | defer std.testing.allocator.free(cmd_line_w); | ||
| 1753 | |||
| 1754 | const cmd_line = try unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w); | ||
| 1755 | defer std.testing.allocator.free(cmd_line); | ||
| 1756 | |||
| 1757 | try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); | ||
| 1758 | } | ||
| 1759 | |||
| 1760 | const ArgvToScriptCommandLineError = error{ | ||
| 1761 | OutOfMemory, | ||
| 1762 | InvalidWtf8, | ||
| 1763 | /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | ||
| 1764 | /// within arguments when executing a `.bat`/`.cmd` script. | ||
| 1765 | /// - NUL/LF signifiies end of arguments, so anything afterwards | ||
| 1766 | /// would be lost after execution. | ||
| 1767 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | ||
| 1768 | /// would be lost after execution. | ||
| 1769 | InvalidBatchScriptArg, | ||
| 1770 | }; | ||
| 1771 | |||
| 1772 | /// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific | ||
| 1773 | /// escaping rules. The caller owns the returned slice. | ||
| 1774 | /// | ||
| 1775 | /// Escapes `argv` using the suggested mitigation against arbitrary command execution from: | ||
| 1776 | /// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | ||
| 1777 | /// | ||
| 1778 | /// The return of this function will look like | ||
| 1779 | /// `cmd.exe /d /e:ON /v:OFF /c "<escaped command line>"` | ||
| 1780 | /// and should be used as the `lpCommandLine` of `CreateProcessW`, while the | ||
| 1781 | /// return of `windowsCmdExePath` should be used as `lpApplicationName`. | ||
| 1782 | /// | ||
| 1783 | /// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise. | ||
| 1784 | /// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem. | ||
| 1785 | fn argvToScriptCommandLineWindows( | ||
| 1786 | allocator: Allocator, | ||
| 1787 | /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD. | ||
| 1788 | /// The script must have been verified to exist at this path before calling this function. | ||
| 1789 | script_path: []const u16, | ||
| 1790 | /// Arguments, not including the script name itself. Expected to be encoded as WTF-8. | ||
| 1791 | script_args: []const []const u8, | ||
| 1792 | ) ArgvToScriptCommandLineError![:0]u16 { | ||
| 1793 | var buf = try std.array_list.Managed(u8).initCapacity(allocator, 64); | ||
| 1794 | defer buf.deinit(); | ||
| 1795 | |||
| 1796 | // `/d` disables execution of AutoRun commands. | ||
| 1797 | // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation: | ||
| 1798 | // > If delayed expansion is enabled via the registry value DelayedExpansion, | ||
| 1799 | // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option. | ||
| 1800 | // > Escaping for % requires the command extension to be enabled. | ||
| 1801 | // > If it’s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option. | ||
| 1802 | // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | ||
| 1803 | buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \""); | ||
| 1804 | |||
| 1805 | // Always quote the path to the script arg | ||
| 1806 | buf.appendAssumeCapacity('"'); | ||
| 1807 | // We always want the path to the batch script to include a path separator in order to | ||
| 1808 | // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary | ||
| 1809 | // command execution mitigation, we just know exactly what script we want to execute | ||
| 1810 | // at this point, and potentially making cmd.exe re-find it is unnecessary. | ||
| 1811 | // | ||
| 1812 | // If the script path does not have a path separator, then we know its relative to CWD and | ||
| 1813 | // we can just put `.\` in the front. | ||
| 1814 | if (mem.findAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) { | ||
| 1815 | try buf.appendSlice(".\\"); | ||
| 1816 | } | ||
| 1817 | // Note that we don't do any escaping/mitigations for this argument, since the relevant | ||
| 1818 | // characters (", %, etc) are illegal in file paths and this function should only be called | ||
| 1819 | // with script paths that have been verified to exist. | ||
| 1820 | try unicode.wtf16LeToWtf8ArrayList(&buf, script_path); | ||
| 1821 | buf.appendAssumeCapacity('"'); | ||
| 1822 | |||
| 1823 | for (script_args) |arg| { | ||
| 1824 | // Literal carriage returns get stripped when run through cmd.exe | ||
| 1825 | // and NUL/newlines act as 'end of command.' Because of this, it's basically | ||
| 1826 | // always a mistake to include these characters in argv, so it's | ||
| 1827 | // an error condition in order to ensure that the return of this | ||
| 1828 | // function can always roundtrip through cmd.exe. | ||
| 1829 | if (std.mem.findAny(u8, arg, "\x00\r\n") != null) { | ||
| 1830 | return error.InvalidBatchScriptArg; | ||
| 1831 | } | ||
| 1832 | |||
| 1833 | // Separate args with a space. | ||
| 1834 | try buf.append(' '); | ||
| 1835 | |||
| 1836 | // Need to quote if the argument is empty (otherwise the arg would just be lost) | ||
| 1837 | // or if the last character is a `\`, since then something like "%~2" in a .bat | ||
| 1838 | // script would cause the closing " to be escaped which we don't want. | ||
| 1839 | var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\'; | ||
| 1840 | if (!needs_quotes) { | ||
| 1841 | for (arg) |c| { | ||
| 1842 | switch (c) { | ||
| 1843 | // Known good characters that don't need to be quoted | ||
| 1844 | 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {}, | ||
| 1845 | // When in doubt, quote | ||
| 1846 | else => { | ||
| 1847 | needs_quotes = true; | ||
| 1848 | break; | ||
| 1849 | }, | ||
| 1850 | } | ||
| 1851 | } | ||
| 1852 | } | ||
| 1853 | if (needs_quotes) { | ||
| 1854 | try buf.append('"'); | ||
| 1855 | } | ||
| 1856 | var backslashes: usize = 0; | ||
| 1857 | for (arg) |c| { | ||
| 1858 | switch (c) { | ||
| 1859 | '\\' => { | ||
| 1860 | backslashes += 1; | ||
| 1861 | }, | ||
| 1862 | '"' => { | ||
| 1863 | try buf.appendNTimes('\\', backslashes); | ||
| 1864 | try buf.append('"'); | ||
| 1865 | backslashes = 0; | ||
| 1866 | }, | ||
| 1867 | // Replace `%` with `%%cd:~,%`. | ||
| 1868 | // | ||
| 1869 | // cmd.exe allows extracting a substring from an environment | ||
| 1870 | // variable with the syntax: `%foo:~<start_index>,<end_index>%`. | ||
| 1871 | // Therefore, `%cd:~,%` will always expand to an empty string | ||
| 1872 | // since both the start and end index are blank, and it is assumed | ||
| 1873 | // that `%cd%` is always available since it is a built-in variable | ||
| 1874 | // that corresponds to the current directory. | ||
| 1875 | // | ||
| 1876 | // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%` | ||
| 1877 | // will stop `%foo%` from being expanded and *after* expansion | ||
| 1878 | // we'll still be left with `%foo%` (the literal string). | ||
| 1879 | '%' => { | ||
| 1880 | // the trailing `%` is appended outside the switch | ||
| 1881 | try buf.appendSlice("%%cd:~,"); | ||
| 1882 | backslashes = 0; | ||
| 1883 | }, | ||
| 1884 | else => { | ||
| 1885 | backslashes = 0; | ||
| 1886 | }, | ||
| 1887 | } | ||
| 1888 | try buf.append(c); | ||
| 1889 | } | ||
| 1890 | if (needs_quotes) { | ||
| 1891 | try buf.appendNTimes('\\', backslashes); | ||
| 1892 | try buf.append('"'); | ||
| 1893 | } | ||
| 1894 | } | ||
| 1895 | |||
| 1896 | try buf.append('"'); | ||
| 1897 | |||
| 1898 | return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | ||
| 1899 | } |
lib/std/zig/LibCInstallation.zig+22-18| ... | @@ -268,15 +268,17 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar | ... | @@ -268,15 +268,17 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar |
| 268 | dev_null, | 268 | dev_null, |
| 269 | }); | 269 | }); |
| 270 | 270 | ||
| 271 | const run_res = std.process.Child.run(gpa, io, .{ | 271 | const run_res = std.process.run(gpa, io, .{ |
| 272 | .argv = argv.items, | ||
| 273 | .max_output_bytes = 1024 * 1024, | 272 | .max_output_bytes = 1024 * 1024, |
| 274 | .env_map = &env_map, | 273 | .spawn_options = .{ |
| 275 | // Some C compilers, such as Clang, are known to rely on argv[0] to find the path | 274 | .argv = argv.items, |
| 276 | // to their own executable, without even bothering to resolve PATH. This results in the message: | 275 | .env_map = &env_map, |
| 277 | // error: unable to execute command: Executable "" doesn't exist! | 276 | // Some C compilers, such as Clang, are known to rely on argv[0] to find the path |
| 278 | // So we use the expandArg0 variant of ChildProcess to give them a helping hand. | 277 | // to their own executable, without even bothering to resolve PATH. This results in the message: |
| 279 | .expand_arg0 = .expand, | 278 | // error: unable to execute command: Executable "" doesn't exist! |
| 279 | // So we use the expandArg0 variant of ChildProcess to give them a helping hand. | ||
| 280 | .expand_arg0 = .expand, | ||
| 281 | }, | ||
| 280 | }) catch |err| switch (err) { | 282 | }) catch |err| switch (err) { |
| 281 | error.OutOfMemory => return error.OutOfMemory, | 283 | error.OutOfMemory => return error.OutOfMemory, |
| 282 | else => { | 284 | else => { |
| ... | @@ -289,7 +291,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar | ... | @@ -289,7 +291,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, gpa: Allocator, io: Io, ar |
| 289 | gpa.free(run_res.stderr); | 291 | gpa.free(run_res.stderr); |
| 290 | } | 292 | } |
| 291 | switch (run_res.term) { | 293 | switch (run_res.term) { |
| 292 | .Exited => |code| if (code != 0) { | 294 | .exited => |code| if (code != 0) { |
| 293 | printVerboseInvocation(argv.items, null, args.verbose, run_res.stderr); | 295 | printVerboseInvocation(argv.items, null, args.verbose, run_res.stderr); |
| 294 | return error.CCompilerExitCode; | 296 | return error.CCompilerExitCode; |
| 295 | }, | 297 | }, |
| ... | @@ -585,15 +587,17 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8 | ... | @@ -585,15 +587,17 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8 |
| 585 | try appendCcExe(&argv, skip_cc_env_var); | 587 | try appendCcExe(&argv, skip_cc_env_var); |
| 586 | try argv.append(arg1); | 588 | try argv.append(arg1); |
| 587 | 589 | ||
| 588 | const run_res = std.process.Child.run(gpa, io, .{ | 590 | const run_res = std.process.run(gpa, io, .{ |
| 589 | .argv = argv.items, | ||
| 590 | .max_output_bytes = 1024 * 1024, | 591 | .max_output_bytes = 1024 * 1024, |
| 591 | .env_map = &env_map, | 592 | .spawn_options = .{ |
| 592 | // Some C compilers, such as Clang, are known to rely on argv[0] to find the path | 593 | .argv = argv.items, |
| 593 | // to their own executable, without even bothering to resolve PATH. This results in the message: | 594 | .env_map = &env_map, |
| 594 | // error: unable to execute command: Executable "" doesn't exist! | 595 | // Some C compilers, such as Clang, are known to rely on argv[0] to find the path |
| 595 | // So we use the expandArg0 variant of ChildProcess to give them a helping hand. | 596 | // to their own executable, without even bothering to resolve PATH. This results in the message: |
| 596 | .expand_arg0 = .expand, | 597 | // error: unable to execute command: Executable "" doesn't exist! |
| 598 | // So we use the expandArg0 variant of ChildProcess to give them a helping hand. | ||
| 599 | .expand_arg0 = .expand, | ||
| 600 | }, | ||
| 597 | }) catch |err| switch (err) { | 601 | }) catch |err| switch (err) { |
| 598 | error.OutOfMemory => return error.OutOfMemory, | 602 | error.OutOfMemory => return error.OutOfMemory, |
| 599 | else => return error.UnableToSpawnCCompiler, | 603 | else => return error.UnableToSpawnCCompiler, |
| ... | @@ -603,7 +607,7 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8 | ... | @@ -603,7 +607,7 @@ fn ccPrintFileName(gpa: Allocator, io: Io, args: CCPrintFileNameOptions) ![:0]u8 |
| 603 | gpa.free(run_res.stderr); | 607 | gpa.free(run_res.stderr); |
| 604 | } | 608 | } |
| 605 | switch (run_res.term) { | 609 | switch (run_res.term) { |
| 606 | .Exited => |code| if (code != 0) { | 610 | .exited => |code| if (code != 0) { |
| 607 | printVerboseInvocation(argv.items, args.search_basename, args.verbose, run_res.stderr); | 611 | printVerboseInvocation(argv.items, args.search_basename, args.verbose, run_res.stderr); |
| 608 | return error.CCompilerExitCode; | 612 | return error.CCompilerExitCode; |
| 609 | }, | 613 | }, |
lib/std/zig/system/darwin.zig+6-9| ... | @@ -17,15 +17,15 @@ pub const macos = @import("darwin/macos.zig"); | ... | @@ -17,15 +17,15 @@ pub const macos = @import("darwin/macos.zig"); |
| 17 | /// | 17 | /// |
| 18 | /// If error.OutOfMemory occurs in Allocator, this function returns null. | 18 | /// If error.OutOfMemory occurs in Allocator, this function returns null. |
| 19 | pub fn isSdkInstalled(gpa: Allocator, io: Io) bool { | 19 | pub fn isSdkInstalled(gpa: Allocator, io: Io) bool { |
| 20 | const result = std.process.Child.run(gpa, io, .{ | 20 | const result = std.process.run(gpa, io, .{ .spawn_options = .{ |
| 21 | .argv = &.{ "xcode-select", "--print-path" }, | 21 | .argv = &.{ "xcode-select", "--print-path" }, |
| 22 | }) catch return false; | 22 | } }) catch return false; |
| 23 | defer { | 23 | defer { |
| 24 | gpa.free(result.stderr); | 24 | gpa.free(result.stderr); |
| 25 | gpa.free(result.stdout); | 25 | gpa.free(result.stdout); |
| 26 | } | 26 | } |
| 27 | return switch (result.term) { | 27 | return switch (result.term) { |
| 28 | .Exited => |code| if (code == 0) result.stdout.len > 0 else false, | 28 | .exited => |code| if (code == 0) result.stdout.len > 0 else false, |
| 29 | else => false, | 29 | else => false, |
| 30 | }; | 30 | }; |
| 31 | } | 31 | } |
| ... | @@ -35,7 +35,7 @@ pub fn isSdkInstalled(gpa: Allocator, io: Io) bool { | ... | @@ -35,7 +35,7 @@ pub fn isSdkInstalled(gpa: Allocator, io: Io) bool { |
| 35 | /// Caller owns the memory. | 35 | /// Caller owns the memory. |
| 36 | /// stderr from xcrun is ignored. | 36 | /// stderr from xcrun is ignored. |
| 37 | /// If error.OutOfMemory occurs in Allocator, this function returns null. | 37 | /// If error.OutOfMemory occurs in Allocator, this function returns null. |
| 38 | pub fn getSdk(gpa: Allocator, io: Io, environ: std.process.Child.Environ, target: *const Target) ?[]const u8 { | 38 | pub fn getSdk(gpa: Allocator, io: Io, target: *const Target) ?[]const u8 { |
| 39 | const is_simulator_abi = target.abi == .simulator; | 39 | const is_simulator_abi = target.abi == .simulator; |
| 40 | const sdk = switch (target.os.tag) { | 40 | const sdk = switch (target.os.tag) { |
| 41 | .driverkit => "driverkit", | 41 | .driverkit => "driverkit", |
| ... | @@ -47,16 +47,13 @@ pub fn getSdk(gpa: Allocator, io: Io, environ: std.process.Child.Environ, target | ... | @@ -47,16 +47,13 @@ pub fn getSdk(gpa: Allocator, io: Io, environ: std.process.Child.Environ, target |
| 47 | else => return null, | 47 | else => return null, |
| 48 | }; | 48 | }; |
| 49 | const argv = &[_][]const u8{ "xcrun", "--sdk", sdk, "--show-sdk-path" }; | 49 | const argv = &[_][]const u8{ "xcrun", "--sdk", sdk, "--show-sdk-path" }; |
| 50 | const result = std.process.Child.run(gpa, io, .{ | 50 | const result = std.process.run(gpa, io, .{ .spawn_options = .{ .argv = argv } }) catch return null; |
| 51 | .argv = argv, | ||
| 52 | .environ = environ, | ||
| 53 | }) catch return null; | ||
| 54 | defer { | 51 | defer { |
| 55 | gpa.free(result.stderr); | 52 | gpa.free(result.stderr); |
| 56 | gpa.free(result.stdout); | 53 | gpa.free(result.stdout); |
| 57 | } | 54 | } |
| 58 | switch (result.term) { | 55 | switch (result.term) { |
| 59 | .Exited => |code| if (code != 0) return null, | 56 | .exited => |code| if (code != 0) return null, |
| 60 | else => return null, | 57 | else => return null, |
| 61 | } | 58 | } |
| 62 | return gpa.dupe(u8, mem.trimEnd(u8, result.stdout, "\r\n")) catch null; | 59 | return gpa.dupe(u8, mem.trimEnd(u8, result.stdout, "\r\n")) catch null; |
src/Compilation.zig+16-12| ... | @@ -6339,15 +6339,15 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr | ... | @@ -6339,15 +6339,15 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr |
| 6339 | if (std.process.can_spawn) { | 6339 | if (std.process.can_spawn) { |
| 6340 | var child = std.process.Child.init(argv.items, arena); | 6340 | var child = std.process.Child.init(argv.items, arena); |
| 6341 | if (comp.clang_passthrough_mode) { | 6341 | if (comp.clang_passthrough_mode) { |
| 6342 | child.stdin_behavior = .Inherit; | 6342 | child.stdin_behavior = .inherit; |
| 6343 | child.stdout_behavior = .Inherit; | 6343 | child.stdout_behavior = .inherit; |
| 6344 | child.stderr_behavior = .Inherit; | 6344 | child.stderr_behavior = .inherit; |
| 6345 | 6345 | ||
| 6346 | const term = child.spawnAndWait(io) catch |err| { | 6346 | const term = child.spawnAndWait(io) catch |err| { |
| 6347 | return comp.failCObj(c_object, "failed to spawn zig clang (passthrough mode) {s}: {s}", .{ argv.items[0], @errorName(err) }); | 6347 | return comp.failCObj(c_object, "failed to spawn zig clang (passthrough mode) {s}: {s}", .{ argv.items[0], @errorName(err) }); |
| 6348 | }; | 6348 | }; |
| 6349 | switch (term) { | 6349 | switch (term) { |
| 6350 | .Exited => |code| { | 6350 | .exited => |code| { |
| 6351 | if (code != 0) { | 6351 | if (code != 0) { |
| 6352 | std.process.exit(code); | 6352 | std.process.exit(code); |
| 6353 | } | 6353 | } |
| ... | @@ -6357,9 +6357,9 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr | ... | @@ -6357,9 +6357,9 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr |
| 6357 | else => std.process.abort(), | 6357 | else => std.process.abort(), |
| 6358 | } | 6358 | } |
| 6359 | } else { | 6359 | } else { |
| 6360 | child.stdin_behavior = .Ignore; | 6360 | child.stdin_behavior = .ignore; |
| 6361 | child.stdout_behavior = .Ignore; | 6361 | child.stdout_behavior = .ignore; |
| 6362 | child.stderr_behavior = .Pipe; | 6362 | child.stderr_behavior = .pipe; |
| 6363 | 6363 | ||
| 6364 | try child.spawn(io); | 6364 | try child.spawn(io); |
| 6365 | 6365 | ||
| ... | @@ -6371,7 +6371,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr | ... | @@ -6371,7 +6371,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr |
| 6371 | }; | 6371 | }; |
| 6372 | 6372 | ||
| 6373 | switch (term) { | 6373 | switch (term) { |
| 6374 | .Exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| { | 6374 | .exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| { |
| 6375 | const bundle = CObject.Diag.Bundle.parse(gpa, io, diag_file_path) catch |err| { | 6375 | const bundle = CObject.Diag.Bundle.parse(gpa, io, diag_file_path) catch |err| { |
| 6376 | log.err("{}: failed to parse clang diagnostics: {s}", .{ err, stderr }); | 6376 | log.err("{}: failed to parse clang diagnostics: {s}", .{ err, stderr }); |
| 6377 | return comp.failCObj(c_object, "clang exited with code {d}", .{code}); | 6377 | return comp.failCObj(c_object, "clang exited with code {d}", .{code}); |
| ... | @@ -6742,9 +6742,9 @@ fn spawnZigRc( | ... | @@ -6742,9 +6742,9 @@ fn spawnZigRc( |
| 6742 | defer node_name.deinit(arena); | 6742 | defer node_name.deinit(arena); |
| 6743 | 6743 | ||
| 6744 | var child = std.process.Child.init(argv, arena); | 6744 | var child = std.process.Child.init(argv, arena); |
| 6745 | child.stdin_behavior = .Ignore; | 6745 | child.stdin_behavior = .ignore; |
| 6746 | child.stdout_behavior = .Pipe; | 6746 | child.stdout_behavior = .pipe; |
| 6747 | child.stderr_behavior = .Pipe; | 6747 | child.stderr_behavior = .pipe; |
| 6748 | child.progress_node = child_progress_node; | 6748 | child.progress_node = child_progress_node; |
| 6749 | 6749 | ||
| 6750 | child.spawn(io) catch |err| { | 6750 | child.spawn(io) catch |err| { |
| ... | @@ -6785,12 +6785,16 @@ fn spawnZigRc( | ... | @@ -6785,12 +6785,16 @@ fn spawnZigRc( |
| 6785 | }; | 6785 | }; |
| 6786 | 6786 | ||
| 6787 | switch (term) { | 6787 | switch (term) { |
| 6788 | .Exited => |code| { | 6788 | .exited => |code| { |
| 6789 | if (code != 0) { | 6789 | if (code != 0) { |
| 6790 | log.err("zig rc failed with stderr:\n{s}", .{stderr.buffered()}); | 6790 | log.err("zig rc failed with stderr:\n{s}", .{stderr.buffered()}); |
| 6791 | return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code}); | 6791 | return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code}); |
| 6792 | } | 6792 | } |
| 6793 | }, | 6793 | }, |
| 6794 | .signal => |sig| { | ||
| 6795 | log.err("zig rc signaled {t} with stderr:\n{s}", .{ sig, stderr.buffered() }); | ||
| 6796 | return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); | ||
| 6797 | }, | ||
| 6794 | else => { | 6798 | else => { |
| 6795 | log.err("zig rc terminated with stderr:\n{s}", .{stderr.buffered()}); | 6799 | log.err("zig rc terminated with stderr:\n{s}", .{stderr.buffered()}); |
| 6796 | return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); | 6800 | return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{}); |
src/link/Lld.zig+13-13| ... | @@ -1606,15 +1606,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi | ... | @@ -1606,15 +1606,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi |
| 1606 | 1606 | ||
| 1607 | var child = std.process.Child.init(argv, arena); | 1607 | var child = std.process.Child.init(argv, arena); |
| 1608 | const term = (if (comp.clang_passthrough_mode) term: { | 1608 | const term = (if (comp.clang_passthrough_mode) term: { |
| 1609 | child.stdin_behavior = .Inherit; | 1609 | child.stdin_behavior = .inherit; |
| 1610 | child.stdout_behavior = .Inherit; | 1610 | child.stdout_behavior = .inherit; |
| 1611 | child.stderr_behavior = .Inherit; | 1611 | child.stderr_behavior = .inherit; |
| 1612 | 1612 | ||
| 1613 | break :term child.spawnAndWait(io); | 1613 | break :term child.spawnAndWait(io); |
| 1614 | } else term: { | 1614 | } else term: { |
| 1615 | child.stdin_behavior = .Ignore; | 1615 | child.stdin_behavior = .ignore; |
| 1616 | child.stdout_behavior = .Ignore; | 1616 | child.stdout_behavior = .ignore; |
| 1617 | child.stderr_behavior = .Pipe; | 1617 | child.stderr_behavior = .pipe; |
| 1618 | 1618 | ||
| 1619 | child.spawn(io) catch |err| break :term err; | 1619 | child.spawn(io) catch |err| break :term err; |
| 1620 | var stderr_reader = child.stderr.?.readerStreaming(io, &.{}); | 1620 | var stderr_reader = child.stderr.?.readerStreaming(io, &.{}); |
| ... | @@ -1656,15 +1656,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi | ... | @@ -1656,15 +1656,15 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi |
| 1656 | .{try comp.dirs.local_cache.join(arena, &.{rsp_path})}, | 1656 | .{try comp.dirs.local_cache.join(arena, &.{rsp_path})}, |
| 1657 | ) }, arena); | 1657 | ) }, arena); |
| 1658 | if (comp.clang_passthrough_mode) { | 1658 | if (comp.clang_passthrough_mode) { |
| 1659 | rsp_child.stdin_behavior = .Inherit; | 1659 | rsp_child.stdin_behavior = .inherit; |
| 1660 | rsp_child.stdout_behavior = .Inherit; | 1660 | rsp_child.stdout_behavior = .inherit; |
| 1661 | rsp_child.stderr_behavior = .Inherit; | 1661 | rsp_child.stderr_behavior = .inherit; |
| 1662 | 1662 | ||
| 1663 | break :term rsp_child.spawnAndWait(io) catch |err| break :err err; | 1663 | break :term rsp_child.spawnAndWait(io) catch |err| break :err err; |
| 1664 | } else { | 1664 | } else { |
| 1665 | rsp_child.stdin_behavior = .Ignore; | 1665 | rsp_child.stdin_behavior = .ignore; |
| 1666 | rsp_child.stdout_behavior = .Ignore; | 1666 | rsp_child.stdout_behavior = .ignore; |
| 1667 | rsp_child.stderr_behavior = .Pipe; | 1667 | rsp_child.stderr_behavior = .pipe; |
| 1668 | 1668 | ||
| 1669 | rsp_child.spawn(io) catch |err| break :err err; | 1669 | rsp_child.spawn(io) catch |err| break :err err; |
| 1670 | var stderr_reader = rsp_child.stderr.?.readerStreaming(io, &.{}); | 1670 | var stderr_reader = rsp_child.stderr.?.readerStreaming(io, &.{}); |
| ... | @@ -1680,7 +1680,7 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi | ... | @@ -1680,7 +1680,7 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi |
| 1680 | 1680 | ||
| 1681 | const diags = &comp.link_diags; | 1681 | const diags = &comp.link_diags; |
| 1682 | switch (term) { | 1682 | switch (term) { |
| 1683 | .Exited => |code| if (code != 0) { | 1683 | .exited => |code| if (code != 0) { |
| 1684 | if (comp.clang_passthrough_mode) std.process.exit(code); | 1684 | if (comp.clang_passthrough_mode) std.process.exit(code); |
| 1685 | diags.lockAndParseLldStderr(argv[1], stderr); | 1685 | diags.lockAndParseLldStderr(argv[1], stderr); |
| 1686 | return error.LinkFailure; | 1686 | return error.LinkFailure; |
src/main.zig+12-12| ... | @@ -4442,9 +4442,9 @@ fn runOrTest( | ... | @@ -4442,9 +4442,9 @@ fn runOrTest( |
| 4442 | } else if (process.can_spawn) { | 4442 | } else if (process.can_spawn) { |
| 4443 | var child = std.process.Child.init(argv.items, gpa); | 4443 | var child = std.process.Child.init(argv.items, gpa); |
| 4444 | child.env_map = &env_map; | 4444 | child.env_map = &env_map; |
| 4445 | child.stdin_behavior = .Inherit; | 4445 | child.stdin_behavior = .inherit; |
| 4446 | child.stdout_behavior = .Inherit; | 4446 | child.stdout_behavior = .inherit; |
| 4447 | child.stderr_behavior = .Inherit; | 4447 | child.stderr_behavior = .inherit; |
| 4448 | 4448 | ||
| 4449 | // Here we release all the locks associated with the Compilation so | 4449 | // Here we release all the locks associated with the Compilation so |
| 4450 | // that whatever this child process wants to do won't deadlock. | 4450 | // that whatever this child process wants to do won't deadlock. |
| ... | @@ -4587,9 +4587,9 @@ fn runOrTestHotSwap( | ... | @@ -4587,9 +4587,9 @@ fn runOrTestHotSwap( |
| 4587 | else => { | 4587 | else => { |
| 4588 | var child = std.process.Child.init(argv.items, gpa); | 4588 | var child = std.process.Child.init(argv.items, gpa); |
| 4589 | 4589 | ||
| 4590 | child.stdin_behavior = .Inherit; | 4590 | child.stdin_behavior = .inherit; |
| 4591 | child.stdout_behavior = .Inherit; | 4591 | child.stdout_behavior = .inherit; |
| 4592 | child.stderr_behavior = .Inherit; | 4592 | child.stderr_behavior = .inherit; |
| 4593 | 4593 | ||
| 4594 | try child.spawn(io); | 4594 | try child.spawn(io); |
| 4595 | 4595 | ||
| ... | @@ -5417,9 +5417,9 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) | ... | @@ -5417,9 +5417,9 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) |
| 5417 | 5417 | ||
| 5418 | if (process.can_spawn) { | 5418 | if (process.can_spawn) { |
| 5419 | var child = std.process.Child.init(child_argv.items, gpa); | 5419 | var child = std.process.Child.init(child_argv.items, gpa); |
| 5420 | child.stdin_behavior = .Inherit; | 5420 | child.stdin_behavior = .inherit; |
| 5421 | child.stdout_behavior = .Inherit; | 5421 | child.stdout_behavior = .inherit; |
| 5422 | child.stderr_behavior = .Inherit; | 5422 | child.stderr_behavior = .inherit; |
| 5423 | 5423 | ||
| 5424 | const term = t: { | 5424 | const term = t: { |
| 5425 | _ = try io.lockStderr(&.{}, .no_color); | 5425 | _ = try io.lockStderr(&.{}, .no_color); |
| ... | @@ -5686,9 +5686,9 @@ fn jitCmd( | ... | @@ -5686,9 +5686,9 @@ fn jitCmd( |
| 5686 | } | 5686 | } |
| 5687 | 5687 | ||
| 5688 | var child = std.process.Child.init(child_argv.items, gpa); | 5688 | var child = std.process.Child.init(child_argv.items, gpa); |
| 5689 | child.stdin_behavior = .Inherit; | 5689 | child.stdin_behavior = .inherit; |
| 5690 | child.stdout_behavior = if (options.capture == null) .Inherit else .Pipe; | 5690 | child.stdout_behavior = if (options.capture == null) .inherit else .pipe; |
| 5691 | child.stderr_behavior = .Inherit; | 5691 | child.stderr_behavior = .inherit; |
| 5692 | 5692 | ||
| 5693 | const term = t: { | 5693 | const term = t: { |
| 5694 | _ = try io.lockStderr(&.{}, .no_color); | 5694 | _ = try io.lockStderr(&.{}, .no_color); |
test/link/macho.zig+1-1| ... | @@ -871,7 +871,7 @@ fn testLinkDirectlyCppTbd(b: *Build, opts: Options) *Step { | ... | @@ -871,7 +871,7 @@ fn testLinkDirectlyCppTbd(b: *Build, opts: Options) *Step { |
| 871 | const io = b.graph.io; | 871 | const io = b.graph.io; |
| 872 | const test_step = addTestStep(b, "link-directly-cpp-tbd", opts); | 872 | const test_step = addTestStep(b, "link-directly-cpp-tbd", opts); |
| 873 | 873 | ||
| 874 | const sdk = std.zig.system.darwin.getSdk(b.allocator, io, .{ .map = &b.graph.env_map }, &opts.target.result) orelse | 874 | const sdk = std.zig.system.darwin.getSdk(b.allocator, io, &opts.target.result) orelse |
| 875 | @panic("macOS SDK is required to run the test"); | 875 | @panic("macOS SDK is required to run the test"); |
| 876 | 876 | ||
| 877 | const exe = addExecutable(b, opts, .{ | 877 | const exe = addExecutable(b, opts, .{ |
test/src/Debugger.zig+1-1| ... | @@ -2306,7 +2306,7 @@ fn addTest( | ... | @@ -2306,7 +2306,7 @@ fn addTest( |
| 2306 | run.addArgs(db_argv2); | 2306 | run.addArgs(db_argv2); |
| 2307 | run.addArtifactArg(exe); | 2307 | run.addArtifactArg(exe); |
| 2308 | for (expected_output) |expected| run.addCheck(.{ .expect_stdout_match = db.b.fmt("{s}\n", .{expected}) }); | 2308 | for (expected_output) |expected| run.addCheck(.{ .expect_stdout_match = db.b.fmt("{s}\n", .{expected}) }); |
| 2309 | run.addCheck(.{ .expect_term = .{ .Exited = success } }); | 2309 | run.addCheck(.{ .expect_term = .{ .exited = success } }); |
| 2310 | run.setStdIn(.{ .bytes = "" }); | 2310 | run.setStdIn(.{ .bytes = "" }); |
| 2311 | db.root_step.dependOn(&run.step); | 2311 | db.root_step.dependOn(&run.step); |
| 2312 | } | 2312 | } |
test/src/StackTrace.zig+3-3| ... | @@ -193,9 +193,9 @@ fn addCaseInstance( | ... | @@ -193,9 +193,9 @@ fn addCaseInstance( |
| 193 | run.removeEnvironmentVariable("CLICOLOR_FORCE"); | 193 | run.removeEnvironmentVariable("CLICOLOR_FORCE"); |
| 194 | run.setEnvironmentVariable("NO_COLOR", "1"); | 194 | run.setEnvironmentVariable("NO_COLOR", "1"); |
| 195 | run.addCheck(.{ .expect_term = term: { | 195 | run.addCheck(.{ .expect_term = term: { |
| 196 | if (!expect_panic) break :term .{ .Exited = 0 }; | 196 | if (!expect_panic) break :term .{ .exited = 0 }; |
| 197 | if (target.result.os.tag == .windows) break :term .{ .Exited = 3 }; | 197 | if (target.result.os.tag == .windows) break :term .{ .exited = 3 }; |
| 198 | break :term .{ .Signal = 6 }; | 198 | break :term .{ .signal = @enumFromInt(6) }; |
| 199 | } }); | 199 | } }); |
| 200 | run.expectStdOutEqual(""); | 200 | run.expectStdOutEqual(""); |
| 201 | 201 |
test/standalone/child_process/main.zig+3-3| ... | @@ -26,9 +26,9 @@ pub fn main() !void { | ... | @@ -26,9 +26,9 @@ pub fn main() !void { |
| 26 | const io = threaded.io(); | 26 | const io = threaded.io(); |
| 27 | 27 | ||
| 28 | var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa); | 28 | var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa); |
| 29 | child.stdin_behavior = .Pipe; | 29 | child.stdin_behavior = .pipe; |
| 30 | child.stdout_behavior = .Pipe; | 30 | child.stdout_behavior = .pipe; |
| 31 | child.stderr_behavior = .Inherit; | 31 | child.stderr_behavior = .inherit; |
| 32 | try child.spawn(io); | 32 | try child.spawn(io); |
| 33 | const child_stdin = child.stdin.?; | 33 | const child_stdin = child.stdin.?; |
| 34 | try child_stdin.writeStreamingAll(io, "hello from stdin"); // verified in child | 34 | try child_stdin.writeStreamingAll(io, "hello from stdin"); // verified in child |
test/standalone/ios/build.zig+1-1| ... | @@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void { | ... | @@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void { |
| 25 | 25 | ||
| 26 | const io = b.graph.io; | 26 | const io = b.graph.io; |
| 27 | 27 | ||
| 28 | if (std.zig.system.darwin.getSdk(b.allocator, io, .{ .map = &b.graph.env_map }, &target.result)) |sdk| { | 28 | if (std.zig.system.darwin.getSdk(b.allocator, io, &target.result)) |sdk| { |
| 29 | b.sysroot = sdk; | 29 | b.sysroot = sdk; |
| 30 | exe.root_module.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/include" }) }); | 30 | exe.root_module.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/include" }) }); |
| 31 | exe.root_module.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/System/Library/Frameworks" }) }); | 31 | exe.root_module.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/System/Library/Frameworks" }) }); |
test/standalone/simple/hello_world/hello.zig+2-12| ... | @@ -1,15 +1,5 @@ | ... | @@ -1,15 +1,5 @@ |
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | 2 | ||
| 3 | // See https://github.com/ziglang/zig/issues/24510 | 3 | pub fn main(init: std.process.Init) !void { |
| 4 | // for the plan to simplify this code. | 4 | try std.Io.File.stdout().writeStreamingAll(init.io, "Hello, World!\n"); |
| 5 | pub fn main() !void { | ||
| 6 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; | ||
| 7 | defer _ = debug_allocator.deinit(); | ||
| 8 | const gpa = debug_allocator.allocator(); | ||
| 9 | |||
| 10 | var threaded: std.Io.Threaded = .init(gpa, .{}); | ||
| 11 | defer threaded.deinit(); | ||
| 12 | const io = threaded.io(); | ||
| 13 | |||
| 14 | try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); | ||
| 15 | } | 5 | } |
test/tests.zig+1-1| ... | @@ -2718,7 +2718,7 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons | ... | @@ -2718,7 +2718,7 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons |
| 2718 | if (b.enable_wasmtime) run.addArg("-fwasmtime"); | 2718 | if (b.enable_wasmtime) run.addArg("-fwasmtime"); |
| 2719 | if (b.enable_darling) run.addArg("-fdarling"); | 2719 | if (b.enable_darling) run.addArg("-fdarling"); |
| 2720 | 2720 | ||
| 2721 | run.addCheck(.{ .expect_term = .{ .Exited = 0 } }); | 2721 | run.addCheck(.{ .expect_term = .{ .exited = 0 } }); |
| 2722 | 2722 | ||
| 2723 | test_step.dependOn(&run.step); | 2723 | test_step.dependOn(&run.step); |
| 2724 | } | 2724 | } |