| author | |
| committer | |
| log | 591bbafee37126dab2e035be717bb928c5953155 |
| tree | 5e71e6f4aba07b1852c03c69f94f09d4a084f717 |
| parent | b9b7f1852308cf41bef511b9ccb3e24e353bf109 |
| parent | aa463adc91d4fb356595b8f7d0af3801c0623e1e |
| signature |
std: restructure child process namespace24 files changed, 2063 insertions(+), 2048 deletions(-)
CMakeLists.txt+104-104| ... | ... | @@ -193,100 +193,6 @@ set(ZIG_CPP_SOURCES |
| 193 | 193 | # Needed because we use cmake, not the zig build system, to build zig2.o. |
| 194 | 194 | set(ZIG_STAGE2_SOURCES |
| 195 | 195 | "${ZIG_CONFIG_ZIG_OUT}" |
| 196 | lib/std/array_hash_map.zig | |
| 197 | lib/std/array_list.zig | |
| 198 | lib/std/ascii.zig | |
| 199 | lib/std/atomic.zig | |
| 200 | lib/std/base64.zig | |
| 201 | lib/std/BitStack.zig | |
| 202 | lib/std/buf_map.zig | |
| 203 | lib/std/Build.zig | |
| 204 | lib/std/Build/Cache.zig | |
| 205 | lib/std/Build/Cache/DepTokenizer.zig | |
| 206 | lib/std/builtin.zig | |
| 207 | lib/std/c.zig | |
| 208 | lib/std/c/linux.zig | |
| 209 | lib/std/child_process.zig | |
| 210 | lib/std/coff.zig | |
| 211 | lib/std/static_string_map.zig | |
| 212 | lib/std/crypto.zig | |
| 213 | lib/std/crypto/blake3.zig | |
| 214 | lib/std/crypto/siphash.zig | |
| 215 | lib/std/debug.zig | |
| 216 | lib/std/dwarf.zig | |
| 217 | lib/std/dwarf/AT.zig | |
| 218 | lib/std/dwarf/ATE.zig | |
| 219 | lib/std/dwarf/FORM.zig | |
| 220 | lib/std/dwarf/LANG.zig | |
| 221 | lib/std/dwarf/OP.zig | |
| 222 | lib/std/dwarf/TAG.zig | |
| 223 | lib/std/elf.zig | |
| 224 | lib/std/fifo.zig | |
| 225 | lib/std/fmt.zig | |
| 226 | lib/std/fmt/format_float.zig | |
| 227 | lib/std/fmt/parse_float.zig | |
| 228 | lib/std/fs.zig | |
| 229 | lib/std/fs/AtomicFile.zig | |
| 230 | lib/std/fs/Dir.zig | |
| 231 | lib/std/fs/File.zig | |
| 232 | lib/std/fs/get_app_data_dir.zig | |
| 233 | lib/std/fs/path.zig | |
| 234 | lib/std/hash.zig | |
| 235 | lib/std/hash/auto_hash.zig | |
| 236 | lib/std/hash/wyhash.zig | |
| 237 | lib/std/hash_map.zig | |
| 238 | lib/std/heap.zig | |
| 239 | lib/std/heap/arena_allocator.zig | |
| 240 | lib/std/io.zig | |
| 241 | lib/std/io/buffered_atomic_file.zig | |
| 242 | lib/std/io/buffered_writer.zig | |
| 243 | lib/std/io/change_detection_stream.zig | |
| 244 | lib/std/io/counting_reader.zig | |
| 245 | lib/std/io/counting_writer.zig | |
| 246 | lib/std/io/find_byte_writer.zig | |
| 247 | lib/std/io/fixed_buffer_stream.zig | |
| 248 | lib/std/io/limited_reader.zig | |
| 249 | lib/std/io/Reader.zig | |
| 250 | lib/std/io/seekable_stream.zig | |
| 251 | lib/std/io/Writer.zig | |
| 252 | lib/std/json.zig | |
| 253 | lib/std/json/stringify.zig | |
| 254 | lib/std/leb128.zig | |
| 255 | lib/std/linked_list.zig | |
| 256 | lib/std/log.zig | |
| 257 | lib/std/macho.zig | |
| 258 | lib/std/math.zig | |
| 259 | lib/std/math/big.zig | |
| 260 | lib/std/math/big/int.zig | |
| 261 | lib/std/math/float.zig | |
| 262 | lib/std/math/frexp.zig | |
| 263 | lib/std/math/isinf.zig | |
| 264 | lib/std/math/isnan.zig | |
| 265 | lib/std/math/log.zig | |
| 266 | lib/std/math/log10.zig | |
| 267 | lib/std/math/log2.zig | |
| 268 | lib/std/math/signbit.zig | |
| 269 | lib/std/math/sqrt.zig | |
| 270 | lib/std/mem.zig | |
| 271 | lib/std/mem/Allocator.zig | |
| 272 | lib/std/meta.zig | |
| 273 | lib/std/meta/trailer_flags.zig | |
| 274 | lib/std/multi_array_list.zig | |
| 275 | lib/std/os.zig | |
| 276 | lib/std/os/linux.zig | |
| 277 | lib/std/os/linux/x86_64.zig | |
| 278 | lib/std/os/linux.zig | |
| 279 | lib/std/os/linux/IoUring.zig | |
| 280 | lib/std/os/linux/io_uring_sqe.zig | |
| 281 | lib/std/os/linux/x86_64.zig | |
| 282 | lib/std/os/windows.zig | |
| 283 | lib/std/os/windows/ntstatus.zig | |
| 284 | lib/std/os/windows/win32error.zig | |
| 285 | lib/std/Progress.zig | |
| 286 | lib/std/pdb.zig | |
| 287 | lib/std/process.zig | |
| 288 | lib/std/Random.zig | |
| 289 | lib/std/sort.zig | |
| 290 | 196 | lib/compiler_rt.zig |
| 291 | 197 | lib/compiler_rt/absv.zig |
| 292 | 198 | lib/compiler_rt/absvdi2.zig |
| ... | ... | @@ -363,7 +269,7 @@ set(ZIG_STAGE2_SOURCES |
| 363 | 269 | lib/compiler_rt/fixxfdi.zig |
| 364 | 270 | lib/compiler_rt/fixxfsi.zig |
| 365 | 271 | lib/compiler_rt/fixxfti.zig |
| 366 | lib/compiler_rt/int_from_float.zig | |
| 272 | lib/compiler_rt/float_from_int.zig | |
| 367 | 273 | lib/compiler_rt/floatdidf.zig |
| 368 | 274 | lib/compiler_rt/floatdihf.zig |
| 369 | 275 | lib/compiler_rt/floatdisf.zig |
| ... | ... | @@ -404,7 +310,7 @@ set(ZIG_STAGE2_SOURCES |
| 404 | 310 | lib/compiler_rt/getf2.zig |
| 405 | 311 | lib/compiler_rt/gexf2.zig |
| 406 | 312 | lib/compiler_rt/int.zig |
| 407 | lib/compiler_rt/float_from_int.zig | |
| 313 | lib/compiler_rt/int_from_float.zig | |
| 408 | 314 | lib/compiler_rt/log.zig |
| 409 | 315 | lib/compiler_rt/log10.zig |
| 410 | 316 | lib/compiler_rt/log2.zig |
| ... | ... | @@ -417,7 +323,11 @@ set(ZIG_STAGE2_SOURCES |
| 417 | 323 | lib/compiler_rt/multf3.zig |
| 418 | 324 | lib/compiler_rt/mulxf3.zig |
| 419 | 325 | lib/compiler_rt/negXi2.zig |
| 326 | lib/compiler_rt/negdf2.zig | |
| 327 | lib/compiler_rt/negsf2.zig | |
| 328 | lib/compiler_rt/negtf2.zig | |
| 420 | 329 | lib/compiler_rt/negv.zig |
| 330 | lib/compiler_rt/negxf2.zig | |
| 421 | 331 | lib/compiler_rt/os_version_check.zig |
| 422 | 332 | lib/compiler_rt/parity.zig |
| 423 | 333 | lib/compiler_rt/popcount.zig |
| ... | ... | @@ -430,15 +340,11 @@ set(ZIG_STAGE2_SOURCES |
| 430 | 340 | lib/compiler_rt/sincos.zig |
| 431 | 341 | lib/compiler_rt/sqrt.zig |
| 432 | 342 | lib/compiler_rt/stack_probe.zig |
| 343 | lib/compiler_rt/subdf3.zig | |
| 433 | 344 | lib/compiler_rt/subo.zig |
| 434 | 345 | lib/compiler_rt/subsf3.zig |
| 435 | lib/compiler_rt/subdf3.zig | |
| 436 | 346 | lib/compiler_rt/subtf3.zig |
| 437 | 347 | lib/compiler_rt/subxf3.zig |
| 438 | lib/compiler_rt/negsf2.zig | |
| 439 | lib/compiler_rt/negdf2.zig | |
| 440 | lib/compiler_rt/negtf2.zig | |
| 441 | lib/compiler_rt/negxf2.zig | |
| 442 | 348 | lib/compiler_rt/tan.zig |
| 443 | 349 | lib/compiler_rt/trig.zig |
| 444 | 350 | lib/compiler_rt/trunc.zig |
| ... | ... | @@ -461,8 +367,12 @@ set(ZIG_STAGE2_SOURCES |
| 461 | 367 | lib/compiler_rt/unorddf2.zig |
| 462 | 368 | lib/compiler_rt/unordsf2.zig |
| 463 | 369 | lib/compiler_rt/unordtf2.zig |
| 464 | lib/std/start.zig | |
| 465 | lib/std/std.zig | |
| 370 | lib/std/BitStack.zig | |
| 371 | lib/std/Build.zig | |
| 372 | lib/std/Build/Cache.zig | |
| 373 | lib/std/Build/Cache/DepTokenizer.zig | |
| 374 | lib/std/Progress.zig | |
| 375 | lib/std/Random.zig | |
| 466 | 376 | lib/std/Target.zig |
| 467 | 377 | lib/std/Target/Query.zig |
| 468 | 378 | lib/std/Target/aarch64.zig |
| ... | ... | @@ -476,8 +386,8 @@ set(ZIG_STAGE2_SOURCES |
| 476 | 386 | lib/std/Target/nvptx.zig |
| 477 | 387 | lib/std/Target/powerpc.zig |
| 478 | 388 | lib/std/Target/riscv.zig |
| 479 | lib/std/Target/sparc.zig | |
| 480 | 389 | lib/std/Target/s390x.zig |
| 390 | lib/std/Target/sparc.zig | |
| 481 | 391 | lib/std/Target/wasm.zig |
| 482 | 392 | lib/std/Target/x86.zig |
| 483 | 393 | lib/std/Thread.zig |
| ... | ... | @@ -486,6 +396,96 @@ set(ZIG_STAGE2_SOURCES |
| 486 | 396 | lib/std/Thread/Pool.zig |
| 487 | 397 | lib/std/Thread/ResetEvent.zig |
| 488 | 398 | lib/std/Thread/WaitGroup.zig |
| 399 | lib/std/array_hash_map.zig | |
| 400 | lib/std/array_list.zig | |
| 401 | lib/std/ascii.zig | |
| 402 | lib/std/atomic.zig | |
| 403 | lib/std/base64.zig | |
| 404 | lib/std/buf_map.zig | |
| 405 | lib/std/builtin.zig | |
| 406 | lib/std/c.zig | |
| 407 | lib/std/c/linux.zig | |
| 408 | lib/std/coff.zig | |
| 409 | lib/std/crypto.zig | |
| 410 | lib/std/crypto/blake3.zig | |
| 411 | lib/std/crypto/siphash.zig | |
| 412 | lib/std/debug.zig | |
| 413 | lib/std/dwarf.zig | |
| 414 | lib/std/dwarf/AT.zig | |
| 415 | lib/std/dwarf/ATE.zig | |
| 416 | lib/std/dwarf/FORM.zig | |
| 417 | lib/std/dwarf/LANG.zig | |
| 418 | lib/std/dwarf/OP.zig | |
| 419 | lib/std/dwarf/TAG.zig | |
| 420 | lib/std/elf.zig | |
| 421 | lib/std/fifo.zig | |
| 422 | lib/std/fmt.zig | |
| 423 | lib/std/fmt/format_float.zig | |
| 424 | lib/std/fmt/parse_float.zig | |
| 425 | lib/std/fs.zig | |
| 426 | lib/std/fs/AtomicFile.zig | |
| 427 | lib/std/fs/Dir.zig | |
| 428 | lib/std/fs/File.zig | |
| 429 | lib/std/fs/get_app_data_dir.zig | |
| 430 | lib/std/fs/path.zig | |
| 431 | lib/std/hash.zig | |
| 432 | lib/std/hash/auto_hash.zig | |
| 433 | lib/std/hash/wyhash.zig | |
| 434 | lib/std/hash_map.zig | |
| 435 | lib/std/heap.zig | |
| 436 | lib/std/heap/arena_allocator.zig | |
| 437 | lib/std/io.zig | |
| 438 | lib/std/io/Reader.zig | |
| 439 | lib/std/io/Writer.zig | |
| 440 | lib/std/io/buffered_atomic_file.zig | |
| 441 | lib/std/io/buffered_writer.zig | |
| 442 | lib/std/io/change_detection_stream.zig | |
| 443 | lib/std/io/counting_reader.zig | |
| 444 | lib/std/io/counting_writer.zig | |
| 445 | lib/std/io/find_byte_writer.zig | |
| 446 | lib/std/io/fixed_buffer_stream.zig | |
| 447 | lib/std/io/limited_reader.zig | |
| 448 | lib/std/io/seekable_stream.zig | |
| 449 | lib/std/json.zig | |
| 450 | lib/std/json/stringify.zig | |
| 451 | lib/std/leb128.zig | |
| 452 | lib/std/linked_list.zig | |
| 453 | lib/std/log.zig | |
| 454 | lib/std/macho.zig | |
| 455 | lib/std/math.zig | |
| 456 | lib/std/math/big.zig | |
| 457 | lib/std/math/big/int.zig | |
| 458 | lib/std/math/float.zig | |
| 459 | lib/std/math/frexp.zig | |
| 460 | lib/std/math/isinf.zig | |
| 461 | lib/std/math/isnan.zig | |
| 462 | lib/std/math/log.zig | |
| 463 | lib/std/math/log10.zig | |
| 464 | lib/std/math/log2.zig | |
| 465 | lib/std/math/signbit.zig | |
| 466 | lib/std/math/sqrt.zig | |
| 467 | lib/std/mem.zig | |
| 468 | lib/std/mem/Allocator.zig | |
| 469 | lib/std/meta.zig | |
| 470 | lib/std/meta/trailer_flags.zig | |
| 471 | lib/std/multi_array_list.zig | |
| 472 | lib/std/os.zig | |
| 473 | lib/std/os/linux.zig | |
| 474 | lib/std/os/linux.zig | |
| 475 | lib/std/os/linux/IoUring.zig | |
| 476 | lib/std/os/linux/io_uring_sqe.zig | |
| 477 | lib/std/os/linux/x86_64.zig | |
| 478 | lib/std/os/linux/x86_64.zig | |
| 479 | lib/std/os/windows.zig | |
| 480 | lib/std/os/windows/ntstatus.zig | |
| 481 | lib/std/os/windows/win32error.zig | |
| 482 | lib/std/pdb.zig | |
| 483 | lib/std/process.zig | |
| 484 | lib/std/process/Child.zig | |
| 485 | lib/std/sort.zig | |
| 486 | lib/std/start.zig | |
| 487 | lib/std/static_string_map.zig | |
| 488 | lib/std/std.zig | |
| 489 | 489 | lib/std/time.zig |
| 490 | 490 | lib/std/treap.zig |
| 491 | 491 | lib/std/unicode.zig |
lib/compiler/aro/aro/Driver.zig+1-1| ... | ... | @@ -803,7 +803,7 @@ pub fn invokeLinker(d: *Driver, tc: *Toolchain, comptime fast_exit: bool) !void |
| 803 | 803 | return d.fatal("unable to dump linker args: {s}", .{errorDescription(er)}); |
| 804 | 804 | }; |
| 805 | 805 | } |
| 806 | var child = std.ChildProcess.init(argv.items, d.comp.gpa); | |
| 806 | var child = std.process.Child.init(argv.items, d.comp.gpa); | |
| 807 | 807 | // TODO handle better |
| 808 | 808 | child.stdin_behavior = .Inherit; |
| 809 | 809 | child.stdout_behavior = .Inherit; |
lib/compiler/std-docs.zig+2-2| ... | ... | @@ -302,7 +302,7 @@ fn buildWasmBinary( |
| 302 | 302 | "--listen=-", |
| 303 | 303 | }); |
| 304 | 304 | |
| 305 | var child = std.ChildProcess.init(argv.items, gpa); | |
| 305 | var child = std.process.Child.init(argv.items, gpa); | |
| 306 | 306 | child.stdin_behavior = .Pipe; |
| 307 | 307 | child.stdout_behavior = .Pipe; |
| 308 | 308 | child.stderr_behavior = .Pipe; |
| ... | ... | @@ -437,7 +437,7 @@ fn openBrowserTabThread(gpa: Allocator, url: []const u8) !void { |
| 437 | 437 | .macos => "open", |
| 438 | 438 | else => "xdg-open", |
| 439 | 439 | }; |
| 440 | var child = std.ChildProcess.init(&.{ main_exe, url }, gpa); | |
| 440 | var child = std.process.Child.init(&.{ main_exe, url }, gpa); | |
| 441 | 441 | child.stdin_behavior = .Ignore; |
| 442 | 442 | child.stdout_behavior = .Ignore; |
| 443 | 443 | child.stderr_behavior = .Ignore; |
lib/std/Build.zig+3-3| ... | ... | @@ -178,7 +178,7 @@ pub const RunError = error{ |
| 178 | 178 | ExitCodeFailure, |
| 179 | 179 | ProcessTerminated, |
| 180 | 180 | ExecNotSupported, |
| 181 | } || std.ChildProcess.SpawnError; | |
| 181 | } || std.process.Child.SpawnError; | |
| 182 | 182 | |
| 183 | 183 | pub const PkgConfigError = error{ |
| 184 | 184 | PkgConfigCrashed, |
| ... | ... | @@ -1719,7 +1719,7 @@ pub fn runAllowFail( |
| 1719 | 1719 | b: *Build, |
| 1720 | 1720 | argv: []const []const u8, |
| 1721 | 1721 | out_code: *u8, |
| 1722 | stderr_behavior: std.ChildProcess.StdIo, | |
| 1722 | stderr_behavior: std.process.Child.StdIo, | |
| 1723 | 1723 | ) RunError![]u8 { |
| 1724 | 1724 | assert(argv.len != 0); |
| 1725 | 1725 | |
| ... | ... | @@ -1727,7 +1727,7 @@ pub fn runAllowFail( |
| 1727 | 1727 | return error.ExecNotSupported; |
| 1728 | 1728 | |
| 1729 | 1729 | const max_output_size = 400 * 1024; |
| 1730 | var child = std.ChildProcess.init(argv, b.allocator); | |
| 1730 | var child = std.process.Child.init(argv, b.allocator); | |
| 1731 | 1731 | child.stdin_behavior = .Ignore; |
| 1732 | 1732 | child.stdout_behavior = .Pipe; |
| 1733 | 1733 | child.stderr_behavior = stderr_behavior; |
lib/std/Build/Step.zig+3-3| ... | ... | @@ -275,7 +275,7 @@ pub fn evalChildProcess(s: *Step, argv: []const []const u8) !void { |
| 275 | 275 | try handleChildProcUnsupported(s, null, argv); |
| 276 | 276 | try handleVerbose(s.owner, null, argv); |
| 277 | 277 | |
| 278 | const result = std.ChildProcess.run(.{ | |
| 278 | const result = std.process.Child.run(.{ | |
| 279 | 279 | .allocator = arena, |
| 280 | 280 | .argv = argv, |
| 281 | 281 | }) catch |err| return s.fail("unable to spawn {s}: {s}", .{ argv[0], @errorName(err) }); |
| ... | ... | @@ -313,7 +313,7 @@ pub fn evalZigProcess( |
| 313 | 313 | try handleChildProcUnsupported(s, null, argv); |
| 314 | 314 | try handleVerbose(s.owner, null, argv); |
| 315 | 315 | |
| 316 | var child = std.ChildProcess.init(argv, arena); | |
| 316 | var child = std.process.Child.init(argv, arena); | |
| 317 | 317 | child.env_map = &b.graph.env_map; |
| 318 | 318 | child.stdin_behavior = .Pipe; |
| 319 | 319 | child.stdout_behavior = .Pipe; |
| ... | ... | @@ -480,7 +480,7 @@ pub inline fn handleChildProcUnsupported( |
| 480 | 480 | |
| 481 | 481 | pub fn handleChildProcessTerm( |
| 482 | 482 | s: *Step, |
| 483 | term: std.ChildProcess.Term, | |
| 483 | term: std.process.Child.Term, | |
| 484 | 484 | opt_cwd: ?[]const u8, |
| 485 | 485 | argv: []const []const u8, |
| 486 | 486 | ) error{ MakeFailed, OutOfMemory }!void { |
lib/std/child_process.zig deleted-1880| ... | ... | @@ -1,1880 +0,0 @@ |
| 1 | const std = @import("std.zig"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const unicode = std.unicode; | |
| 4 | const io = std.io; | |
| 5 | const fs = std.fs; | |
| 6 | const process = std.process; | |
| 7 | const File = std.fs.File; | |
| 8 | const windows = std.os.windows; | |
| 9 | const linux = std.os.linux; | |
| 10 | const posix = std.posix; | |
| 11 | const mem = std.mem; | |
| 12 | const math = std.math; | |
| 13 | const debug = std.debug; | |
| 14 | const EnvMap = process.EnvMap; | |
| 15 | const maxInt = std.math.maxInt; | |
| 16 | const assert = std.debug.assert; | |
| 17 | const native_os = builtin.os.tag; | |
| 18 | ||
| 19 | pub const ChildProcess = struct { | |
| 20 | pub const Id = switch (native_os) { | |
| 21 | .windows => windows.HANDLE, | |
| 22 | .wasi => void, | |
| 23 | else => posix.pid_t, | |
| 24 | }; | |
| 25 | ||
| 26 | /// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`. | |
| 27 | /// On Windows this is the hProcess. | |
| 28 | /// On POSIX this is the pid. | |
| 29 | id: Id, | |
| 30 | thread_handle: if (native_os == .windows) windows.HANDLE else void, | |
| 31 | ||
| 32 | allocator: mem.Allocator, | |
| 33 | ||
| 34 | /// The writing end of the child process's standard input pipe. | |
| 35 | /// Usage requires `stdin_behavior == StdIo.Pipe`. | |
| 36 | /// Available after calling `spawn()`. | |
| 37 | stdin: ?File, | |
| 38 | ||
| 39 | /// The reading end of the child process's standard output pipe. | |
| 40 | /// Usage requires `stdout_behavior == StdIo.Pipe`. | |
| 41 | /// Available after calling `spawn()`. | |
| 42 | stdout: ?File, | |
| 43 | ||
| 44 | /// The reading end of the child process's standard error pipe. | |
| 45 | /// Usage requires `stderr_behavior == StdIo.Pipe`. | |
| 46 | /// Available after calling `spawn()`. | |
| 47 | stderr: ?File, | |
| 48 | ||
| 49 | /// Terminated state of the child process. | |
| 50 | /// Available after calling `wait()`. | |
| 51 | term: ?(SpawnError!Term), | |
| 52 | ||
| 53 | argv: []const []const u8, | |
| 54 | ||
| 55 | /// Leave as null to use the current env map using the supplied allocator. | |
| 56 | env_map: ?*const EnvMap, | |
| 57 | ||
| 58 | stdin_behavior: StdIo, | |
| 59 | stdout_behavior: StdIo, | |
| 60 | stderr_behavior: StdIo, | |
| 61 | ||
| 62 | /// Set to change the user id when spawning the child process. | |
| 63 | uid: if (native_os == .windows or native_os == .wasi) void else ?posix.uid_t, | |
| 64 | ||
| 65 | /// Set to change the group id when spawning the child process. | |
| 66 | gid: if (native_os == .windows or native_os == .wasi) void else ?posix.gid_t, | |
| 67 | ||
| 68 | /// Set to change the current working directory when spawning the child process. | |
| 69 | cwd: ?[]const u8, | |
| 70 | /// Set to change the current working directory when spawning the child process. | |
| 71 | /// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190 | |
| 72 | /// Once that is done, `cwd` will be deprecated in favor of this field. | |
| 73 | cwd_dir: ?fs.Dir = null, | |
| 74 | ||
| 75 | err_pipe: ?if (native_os == .windows) void else [2]posix.fd_t, | |
| 76 | ||
| 77 | expand_arg0: Arg0Expand, | |
| 78 | ||
| 79 | /// Darwin-only. Disable ASLR for the child process. | |
| 80 | disable_aslr: bool = false, | |
| 81 | ||
| 82 | /// Darwin-only. Start child process in suspended state as if SIGSTOP was sent. | |
| 83 | start_suspended: bool = false, | |
| 84 | ||
| 85 | /// Set to true to obtain rusage information for the child process. | |
| 86 | /// Depending on the target platform and implementation status, the | |
| 87 | /// requested statistics may or may not be available. If they are | |
| 88 | /// available, then the `resource_usage_statistics` field will be populated | |
| 89 | /// after calling `wait`. | |
| 90 | /// On Linux and Darwin, this obtains rusage statistics from wait4(). | |
| 91 | request_resource_usage_statistics: bool = false, | |
| 92 | ||
| 93 | /// This is available after calling wait if | |
| 94 | /// `request_resource_usage_statistics` was set to `true` before calling | |
| 95 | /// `spawn`. | |
| 96 | resource_usage_statistics: ResourceUsageStatistics = .{}, | |
| 97 | ||
| 98 | pub const ResourceUsageStatistics = struct { | |
| 99 | rusage: @TypeOf(rusage_init) = rusage_init, | |
| 100 | ||
| 101 | /// Returns the peak resident set size of the child process, in bytes, | |
| 102 | /// if available. | |
| 103 | pub inline fn getMaxRss(rus: ResourceUsageStatistics) ?usize { | |
| 104 | switch (native_os) { | |
| 105 | .linux => { | |
| 106 | if (rus.rusage) |ru| { | |
| 107 | return @as(usize, @intCast(ru.maxrss)) * 1024; | |
| 108 | } else { | |
| 109 | return null; | |
| 110 | } | |
| 111 | }, | |
| 112 | .windows => { | |
| 113 | if (rus.rusage) |ru| { | |
| 114 | return ru.PeakWorkingSetSize; | |
| 115 | } else { | |
| 116 | return null; | |
| 117 | } | |
| 118 | }, | |
| 119 | .macos, .ios => { | |
| 120 | if (rus.rusage) |ru| { | |
| 121 | // Darwin oddly reports in bytes instead of kilobytes. | |
| 122 | return @as(usize, @intCast(ru.maxrss)); | |
| 123 | } else { | |
| 124 | return null; | |
| 125 | } | |
| 126 | }, | |
| 127 | else => return null, | |
| 128 | } | |
| 129 | } | |
| 130 | ||
| 131 | const rusage_init = switch (native_os) { | |
| 132 | .linux, .macos, .ios => @as(?posix.rusage, null), | |
| 133 | .windows => @as(?windows.VM_COUNTERS, null), | |
| 134 | else => {}, | |
| 135 | }; | |
| 136 | }; | |
| 137 | ||
| 138 | pub const Arg0Expand = posix.Arg0Expand; | |
| 139 | ||
| 140 | pub const SpawnError = error{ | |
| 141 | OutOfMemory, | |
| 142 | ||
| 143 | /// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV. | |
| 144 | NoDevice, | |
| 145 | ||
| 146 | /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8. | |
| 147 | /// https://simonsapin.github.io/wtf-8/ | |
| 148 | InvalidWtf8, | |
| 149 | ||
| 150 | /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process. | |
| 151 | CurrentWorkingDirectoryUnlinked, | |
| 152 | ||
| 153 | /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | |
| 154 | /// within arguments when executing a `.bat`/`.cmd` script. | |
| 155 | /// - NUL/LF signifiies end of arguments, so anything afterwards | |
| 156 | /// would be lost after execution. | |
| 157 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | |
| 158 | /// would be lost after execution. | |
| 159 | InvalidBatchScriptArg, | |
| 160 | } || | |
| 161 | posix.ExecveError || | |
| 162 | posix.SetIdError || | |
| 163 | posix.ChangeCurDirError || | |
| 164 | windows.CreateProcessError || | |
| 165 | windows.GetProcessMemoryInfoError || | |
| 166 | windows.WaitForSingleObjectError; | |
| 167 | ||
| 168 | pub const Term = union(enum) { | |
| 169 | Exited: u8, | |
| 170 | Signal: u32, | |
| 171 | Stopped: u32, | |
| 172 | Unknown: u32, | |
| 173 | }; | |
| 174 | ||
| 175 | /// Behavior of the child process's standard input, output, and error | |
| 176 | /// streams. | |
| 177 | pub const StdIo = enum { | |
| 178 | /// Inherit the stream from the parent process. | |
| 179 | Inherit, | |
| 180 | ||
| 181 | /// Pass a null stream to the child process. | |
| 182 | /// This is /dev/null on POSIX and NUL on Windows. | |
| 183 | Ignore, | |
| 184 | ||
| 185 | /// Create a pipe for the stream. | |
| 186 | /// The corresponding field (`stdout`, `stderr`, or `stdin`) | |
| 187 | /// will be assigned a `File` object that can be used | |
| 188 | /// to read from or write to the pipe. | |
| 189 | Pipe, | |
| 190 | ||
| 191 | /// Close the stream after the child process spawns. | |
| 192 | Close, | |
| 193 | }; | |
| 194 | ||
| 195 | /// First argument in argv is the executable. | |
| 196 | pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess { | |
| 197 | return .{ | |
| 198 | .allocator = allocator, | |
| 199 | .argv = argv, | |
| 200 | .id = undefined, | |
| 201 | .thread_handle = undefined, | |
| 202 | .err_pipe = null, | |
| 203 | .term = null, | |
| 204 | .env_map = null, | |
| 205 | .cwd = null, | |
| 206 | .uid = if (native_os == .windows or native_os == .wasi) {} else null, | |
| 207 | .gid = if (native_os == .windows or native_os == .wasi) {} else null, | |
| 208 | .stdin = null, | |
| 209 | .stdout = null, | |
| 210 | .stderr = null, | |
| 211 | .stdin_behavior = StdIo.Inherit, | |
| 212 | .stdout_behavior = StdIo.Inherit, | |
| 213 | .stderr_behavior = StdIo.Inherit, | |
| 214 | .expand_arg0 = .no_expand, | |
| 215 | }; | |
| 216 | } | |
| 217 | ||
| 218 | pub fn setUserName(self: *ChildProcess, name: []const u8) !void { | |
| 219 | const user_info = try std.process.getUserInfo(name); | |
| 220 | self.uid = user_info.uid; | |
| 221 | self.gid = user_info.gid; | |
| 222 | } | |
| 223 | ||
| 224 | /// On success must call `kill` or `wait`. | |
| 225 | /// After spawning the `id` is available. | |
| 226 | pub fn spawn(self: *ChildProcess) SpawnError!void { | |
| 227 | if (!std.process.can_spawn) { | |
| 228 | @compileError("the target operating system cannot spawn processes"); | |
| 229 | } | |
| 230 | ||
| 231 | if (native_os == .windows) { | |
| 232 | return self.spawnWindows(); | |
| 233 | } else { | |
| 234 | return self.spawnPosix(); | |
| 235 | } | |
| 236 | } | |
| 237 | ||
| 238 | pub fn spawnAndWait(self: *ChildProcess) SpawnError!Term { | |
| 239 | try self.spawn(); | |
| 240 | return self.wait(); | |
| 241 | } | |
| 242 | ||
| 243 | /// Forcibly terminates child process and then cleans up all resources. | |
| 244 | pub fn kill(self: *ChildProcess) !Term { | |
| 245 | if (native_os == .windows) { | |
| 246 | return self.killWindows(1); | |
| 247 | } else { | |
| 248 | return self.killPosix(); | |
| 249 | } | |
| 250 | } | |
| 251 | ||
| 252 | pub fn killWindows(self: *ChildProcess, exit_code: windows.UINT) !Term { | |
| 253 | if (self.term) |term| { | |
| 254 | self.cleanupStreams(); | |
| 255 | return term; | |
| 256 | } | |
| 257 | ||
| 258 | windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) { | |
| 259 | error.PermissionDenied => { | |
| 260 | // Usually when TerminateProcess triggers a ACCESS_DENIED error, it | |
| 261 | // indicates that the process has already exited, but there may be | |
| 262 | // some rare edge cases where our process handle no longer has the | |
| 263 | // PROCESS_TERMINATE access right, so let's do another check to make | |
| 264 | // sure the process is really no longer running: | |
| 265 | windows.WaitForSingleObjectEx(self.id, 0, false) catch return err; | |
| 266 | return error.AlreadyTerminated; | |
| 267 | }, | |
| 268 | else => return err, | |
| 269 | }; | |
| 270 | try self.waitUnwrappedWindows(); | |
| 271 | return self.term.?; | |
| 272 | } | |
| 273 | ||
| 274 | pub fn killPosix(self: *ChildProcess) !Term { | |
| 275 | if (self.term) |term| { | |
| 276 | self.cleanupStreams(); | |
| 277 | return term; | |
| 278 | } | |
| 279 | posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { | |
| 280 | error.ProcessNotFound => return error.AlreadyTerminated, | |
| 281 | else => return err, | |
| 282 | }; | |
| 283 | try self.waitUnwrapped(); | |
| 284 | return self.term.?; | |
| 285 | } | |
| 286 | ||
| 287 | /// Blocks until child process terminates and then cleans up all resources. | |
| 288 | pub fn wait(self: *ChildProcess) !Term { | |
| 289 | const term = if (native_os == .windows) | |
| 290 | try self.waitWindows() | |
| 291 | else | |
| 292 | try self.waitPosix(); | |
| 293 | ||
| 294 | self.id = undefined; | |
| 295 | ||
| 296 | return term; | |
| 297 | } | |
| 298 | ||
| 299 | pub const RunResult = struct { | |
| 300 | term: Term, | |
| 301 | stdout: []u8, | |
| 302 | stderr: []u8, | |
| 303 | }; | |
| 304 | ||
| 305 | fn fifoToOwnedArrayList(fifo: *std.io.PollFifo) std.ArrayList(u8) { | |
| 306 | if (fifo.head > 0) { | |
| 307 | @memcpy(fifo.buf[0..fifo.count], fifo.buf[fifo.head..][0..fifo.count]); | |
| 308 | } | |
| 309 | const result = std.ArrayList(u8){ | |
| 310 | .items = fifo.buf[0..fifo.count], | |
| 311 | .capacity = fifo.buf.len, | |
| 312 | .allocator = fifo.allocator, | |
| 313 | }; | |
| 314 | fifo.* = std.io.PollFifo.init(fifo.allocator); | |
| 315 | return result; | |
| 316 | } | |
| 317 | ||
| 318 | /// Collect the output from the process's stdout and stderr. Will return once all output | |
| 319 | /// has been collected. This does not mean that the process has ended. `wait` should still | |
| 320 | /// be called to wait for and clean up the process. | |
| 321 | /// | |
| 322 | /// The process must be started with stdout_behavior and stderr_behavior == .Pipe | |
| 323 | pub fn collectOutput( | |
| 324 | child: ChildProcess, | |
| 325 | stdout: *std.ArrayList(u8), | |
| 326 | stderr: *std.ArrayList(u8), | |
| 327 | max_output_bytes: usize, | |
| 328 | ) !void { | |
| 329 | debug.assert(child.stdout_behavior == .Pipe); | |
| 330 | debug.assert(child.stderr_behavior == .Pipe); | |
| 331 | ||
| 332 | // we could make this work with multiple allocators but YAGNI | |
| 333 | if (stdout.allocator.ptr != stderr.allocator.ptr or | |
| 334 | stdout.allocator.vtable != stderr.allocator.vtable) | |
| 335 | { | |
| 336 | unreachable; // ChildProcess.collectOutput only supports 1 allocator | |
| 337 | } | |
| 338 | ||
| 339 | var poller = std.io.poll(stdout.allocator, enum { stdout, stderr }, .{ | |
| 340 | .stdout = child.stdout.?, | |
| 341 | .stderr = child.stderr.?, | |
| 342 | }); | |
| 343 | defer poller.deinit(); | |
| 344 | ||
| 345 | while (try poller.poll()) { | |
| 346 | if (poller.fifo(.stdout).count > max_output_bytes) | |
| 347 | return error.StdoutStreamTooLong; | |
| 348 | if (poller.fifo(.stderr).count > max_output_bytes) | |
| 349 | return error.StderrStreamTooLong; | |
| 350 | } | |
| 351 | ||
| 352 | stdout.* = fifoToOwnedArrayList(poller.fifo(.stdout)); | |
| 353 | stderr.* = fifoToOwnedArrayList(poller.fifo(.stderr)); | |
| 354 | } | |
| 355 | ||
| 356 | pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{ | |
| 357 | StdoutStreamTooLong, | |
| 358 | StderrStreamTooLong, | |
| 359 | }; | |
| 360 | ||
| 361 | /// Spawns a child process, waits for it, collecting stdout and stderr, and then returns. | |
| 362 | /// If it succeeds, the caller owns result.stdout and result.stderr memory. | |
| 363 | pub fn run(args: struct { | |
| 364 | allocator: mem.Allocator, | |
| 365 | argv: []const []const u8, | |
| 366 | cwd: ?[]const u8 = null, | |
| 367 | cwd_dir: ?fs.Dir = null, | |
| 368 | env_map: ?*const EnvMap = null, | |
| 369 | max_output_bytes: usize = 50 * 1024, | |
| 370 | expand_arg0: Arg0Expand = .no_expand, | |
| 371 | }) RunError!RunResult { | |
| 372 | var child = ChildProcess.init(args.argv, args.allocator); | |
| 373 | child.stdin_behavior = .Ignore; | |
| 374 | child.stdout_behavior = .Pipe; | |
| 375 | child.stderr_behavior = .Pipe; | |
| 376 | child.cwd = args.cwd; | |
| 377 | child.cwd_dir = args.cwd_dir; | |
| 378 | child.env_map = args.env_map; | |
| 379 | child.expand_arg0 = args.expand_arg0; | |
| 380 | ||
| 381 | var stdout = std.ArrayList(u8).init(args.allocator); | |
| 382 | var stderr = std.ArrayList(u8).init(args.allocator); | |
| 383 | errdefer { | |
| 384 | stdout.deinit(); | |
| 385 | stderr.deinit(); | |
| 386 | } | |
| 387 | ||
| 388 | try child.spawn(); | |
| 389 | try child.collectOutput(&stdout, &stderr, args.max_output_bytes); | |
| 390 | ||
| 391 | return RunResult{ | |
| 392 | .term = try child.wait(), | |
| 393 | .stdout = try stdout.toOwnedSlice(), | |
| 394 | .stderr = try stderr.toOwnedSlice(), | |
| 395 | }; | |
| 396 | } | |
| 397 | ||
| 398 | fn waitWindows(self: *ChildProcess) !Term { | |
| 399 | if (self.term) |term| { | |
| 400 | self.cleanupStreams(); | |
| 401 | return term; | |
| 402 | } | |
| 403 | ||
| 404 | try self.waitUnwrappedWindows(); | |
| 405 | return self.term.?; | |
| 406 | } | |
| 407 | ||
| 408 | fn waitPosix(self: *ChildProcess) !Term { | |
| 409 | if (self.term) |term| { | |
| 410 | self.cleanupStreams(); | |
| 411 | return term; | |
| 412 | } | |
| 413 | ||
| 414 | try self.waitUnwrapped(); | |
| 415 | return self.term.?; | |
| 416 | } | |
| 417 | ||
| 418 | fn waitUnwrappedWindows(self: *ChildProcess) !void { | |
| 419 | const result = windows.WaitForSingleObjectEx(self.id, windows.INFINITE, false); | |
| 420 | ||
| 421 | self.term = @as(SpawnError!Term, x: { | |
| 422 | var exit_code: windows.DWORD = undefined; | |
| 423 | if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) { | |
| 424 | break :x Term{ .Unknown = 0 }; | |
| 425 | } else { | |
| 426 | break :x Term{ .Exited = @as(u8, @truncate(exit_code)) }; | |
| 427 | } | |
| 428 | }); | |
| 429 | ||
| 430 | if (self.request_resource_usage_statistics) { | |
| 431 | self.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(self.id); | |
| 432 | } | |
| 433 | ||
| 434 | posix.close(self.id); | |
| 435 | posix.close(self.thread_handle); | |
| 436 | self.cleanupStreams(); | |
| 437 | return result; | |
| 438 | } | |
| 439 | ||
| 440 | fn waitUnwrapped(self: *ChildProcess) !void { | |
| 441 | const res: posix.WaitPidResult = res: { | |
| 442 | if (self.request_resource_usage_statistics) { | |
| 443 | switch (native_os) { | |
| 444 | .linux, .macos, .ios => { | |
| 445 | var ru: posix.rusage = undefined; | |
| 446 | const res = posix.wait4(self.id, 0, &ru); | |
| 447 | self.resource_usage_statistics.rusage = ru; | |
| 448 | break :res res; | |
| 449 | }, | |
| 450 | else => {}, | |
| 451 | } | |
| 452 | } | |
| 453 | ||
| 454 | break :res posix.waitpid(self.id, 0); | |
| 455 | }; | |
| 456 | const status = res.status; | |
| 457 | self.cleanupStreams(); | |
| 458 | self.handleWaitResult(status); | |
| 459 | } | |
| 460 | ||
| 461 | fn handleWaitResult(self: *ChildProcess, status: u32) void { | |
| 462 | self.term = self.cleanupAfterWait(status); | |
| 463 | } | |
| 464 | ||
| 465 | fn cleanupStreams(self: *ChildProcess) void { | |
| 466 | if (self.stdin) |*stdin| { | |
| 467 | stdin.close(); | |
| 468 | self.stdin = null; | |
| 469 | } | |
| 470 | if (self.stdout) |*stdout| { | |
| 471 | stdout.close(); | |
| 472 | self.stdout = null; | |
| 473 | } | |
| 474 | if (self.stderr) |*stderr| { | |
| 475 | stderr.close(); | |
| 476 | self.stderr = null; | |
| 477 | } | |
| 478 | } | |
| 479 | ||
| 480 | fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term { | |
| 481 | if (self.err_pipe) |err_pipe| { | |
| 482 | defer destroyPipe(err_pipe); | |
| 483 | ||
| 484 | if (native_os == .linux) { | |
| 485 | var fd = [1]posix.pollfd{posix.pollfd{ | |
| 486 | .fd = err_pipe[0], | |
| 487 | .events = posix.POLL.IN, | |
| 488 | .revents = undefined, | |
| 489 | }}; | |
| 490 | ||
| 491 | // Check if the eventfd buffer stores a non-zero value by polling | |
| 492 | // it, that's the error code returned by the child process. | |
| 493 | _ = posix.poll(&fd, 0) catch unreachable; | |
| 494 | ||
| 495 | // According to eventfd(2) the descriptor is readable if the counter | |
| 496 | // has a value greater than 0 | |
| 497 | if ((fd[0].revents & posix.POLL.IN) != 0) { | |
| 498 | const err_int = try readIntFd(err_pipe[0]); | |
| 499 | return @as(SpawnError, @errorCast(@errorFromInt(err_int))); | |
| 500 | } | |
| 501 | } else { | |
| 502 | // Write maxInt(ErrInt) to the write end of the err_pipe. This is after | |
| 503 | // waitpid, so this write is guaranteed to be after the child | |
| 504 | // pid potentially wrote an error. This way we can do a blocking | |
| 505 | // read on the error pipe and either get maxInt(ErrInt) (no error) or | |
| 506 | // an error code. | |
| 507 | try writeIntFd(err_pipe[1], maxInt(ErrInt)); | |
| 508 | const err_int = try readIntFd(err_pipe[0]); | |
| 509 | // Here we potentially return the fork child's error from the parent | |
| 510 | // pid. | |
| 511 | if (err_int != maxInt(ErrInt)) { | |
| 512 | return @as(SpawnError, @errorCast(@errorFromInt(err_int))); | |
| 513 | } | |
| 514 | } | |
| 515 | } | |
| 516 | ||
| 517 | return statusToTerm(status); | |
| 518 | } | |
| 519 | ||
| 520 | fn statusToTerm(status: u32) Term { | |
| 521 | return if (posix.W.IFEXITED(status)) | |
| 522 | Term{ .Exited = posix.W.EXITSTATUS(status) } | |
| 523 | else if (posix.W.IFSIGNALED(status)) | |
| 524 | Term{ .Signal = posix.W.TERMSIG(status) } | |
| 525 | else if (posix.W.IFSTOPPED(status)) | |
| 526 | Term{ .Stopped = posix.W.STOPSIG(status) } | |
| 527 | else | |
| 528 | Term{ .Unknown = status }; | |
| 529 | } | |
| 530 | ||
| 531 | fn spawnPosix(self: *ChildProcess) SpawnError!void { | |
| 532 | const pipe_flags: posix.O = .{}; | |
| 533 | const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try posix.pipe2(pipe_flags) else undefined; | |
| 534 | errdefer if (self.stdin_behavior == StdIo.Pipe) { | |
| 535 | destroyPipe(stdin_pipe); | |
| 536 | }; | |
| 537 | ||
| 538 | const stdout_pipe = if (self.stdout_behavior == StdIo.Pipe) try posix.pipe2(pipe_flags) else undefined; | |
| 539 | errdefer if (self.stdout_behavior == StdIo.Pipe) { | |
| 540 | destroyPipe(stdout_pipe); | |
| 541 | }; | |
| 542 | ||
| 543 | const stderr_pipe = if (self.stderr_behavior == StdIo.Pipe) try posix.pipe2(pipe_flags) else undefined; | |
| 544 | errdefer if (self.stderr_behavior == StdIo.Pipe) { | |
| 545 | destroyPipe(stderr_pipe); | |
| 546 | }; | |
| 547 | ||
| 548 | const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); | |
| 549 | const dev_null_fd = if (any_ignore) | |
| 550 | posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) { | |
| 551 | error.PathAlreadyExists => unreachable, | |
| 552 | error.NoSpaceLeft => unreachable, | |
| 553 | error.FileTooBig => unreachable, | |
| 554 | error.DeviceBusy => unreachable, | |
| 555 | error.FileLocksNotSupported => unreachable, | |
| 556 | error.BadPathName => unreachable, // Windows-only | |
| 557 | error.WouldBlock => unreachable, | |
| 558 | error.NetworkNotFound => unreachable, // Windows-only | |
| 559 | else => |e| return e, | |
| 560 | } | |
| 561 | else | |
| 562 | undefined; | |
| 563 | defer { | |
| 564 | if (any_ignore) posix.close(dev_null_fd); | |
| 565 | } | |
| 566 | ||
| 567 | var arena_allocator = std.heap.ArenaAllocator.init(self.allocator); | |
| 568 | defer arena_allocator.deinit(); | |
| 569 | const arena = arena_allocator.allocator(); | |
| 570 | ||
| 571 | // The POSIX standard does not allow malloc() between fork() and execve(), | |
| 572 | // and `self.allocator` may be a libc allocator. | |
| 573 | // I have personally observed the child process deadlocking when it tries | |
| 574 | // to call malloc() due to a heap allocation between fork() and execve(), | |
| 575 | // in musl v1.1.24. | |
| 576 | // Additionally, we want to reduce the number of possible ways things | |
| 577 | // can fail between fork() and execve(). | |
| 578 | // Therefore, we do all the allocation for the execve() before the fork(). | |
| 579 | // This means we must do the null-termination of argv and env vars here. | |
| 580 | const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null); | |
| 581 | for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; | |
| 582 | ||
| 583 | const envp = m: { | |
| 584 | if (self.env_map) |env_map| { | |
| 585 | const envp_buf = try createNullDelimitedEnvMap(arena, env_map); | |
| 586 | break :m envp_buf.ptr; | |
| 587 | } else if (builtin.link_libc) { | |
| 588 | break :m std.c.environ; | |
| 589 | } else if (builtin.output_mode == .Exe) { | |
| 590 | // Then we have Zig start code and this works. | |
| 591 | // TODO type-safety for null-termination of `os.environ`. | |
| 592 | break :m @as([*:null]const ?[*:0]const u8, @ptrCast(std.os.environ.ptr)); | |
| 593 | } else { | |
| 594 | // TODO come up with a solution for this. | |
| 595 | @compileError("missing std lib enhancement: ChildProcess implementation has no way to collect the environment variables to forward to the child process"); | |
| 596 | } | |
| 597 | }; | |
| 598 | ||
| 599 | // This pipe is used to communicate errors between the time of fork | |
| 600 | // and execve from the child process to the parent process. | |
| 601 | const err_pipe = blk: { | |
| 602 | if (native_os == .linux) { | |
| 603 | const fd = try posix.eventfd(0, linux.EFD.CLOEXEC); | |
| 604 | // There's no distinction between the readable and the writeable | |
| 605 | // end with eventfd | |
| 606 | break :blk [2]posix.fd_t{ fd, fd }; | |
| 607 | } else { | |
| 608 | break :blk try posix.pipe2(.{ .CLOEXEC = true }); | |
| 609 | } | |
| 610 | }; | |
| 611 | errdefer destroyPipe(err_pipe); | |
| 612 | ||
| 613 | const pid_result = try posix.fork(); | |
| 614 | if (pid_result == 0) { | |
| 615 | // we are the child | |
| 616 | setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 617 | setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 618 | setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 619 | ||
| 620 | if (self.stdin_behavior == .Pipe) { | |
| 621 | posix.close(stdin_pipe[0]); | |
| 622 | posix.close(stdin_pipe[1]); | |
| 623 | } | |
| 624 | if (self.stdout_behavior == .Pipe) { | |
| 625 | posix.close(stdout_pipe[0]); | |
| 626 | posix.close(stdout_pipe[1]); | |
| 627 | } | |
| 628 | if (self.stderr_behavior == .Pipe) { | |
| 629 | posix.close(stderr_pipe[0]); | |
| 630 | posix.close(stderr_pipe[1]); | |
| 631 | } | |
| 632 | ||
| 633 | if (self.cwd_dir) |cwd| { | |
| 634 | posix.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 635 | } else if (self.cwd) |cwd| { | |
| 636 | posix.chdir(cwd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 637 | } | |
| 638 | ||
| 639 | if (self.gid) |gid| { | |
| 640 | posix.setregid(gid, gid) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 641 | } | |
| 642 | ||
| 643 | if (self.uid) |uid| { | |
| 644 | posix.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 645 | } | |
| 646 | ||
| 647 | const err = switch (self.expand_arg0) { | |
| 648 | .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), | |
| 649 | .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), | |
| 650 | }; | |
| 651 | forkChildErrReport(err_pipe[1], err); | |
| 652 | } | |
| 653 | ||
| 654 | // we are the parent | |
| 655 | const pid: i32 = @intCast(pid_result); | |
| 656 | if (self.stdin_behavior == StdIo.Pipe) { | |
| 657 | self.stdin = File{ .handle = stdin_pipe[1] }; | |
| 658 | } else { | |
| 659 | self.stdin = null; | |
| 660 | } | |
| 661 | if (self.stdout_behavior == StdIo.Pipe) { | |
| 662 | self.stdout = File{ .handle = stdout_pipe[0] }; | |
| 663 | } else { | |
| 664 | self.stdout = null; | |
| 665 | } | |
| 666 | if (self.stderr_behavior == StdIo.Pipe) { | |
| 667 | self.stderr = File{ .handle = stderr_pipe[0] }; | |
| 668 | } else { | |
| 669 | self.stderr = null; | |
| 670 | } | |
| 671 | ||
| 672 | self.id = pid; | |
| 673 | self.err_pipe = err_pipe; | |
| 674 | self.term = null; | |
| 675 | ||
| 676 | if (self.stdin_behavior == StdIo.Pipe) { | |
| 677 | posix.close(stdin_pipe[0]); | |
| 678 | } | |
| 679 | if (self.stdout_behavior == StdIo.Pipe) { | |
| 680 | posix.close(stdout_pipe[1]); | |
| 681 | } | |
| 682 | if (self.stderr_behavior == StdIo.Pipe) { | |
| 683 | posix.close(stderr_pipe[1]); | |
| 684 | } | |
| 685 | } | |
| 686 | ||
| 687 | fn spawnWindows(self: *ChildProcess) SpawnError!void { | |
| 688 | var saAttr = windows.SECURITY_ATTRIBUTES{ | |
| 689 | .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), | |
| 690 | .bInheritHandle = windows.TRUE, | |
| 691 | .lpSecurityDescriptor = null, | |
| 692 | }; | |
| 693 | ||
| 694 | const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); | |
| 695 | ||
| 696 | const nul_handle = if (any_ignore) | |
| 697 | // "\Device\Null" or "\??\NUL" | |
| 698 | windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{ | |
| 699 | .access_mask = windows.GENERIC_READ | windows.GENERIC_WRITE | windows.SYNCHRONIZE, | |
| 700 | .share_access = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, | |
| 701 | .sa = &saAttr, | |
| 702 | .creation = windows.OPEN_EXISTING, | |
| 703 | }) catch |err| switch (err) { | |
| 704 | error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" | |
| 705 | error.PipeBusy => return error.Unexpected, // not possible for "NUL" | |
| 706 | error.FileNotFound => return error.Unexpected, // not possible for "NUL" | |
| 707 | error.AccessDenied => return error.Unexpected, // not possible for "NUL" | |
| 708 | error.NameTooLong => return error.Unexpected, // not possible for "NUL" | |
| 709 | error.WouldBlock => return error.Unexpected, // not possible for "NUL" | |
| 710 | error.NetworkNotFound => return error.Unexpected, // not possible for "NUL" | |
| 711 | error.AntivirusInterference => return error.Unexpected, // not possible for "NUL" | |
| 712 | else => |e| return e, | |
| 713 | } | |
| 714 | else | |
| 715 | undefined; | |
| 716 | defer { | |
| 717 | if (any_ignore) posix.close(nul_handle); | |
| 718 | } | |
| 719 | ||
| 720 | var g_hChildStd_IN_Rd: ?windows.HANDLE = null; | |
| 721 | var g_hChildStd_IN_Wr: ?windows.HANDLE = null; | |
| 722 | switch (self.stdin_behavior) { | |
| 723 | StdIo.Pipe => { | |
| 724 | try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); | |
| 725 | }, | |
| 726 | StdIo.Ignore => { | |
| 727 | g_hChildStd_IN_Rd = nul_handle; | |
| 728 | }, | |
| 729 | StdIo.Inherit => { | |
| 730 | g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; | |
| 731 | }, | |
| 732 | StdIo.Close => { | |
| 733 | g_hChildStd_IN_Rd = null; | |
| 734 | }, | |
| 735 | } | |
| 736 | errdefer if (self.stdin_behavior == StdIo.Pipe) { | |
| 737 | windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); | |
| 738 | }; | |
| 739 | ||
| 740 | var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; | |
| 741 | var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; | |
| 742 | switch (self.stdout_behavior) { | |
| 743 | StdIo.Pipe => { | |
| 744 | try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); | |
| 745 | }, | |
| 746 | StdIo.Ignore => { | |
| 747 | g_hChildStd_OUT_Wr = nul_handle; | |
| 748 | }, | |
| 749 | StdIo.Inherit => { | |
| 750 | g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; | |
| 751 | }, | |
| 752 | StdIo.Close => { | |
| 753 | g_hChildStd_OUT_Wr = null; | |
| 754 | }, | |
| 755 | } | |
| 756 | errdefer if (self.stdout_behavior == StdIo.Pipe) { | |
| 757 | windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); | |
| 758 | }; | |
| 759 | ||
| 760 | var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; | |
| 761 | var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; | |
| 762 | switch (self.stderr_behavior) { | |
| 763 | StdIo.Pipe => { | |
| 764 | try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); | |
| 765 | }, | |
| 766 | StdIo.Ignore => { | |
| 767 | g_hChildStd_ERR_Wr = nul_handle; | |
| 768 | }, | |
| 769 | StdIo.Inherit => { | |
| 770 | g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; | |
| 771 | }, | |
| 772 | StdIo.Close => { | |
| 773 | g_hChildStd_ERR_Wr = null; | |
| 774 | }, | |
| 775 | } | |
| 776 | errdefer if (self.stderr_behavior == StdIo.Pipe) { | |
| 777 | windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); | |
| 778 | }; | |
| 779 | ||
| 780 | var siStartInfo = windows.STARTUPINFOW{ | |
| 781 | .cb = @sizeOf(windows.STARTUPINFOW), | |
| 782 | .hStdError = g_hChildStd_ERR_Wr, | |
| 783 | .hStdOutput = g_hChildStd_OUT_Wr, | |
| 784 | .hStdInput = g_hChildStd_IN_Rd, | |
| 785 | .dwFlags = windows.STARTF_USESTDHANDLES, | |
| 786 | ||
| 787 | .lpReserved = null, | |
| 788 | .lpDesktop = null, | |
| 789 | .lpTitle = null, | |
| 790 | .dwX = 0, | |
| 791 | .dwY = 0, | |
| 792 | .dwXSize = 0, | |
| 793 | .dwYSize = 0, | |
| 794 | .dwXCountChars = 0, | |
| 795 | .dwYCountChars = 0, | |
| 796 | .dwFillAttribute = 0, | |
| 797 | .wShowWindow = 0, | |
| 798 | .cbReserved2 = 0, | |
| 799 | .lpReserved2 = null, | |
| 800 | }; | |
| 801 | var piProcInfo: windows.PROCESS_INFORMATION = undefined; | |
| 802 | ||
| 803 | const cwd_w = if (self.cwd) |cwd| try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd) else null; | |
| 804 | defer if (cwd_w) |cwd| self.allocator.free(cwd); | |
| 805 | const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; | |
| 806 | ||
| 807 | const maybe_envp_buf = if (self.env_map) |env_map| try createWindowsEnvBlock(self.allocator, env_map) else null; | |
| 808 | defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf); | |
| 809 | const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; | |
| 810 | ||
| 811 | const app_name_wtf8 = self.argv[0]; | |
| 812 | const app_name_is_absolute = fs.path.isAbsolute(app_name_wtf8); | |
| 813 | ||
| 814 | // the cwd set in ChildProcess is in effect when choosing the executable path | |
| 815 | // to match posix semantics | |
| 816 | var cwd_path_w_needs_free = false; | |
| 817 | const cwd_path_w = x: { | |
| 818 | // If the app name is absolute, then we need to use its dirname as the cwd | |
| 819 | if (app_name_is_absolute) { | |
| 820 | cwd_path_w_needs_free = true; | |
| 821 | const dir = fs.path.dirname(app_name_wtf8).?; | |
| 822 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, dir); | |
| 823 | } else if (self.cwd) |cwd| { | |
| 824 | cwd_path_w_needs_free = true; | |
| 825 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd); | |
| 826 | } else { | |
| 827 | break :x &[_:0]u16{}; // empty for cwd | |
| 828 | } | |
| 829 | }; | |
| 830 | defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); | |
| 831 | ||
| 832 | // If the app name has more than just a filename, then we need to separate that | |
| 833 | // into the basename and dirname and use the dirname as an addition to the cwd | |
| 834 | // path. This is because NtQueryDirectoryFile cannot accept FileName params with | |
| 835 | // path separators. | |
| 836 | const app_basename_wtf8 = fs.path.basename(app_name_wtf8); | |
| 837 | // If the app name is absolute, then the cwd will already have the app's dirname in it, | |
| 838 | // so only populate app_dirname if app name is a relative path with > 0 path separators. | |
| 839 | const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_wtf8) else null; | |
| 840 | const app_dirname_w: ?[:0]u16 = x: { | |
| 841 | if (maybe_app_dirname_wtf8) |app_dirname_wtf8| { | |
| 842 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_dirname_wtf8); | |
| 843 | } | |
| 844 | break :x null; | |
| 845 | }; | |
| 846 | defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); | |
| 847 | ||
| 848 | const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8); | |
| 849 | defer self.allocator.free(app_name_w); | |
| 850 | ||
| 851 | run: { | |
| 852 | const PATH: [:0]const u16 = std.process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; | |
| 853 | const PATHEXT: [:0]const u16 = std.process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; | |
| 854 | ||
| 855 | // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules | |
| 856 | // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously | |
| 857 | // constructed arguments. | |
| 858 | // | |
| 859 | // We'll need to wait until we're actually trying to run the command to know for sure | |
| 860 | // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually | |
| 861 | // serializing the command line until we determine how it should be serialized. | |
| 862 | var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv); | |
| 863 | defer cmd_line_cache.deinit(); | |
| 864 | ||
| 865 | var app_buf = std.ArrayListUnmanaged(u16){}; | |
| 866 | defer app_buf.deinit(self.allocator); | |
| 867 | ||
| 868 | try app_buf.appendSlice(self.allocator, app_name_w); | |
| 869 | ||
| 870 | var dir_buf = std.ArrayListUnmanaged(u16){}; | |
| 871 | defer dir_buf.deinit(self.allocator); | |
| 872 | ||
| 873 | if (cwd_path_w.len > 0) { | |
| 874 | try dir_buf.appendSlice(self.allocator, cwd_path_w); | |
| 875 | } | |
| 876 | if (app_dirname_w) |app_dir| { | |
| 877 | if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); | |
| 878 | try dir_buf.appendSlice(self.allocator, app_dir); | |
| 879 | } | |
| 880 | if (dir_buf.items.len > 0) { | |
| 881 | // Need to normalize the path, openDirW can't handle things like double backslashes | |
| 882 | const normalized_len = windows.normalizePath(u16, dir_buf.items) catch return error.BadPathName; | |
| 883 | dir_buf.shrinkRetainingCapacity(normalized_len); | |
| 884 | } | |
| 885 | ||
| 886 | windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| { | |
| 887 | const original_err = switch (no_path_err) { | |
| 888 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | |
| 889 | error.InvalidArg0 => return error.FileNotFound, | |
| 890 | error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, | |
| 891 | error.UnrecoverableInvalidExe => return error.InvalidExe, | |
| 892 | else => |e| return e, | |
| 893 | }; | |
| 894 | ||
| 895 | // If the app name had path separators, that disallows PATH searching, | |
| 896 | // and there's no need to search the PATH if the app name is absolute. | |
| 897 | // We still search the path if the cwd is absolute because of the | |
| 898 | // "cwd set in ChildProcess is in effect when choosing the executable path | |
| 899 | // to match posix semantics" behavior--we don't want to skip searching | |
| 900 | // the PATH just because we were trying to set the cwd of the child process. | |
| 901 | if (app_dirname_w != null or app_name_is_absolute) { | |
| 902 | return original_err; | |
| 903 | } | |
| 904 | ||
| 905 | var it = mem.tokenizeScalar(u16, PATH, ';'); | |
| 906 | while (it.next()) |search_path| { | |
| 907 | dir_buf.clearRetainingCapacity(); | |
| 908 | try dir_buf.appendSlice(self.allocator, search_path); | |
| 909 | // Need to normalize the path, some PATH values can contain things like double | |
| 910 | // backslashes which openDirW can't handle | |
| 911 | const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue; | |
| 912 | dir_buf.shrinkRetainingCapacity(normalized_len); | |
| 913 | ||
| 914 | if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) { | |
| 915 | break :run; | |
| 916 | } else |err| switch (err) { | |
| 917 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | |
| 918 | error.InvalidArg0 => return error.FileNotFound, | |
| 919 | error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, | |
| 920 | error.UnrecoverableInvalidExe => return error.InvalidExe, | |
| 921 | else => |e| return e, | |
| 922 | } | |
| 923 | } else { | |
| 924 | return original_err; | |
| 925 | } | |
| 926 | }; | |
| 927 | } | |
| 928 | ||
| 929 | if (g_hChildStd_IN_Wr) |h| { | |
| 930 | self.stdin = File{ .handle = h }; | |
| 931 | } else { | |
| 932 | self.stdin = null; | |
| 933 | } | |
| 934 | if (g_hChildStd_OUT_Rd) |h| { | |
| 935 | self.stdout = File{ .handle = h }; | |
| 936 | } else { | |
| 937 | self.stdout = null; | |
| 938 | } | |
| 939 | if (g_hChildStd_ERR_Rd) |h| { | |
| 940 | self.stderr = File{ .handle = h }; | |
| 941 | } else { | |
| 942 | self.stderr = null; | |
| 943 | } | |
| 944 | ||
| 945 | self.id = piProcInfo.hProcess; | |
| 946 | self.thread_handle = piProcInfo.hThread; | |
| 947 | self.term = null; | |
| 948 | ||
| 949 | if (self.stdin_behavior == StdIo.Pipe) { | |
| 950 | posix.close(g_hChildStd_IN_Rd.?); | |
| 951 | } | |
| 952 | if (self.stderr_behavior == StdIo.Pipe) { | |
| 953 | posix.close(g_hChildStd_ERR_Wr.?); | |
| 954 | } | |
| 955 | if (self.stdout_behavior == StdIo.Pipe) { | |
| 956 | posix.close(g_hChildStd_OUT_Wr.?); | |
| 957 | } | |
| 958 | } | |
| 959 | ||
| 960 | fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { | |
| 961 | switch (stdio) { | |
| 962 | .Pipe => try posix.dup2(pipe_fd, std_fileno), | |
| 963 | .Close => posix.close(std_fileno), | |
| 964 | .Inherit => {}, | |
| 965 | .Ignore => try posix.dup2(dev_null_fd, std_fileno), | |
| 966 | } | |
| 967 | } | |
| 968 | }; | |
| 969 | ||
| 970 | /// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. | |
| 971 | /// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. | |
| 972 | /// Note: `app_buf` should not contain any leading path separators. | |
| 973 | /// Note: If the dir is the cwd, dir_buf should be empty (len = 0). | |
| 974 | fn windowsCreateProcessPathExt( | |
| 975 | allocator: mem.Allocator, | |
| 976 | dir_buf: *std.ArrayListUnmanaged(u16), | |
| 977 | app_buf: *std.ArrayListUnmanaged(u16), | |
| 978 | pathext: [:0]const u16, | |
| 979 | cmd_line_cache: *WindowsCommandLineCache, | |
| 980 | envp_ptr: ?[*]u16, | |
| 981 | cwd_ptr: ?[*:0]u16, | |
| 982 | lpStartupInfo: *windows.STARTUPINFOW, | |
| 983 | lpProcessInformation: *windows.PROCESS_INFORMATION, | |
| 984 | ) !void { | |
| 985 | const app_name_len = app_buf.items.len; | |
| 986 | const dir_path_len = dir_buf.items.len; | |
| 987 | ||
| 988 | if (app_name_len == 0) return error.FileNotFound; | |
| 989 | ||
| 990 | defer app_buf.shrinkRetainingCapacity(app_name_len); | |
| 991 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | |
| 992 | ||
| 993 | // The name of the game here is to avoid CreateProcessW calls at all costs, | |
| 994 | // and only ever try calling it when we have a real candidate for execution. | |
| 995 | // Secondarily, we want to minimize the number of syscalls used when checking | |
| 996 | // for each PATHEXT-appended version of the app name. | |
| 997 | // | |
| 998 | // An overview of the technique used: | |
| 999 | // - Open the search directory for iteration (either cwd or a path from PATH) | |
| 1000 | // - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to | |
| 1001 | // check if anything that could possibly match either the unappended version | |
| 1002 | // of the app name or any of the versions with a PATHEXT value appended exists. | |
| 1003 | // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early | |
| 1004 | // without needing to use PATHEXT at all. | |
| 1005 | // | |
| 1006 | // This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence | |
| 1007 | // for any directory that doesn't contain any possible matches, instead of having | |
| 1008 | // to use a separate look up for each individual filename combination (unappended + | |
| 1009 | // each PATHEXT appended). For directories where the wildcard *does* match something, | |
| 1010 | // we iterate the matches and take note of any that are either the unappended version, | |
| 1011 | // or a version with a supported PATHEXT appended. We then try calling CreateProcessW | |
| 1012 | // with the found versions in the appropriate order. | |
| 1013 | ||
| 1014 | var dir = dir: { | |
| 1015 | // needs to be null-terminated | |
| 1016 | try dir_buf.append(allocator, 0); | |
| 1017 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | |
| 1018 | const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | |
| 1019 | const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); | |
| 1020 | break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch | |
| 1021 | return error.FileNotFound; | |
| 1022 | }; | |
| 1023 | defer dir.close(); | |
| 1024 | ||
| 1025 | // Add wildcard and null-terminator | |
| 1026 | try app_buf.append(allocator, '*'); | |
| 1027 | try app_buf.append(allocator, 0); | |
| 1028 | const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; | |
| 1029 | ||
| 1030 | // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries | |
| 1031 | // returned per NtQueryDirectoryFile call. | |
| 1032 | var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined; | |
| 1033 | const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); | |
| 1034 | if (file_information_buf.len < file_info_maximum_single_entry_size) { | |
| 1035 | @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); | |
| 1036 | } | |
| 1037 | var io_status: windows.IO_STATUS_BLOCK = undefined; | |
| 1038 | ||
| 1039 | const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; | |
| 1040 | var pathext_seen = [_]bool{false} ** num_supported_pathext; | |
| 1041 | var any_pathext_seen = false; | |
| 1042 | var unappended_exists = false; | |
| 1043 | ||
| 1044 | // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions | |
| 1045 | // of the app_name we should try to spawn. | |
| 1046 | // Note: This is necessary because the order of the files returned is filesystem-dependent: | |
| 1047 | // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. | |
| 1048 | // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. | |
| 1049 | while (true) { | |
| 1050 | const app_name_len_bytes = math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; | |
| 1051 | var app_name_unicode_string = windows.UNICODE_STRING{ | |
| 1052 | .Length = app_name_len_bytes, | |
| 1053 | .MaximumLength = app_name_len_bytes, | |
| 1054 | .Buffer = @constCast(app_name_wildcard.ptr), | |
| 1055 | }; | |
| 1056 | const rc = windows.ntdll.NtQueryDirectoryFile( | |
| 1057 | dir.fd, | |
| 1058 | null, | |
| 1059 | null, | |
| 1060 | null, | |
| 1061 | &io_status, | |
| 1062 | &file_information_buf, | |
| 1063 | file_information_buf.len, | |
| 1064 | .FileDirectoryInformation, | |
| 1065 | windows.FALSE, // single result | |
| 1066 | &app_name_unicode_string, | |
| 1067 | windows.FALSE, // restart iteration | |
| 1068 | ); | |
| 1069 | ||
| 1070 | // If we get nothing with the wildcard, then we can just bail out | |
| 1071 | // as we know appending PATHEXT will not yield anything. | |
| 1072 | switch (rc) { | |
| 1073 | .SUCCESS => {}, | |
| 1074 | .NO_SUCH_FILE => return error.FileNotFound, | |
| 1075 | .NO_MORE_FILES => break, | |
| 1076 | .ACCESS_DENIED => return error.AccessDenied, | |
| 1077 | else => return windows.unexpectedStatus(rc), | |
| 1078 | } | |
| 1079 | ||
| 1080 | // According to the docs, this can only happen if there is not enough room in the | |
| 1081 | // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. | |
| 1082 | // Therefore, this condition should not be possible to hit with the buffer size we use. | |
| 1083 | std.debug.assert(io_status.Information != 0); | |
| 1084 | ||
| 1085 | var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; | |
| 1086 | while (it.next()) |info| { | |
| 1087 | // Skip directories | |
| 1088 | if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; | |
| 1089 | const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; | |
| 1090 | // Because all results start with the app_name since we're using the wildcard `app_name*`, | |
| 1091 | // if the length is equal to app_name then this is an exact match | |
| 1092 | if (filename.len == app_name_len) { | |
| 1093 | // Note: We can't break early here because it's possible that the unappended version | |
| 1094 | // fails to spawn, in which case we still want to try the PATHEXT appended versions. | |
| 1095 | unappended_exists = true; | |
| 1096 | } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { | |
| 1097 | pathext_seen[@intFromEnum(pathext_ext)] = true; | |
| 1098 | any_pathext_seen = true; | |
| 1099 | } | |
| 1100 | } | |
| 1101 | } | |
| 1102 | ||
| 1103 | const unappended_err = unappended: { | |
| 1104 | if (unappended_exists) { | |
| 1105 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | |
| 1106 | '/', '\\' => {}, | |
| 1107 | else => try dir_buf.append(allocator, fs.path.sep), | |
| 1108 | }; | |
| 1109 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | |
| 1110 | try dir_buf.append(allocator, 0); | |
| 1111 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | |
| 1112 | ||
| 1113 | const is_bat_or_cmd = bat_or_cmd: { | |
| 1114 | const app_name = app_buf.items[0..app_name_len]; | |
| 1115 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false; | |
| 1116 | const ext = app_name[ext_start..]; | |
| 1117 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false; | |
| 1118 | switch (ext_enum) { | |
| 1119 | .cmd, .bat => break :bat_or_cmd true, | |
| 1120 | else => break :bat_or_cmd false, | |
| 1121 | } | |
| 1122 | }; | |
| 1123 | const cmd_line_w = if (is_bat_or_cmd) | |
| 1124 | try cmd_line_cache.scriptCommandLine(full_app_name) | |
| 1125 | else | |
| 1126 | try cmd_line_cache.commandLine(); | |
| 1127 | const app_name_w = if (is_bat_or_cmd) | |
| 1128 | try cmd_line_cache.cmdExePath() | |
| 1129 | else | |
| 1130 | full_app_name; | |
| 1131 | ||
| 1132 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { | |
| 1133 | return; | |
| 1134 | } else |err| switch (err) { | |
| 1135 | error.FileNotFound, | |
| 1136 | error.AccessDenied, | |
| 1137 | => break :unappended err, | |
| 1138 | error.InvalidExe => { | |
| 1139 | // On InvalidExe, if the extension of the app name is .exe then | |
| 1140 | // it's treated as an unrecoverable error. Otherwise, it'll be | |
| 1141 | // skipped as normal. | |
| 1142 | const app_name = app_buf.items[0..app_name_len]; | |
| 1143 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; | |
| 1144 | const ext = app_name[ext_start..]; | |
| 1145 | if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | |
| 1146 | return error.UnrecoverableInvalidExe; | |
| 1147 | } | |
| 1148 | break :unappended err; | |
| 1149 | }, | |
| 1150 | else => return err, | |
| 1151 | } | |
| 1152 | } | |
| 1153 | break :unappended error.FileNotFound; | |
| 1154 | }; | |
| 1155 | ||
| 1156 | if (!any_pathext_seen) return unappended_err; | |
| 1157 | ||
| 1158 | // Now try any PATHEXT appended versions that we've seen | |
| 1159 | var ext_it = mem.tokenizeScalar(u16, pathext, ';'); | |
| 1160 | while (ext_it.next()) |ext| { | |
| 1161 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; | |
| 1162 | if (!pathext_seen[@intFromEnum(ext_enum)]) continue; | |
| 1163 | ||
| 1164 | dir_buf.shrinkRetainingCapacity(dir_path_len); | |
| 1165 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | |
| 1166 | '/', '\\' => {}, | |
| 1167 | else => try dir_buf.append(allocator, fs.path.sep), | |
| 1168 | }; | |
| 1169 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | |
| 1170 | try dir_buf.appendSlice(allocator, ext); | |
| 1171 | try dir_buf.append(allocator, 0); | |
| 1172 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | |
| 1173 | ||
| 1174 | const is_bat_or_cmd = switch (ext_enum) { | |
| 1175 | .cmd, .bat => true, | |
| 1176 | else => false, | |
| 1177 | }; | |
| 1178 | const cmd_line_w = if (is_bat_or_cmd) | |
| 1179 | try cmd_line_cache.scriptCommandLine(full_app_name) | |
| 1180 | else | |
| 1181 | try cmd_line_cache.commandLine(); | |
| 1182 | const app_name_w = if (is_bat_or_cmd) | |
| 1183 | try cmd_line_cache.cmdExePath() | |
| 1184 | else | |
| 1185 | full_app_name; | |
| 1186 | ||
| 1187 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { | |
| 1188 | return; | |
| 1189 | } else |err| switch (err) { | |
| 1190 | error.FileNotFound => continue, | |
| 1191 | error.AccessDenied => continue, | |
| 1192 | error.InvalidExe => { | |
| 1193 | // On InvalidExe, if the extension of the app name is .exe then | |
| 1194 | // it's treated as an unrecoverable error. Otherwise, it'll be | |
| 1195 | // skipped as normal. | |
| 1196 | if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | |
| 1197 | return error.UnrecoverableInvalidExe; | |
| 1198 | } | |
| 1199 | continue; | |
| 1200 | }, | |
| 1201 | else => return err, | |
| 1202 | } | |
| 1203 | } | |
| 1204 | ||
| 1205 | return unappended_err; | |
| 1206 | } | |
| 1207 | ||
| 1208 | fn windowsCreateProcess(app_name: [*:0]u16, cmd_line: [*:0]u16, envp_ptr: ?[*]u16, cwd_ptr: ?[*:0]u16, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION) !void { | |
| 1209 | // TODO the docs for environment pointer say: | |
| 1210 | // > A pointer to the environment block for the new process. If this parameter | |
| 1211 | // > is NULL, the new process uses the environment of the calling process. | |
| 1212 | // > ... | |
| 1213 | // > An environment block can contain either Unicode or ANSI characters. If | |
| 1214 | // > the environment block pointed to by lpEnvironment contains Unicode | |
| 1215 | // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. | |
| 1216 | // > If this parameter is NULL and the environment block of the parent process | |
| 1217 | // > contains Unicode characters, you must also ensure that dwCreationFlags | |
| 1218 | // > includes CREATE_UNICODE_ENVIRONMENT. | |
| 1219 | // This seems to imply that we have to somehow know whether our process parent passed | |
| 1220 | // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter. | |
| 1221 | // Since we do not know this information that would imply that we must not pass NULL | |
| 1222 | // for the parameter. | |
| 1223 | // However this would imply that programs compiled with -DUNICODE could not pass | |
| 1224 | // environment variables to programs that were not, which seems unlikely. | |
| 1225 | // More investigation is needed. | |
| 1226 | return windows.CreateProcessW( | |
| 1227 | app_name, | |
| 1228 | cmd_line, | |
| 1229 | null, | |
| 1230 | null, | |
| 1231 | windows.TRUE, | |
| 1232 | windows.CREATE_UNICODE_ENVIRONMENT, | |
| 1233 | @as(?*anyopaque, @ptrCast(envp_ptr)), | |
| 1234 | cwd_ptr, | |
| 1235 | lpStartupInfo, | |
| 1236 | lpProcessInformation, | |
| 1237 | ); | |
| 1238 | } | |
| 1239 | ||
| 1240 | // Should be kept in sync with `windowsCreateProcessSupportsExtension` | |
| 1241 | const CreateProcessSupportedExtension = enum { | |
| 1242 | bat, | |
| 1243 | cmd, | |
| 1244 | com, | |
| 1245 | exe, | |
| 1246 | }; | |
| 1247 | ||
| 1248 | /// Case-insensitive WTF-16 lookup | |
| 1249 | fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { | |
| 1250 | if (ext.len != 4) return null; | |
| 1251 | const State = enum { | |
| 1252 | start, | |
| 1253 | dot, | |
| 1254 | b, | |
| 1255 | ba, | |
| 1256 | c, | |
| 1257 | cm, | |
| 1258 | co, | |
| 1259 | e, | |
| 1260 | ex, | |
| 1261 | }; | |
| 1262 | var state: State = .start; | |
| 1263 | for (ext) |c| switch (state) { | |
| 1264 | .start => switch (c) { | |
| 1265 | '.' => state = .dot, | |
| 1266 | else => return null, | |
| 1267 | }, | |
| 1268 | .dot => switch (c) { | |
| 1269 | 'b', 'B' => state = .b, | |
| 1270 | 'c', 'C' => state = .c, | |
| 1271 | 'e', 'E' => state = .e, | |
| 1272 | else => return null, | |
| 1273 | }, | |
| 1274 | .b => switch (c) { | |
| 1275 | 'a', 'A' => state = .ba, | |
| 1276 | else => return null, | |
| 1277 | }, | |
| 1278 | .c => switch (c) { | |
| 1279 | 'm', 'M' => state = .cm, | |
| 1280 | 'o', 'O' => state = .co, | |
| 1281 | else => return null, | |
| 1282 | }, | |
| 1283 | .e => switch (c) { | |
| 1284 | 'x', 'X' => state = .ex, | |
| 1285 | else => return null, | |
| 1286 | }, | |
| 1287 | .ba => switch (c) { | |
| 1288 | 't', 'T' => return .bat, | |
| 1289 | else => return null, | |
| 1290 | }, | |
| 1291 | .cm => switch (c) { | |
| 1292 | 'd', 'D' => return .cmd, | |
| 1293 | else => return null, | |
| 1294 | }, | |
| 1295 | .co => switch (c) { | |
| 1296 | 'm', 'M' => return .com, | |
| 1297 | else => return null, | |
| 1298 | }, | |
| 1299 | .ex => switch (c) { | |
| 1300 | 'e', 'E' => return .exe, | |
| 1301 | else => return null, | |
| 1302 | }, | |
| 1303 | }; | |
| 1304 | return null; | |
| 1305 | } | |
| 1306 | ||
| 1307 | test windowsCreateProcessSupportsExtension { | |
| 1308 | try std.testing.expectEqual(CreateProcessSupportedExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?); | |
| 1309 | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); | |
| 1310 | } | |
| 1311 | ||
| 1312 | /// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW. | |
| 1313 | /// | |
| 1314 | /// Serialization is done on-demand and the result is cached in order to allow for: | |
| 1315 | /// - Only serializing the particular type of command line needed (`.bat`/`.cmd` | |
| 1316 | /// command line serialization is different from `.exe`/etc) | |
| 1317 | /// - Reusing the serialized command lines if necessary (i.e. if the execution | |
| 1318 | /// of a command fails and the PATH is going to be continued to be searched | |
| 1319 | /// for more candidates) | |
| 1320 | pub const WindowsCommandLineCache = struct { | |
| 1321 | cmd_line: ?[:0]u16 = null, | |
| 1322 | script_cmd_line: ?[:0]u16 = null, | |
| 1323 | cmd_exe_path: ?[:0]u16 = null, | |
| 1324 | argv: []const []const u8, | |
| 1325 | allocator: mem.Allocator, | |
| 1326 | ||
| 1327 | pub fn init(allocator: mem.Allocator, argv: []const []const u8) WindowsCommandLineCache { | |
| 1328 | return .{ | |
| 1329 | .allocator = allocator, | |
| 1330 | .argv = argv, | |
| 1331 | }; | |
| 1332 | } | |
| 1333 | ||
| 1334 | pub fn deinit(self: *WindowsCommandLineCache) void { | |
| 1335 | if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line); | |
| 1336 | if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line); | |
| 1337 | if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path); | |
| 1338 | } | |
| 1339 | ||
| 1340 | pub fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 { | |
| 1341 | if (self.cmd_line == null) { | |
| 1342 | self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv); | |
| 1343 | } | |
| 1344 | return self.cmd_line.?; | |
| 1345 | } | |
| 1346 | ||
| 1347 | /// Not cached, since the path to the batch script will change during PATH searching. | |
| 1348 | /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched, | |
| 1349 | /// then script_path should include both the search path and the script filename | |
| 1350 | /// (this allows avoiding cmd.exe having to search the PATH again). | |
| 1351 | pub fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 { | |
| 1352 | if (self.script_cmd_line) |v| self.allocator.free(v); | |
| 1353 | self.script_cmd_line = try argvToScriptCommandLineWindows( | |
| 1354 | self.allocator, | |
| 1355 | script_path, | |
| 1356 | self.argv[1..], | |
| 1357 | ); | |
| 1358 | return self.script_cmd_line.?; | |
| 1359 | } | |
| 1360 | ||
| 1361 | pub fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 { | |
| 1362 | if (self.cmd_exe_path == null) { | |
| 1363 | self.cmd_exe_path = try windowsCmdExePath(self.allocator); | |
| 1364 | } | |
| 1365 | return self.cmd_exe_path.?; | |
| 1366 | } | |
| 1367 | }; | |
| 1368 | ||
| 1369 | pub fn windowsCmdExePath(allocator: mem.Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { | |
| 1370 | var buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 128); | |
| 1371 | errdefer buf.deinit(allocator); | |
| 1372 | while (true) { | |
| 1373 | const unused_slice = buf.unusedCapacitySlice(); | |
| 1374 | // TODO: Get the system directory from PEB.ReadOnlyStaticServerData | |
| 1375 | const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len)); | |
| 1376 | if (len == 0) { | |
| 1377 | switch (windows.kernel32.GetLastError()) { | |
| 1378 | else => |err| return windows.unexpectedError(err), | |
| 1379 | } | |
| 1380 | } | |
| 1381 | if (len > unused_slice.len) { | |
| 1382 | try buf.ensureUnusedCapacity(allocator, len); | |
| 1383 | } else { | |
| 1384 | buf.items.len = len; | |
| 1385 | break; | |
| 1386 | } | |
| 1387 | } | |
| 1388 | switch (buf.items[buf.items.len - 1]) { | |
| 1389 | '/', '\\' => {}, | |
| 1390 | else => try buf.append(allocator, fs.path.sep), | |
| 1391 | } | |
| 1392 | try buf.appendSlice(allocator, std.unicode.utf8ToUtf16LeStringLiteral("cmd.exe")); | |
| 1393 | return try buf.toOwnedSliceSentinel(allocator, 0); | |
| 1394 | } | |
| 1395 | ||
| 1396 | pub const ArgvToScriptCommandLineError = error{ | |
| 1397 | OutOfMemory, | |
| 1398 | InvalidWtf8, | |
| 1399 | /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | |
| 1400 | /// within arguments when executing a `.bat`/`.cmd` script. | |
| 1401 | /// - NUL/LF signifiies end of arguments, so anything afterwards | |
| 1402 | /// would be lost after execution. | |
| 1403 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | |
| 1404 | /// would be lost after execution. | |
| 1405 | InvalidBatchScriptArg, | |
| 1406 | }; | |
| 1407 | ||
| 1408 | /// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific | |
| 1409 | /// escaping rules. The caller owns the returned slice. | |
| 1410 | /// | |
| 1411 | /// Escapes `argv` using the suggested mitigation against arbitrary command execution from: | |
| 1412 | /// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | |
| 1413 | pub fn argvToScriptCommandLineWindows( | |
| 1414 | allocator: mem.Allocator, | |
| 1415 | /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD. | |
| 1416 | /// The script must have been verified to exist at this path before calling this function. | |
| 1417 | script_path: []const u16, | |
| 1418 | /// Arguments, not including the script name itself. Expected to be encoded as WTF-8. | |
| 1419 | script_args: []const []const u8, | |
| 1420 | ) ArgvToScriptCommandLineError![:0]u16 { | |
| 1421 | var buf = try std.ArrayList(u8).initCapacity(allocator, 64); | |
| 1422 | defer buf.deinit(); | |
| 1423 | ||
| 1424 | // `/d` disables execution of AutoRun commands. | |
| 1425 | // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation: | |
| 1426 | // > If delayed expansion is enabled via the registry value DelayedExpansion, | |
| 1427 | // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option. | |
| 1428 | // > Escaping for % requires the command extension to be enabled. | |
| 1429 | // > If it’s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option. | |
| 1430 | // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | |
| 1431 | buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \""); | |
| 1432 | ||
| 1433 | // Always quote the path to the script arg | |
| 1434 | buf.appendAssumeCapacity('"'); | |
| 1435 | // We always want the path to the batch script to include a path separator in order to | |
| 1436 | // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary | |
| 1437 | // command execution mitigation, we just know exactly what script we want to execute | |
| 1438 | // at this point, and potentially making cmd.exe re-find it is unnecessary. | |
| 1439 | // | |
| 1440 | // If the script path does not have a path separator, then we know its relative to CWD and | |
| 1441 | // we can just put `.\` in the front. | |
| 1442 | if (mem.indexOfAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) { | |
| 1443 | try buf.appendSlice(".\\"); | |
| 1444 | } | |
| 1445 | // Note that we don't do any escaping/mitigations for this argument, since the relevant | |
| 1446 | // characters (", %, etc) are illegal in file paths and this function should only be called | |
| 1447 | // with script paths that have been verified to exist. | |
| 1448 | try std.unicode.wtf16LeToWtf8ArrayList(&buf, script_path); | |
| 1449 | buf.appendAssumeCapacity('"'); | |
| 1450 | ||
| 1451 | for (script_args) |arg| { | |
| 1452 | // Literal carriage returns get stripped when run through cmd.exe | |
| 1453 | // and NUL/newlines act as 'end of command.' Because of this, it's basically | |
| 1454 | // always a mistake to include these characters in argv, so it's | |
| 1455 | // an error condition in order to ensure that the return of this | |
| 1456 | // function can always roundtrip through cmd.exe. | |
| 1457 | if (std.mem.indexOfAny(u8, arg, "\x00\r\n") != null) { | |
| 1458 | return error.InvalidBatchScriptArg; | |
| 1459 | } | |
| 1460 | ||
| 1461 | // Separate args with a space. | |
| 1462 | try buf.append(' '); | |
| 1463 | ||
| 1464 | // Need to quote if the argument is empty (otherwise the arg would just be lost) | |
| 1465 | // or if the last character is a `\`, since then something like "%~2" in a .bat | |
| 1466 | // script would cause the closing " to be escaped which we don't want. | |
| 1467 | var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\'; | |
| 1468 | if (!needs_quotes) { | |
| 1469 | for (arg) |c| { | |
| 1470 | switch (c) { | |
| 1471 | // Known good characters that don't need to be quoted | |
| 1472 | 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {}, | |
| 1473 | // When in doubt, quote | |
| 1474 | else => { | |
| 1475 | needs_quotes = true; | |
| 1476 | break; | |
| 1477 | }, | |
| 1478 | } | |
| 1479 | } | |
| 1480 | } | |
| 1481 | if (needs_quotes) { | |
| 1482 | try buf.append('"'); | |
| 1483 | } | |
| 1484 | var backslashes: usize = 0; | |
| 1485 | for (arg) |c| { | |
| 1486 | switch (c) { | |
| 1487 | '\\' => { | |
| 1488 | backslashes += 1; | |
| 1489 | }, | |
| 1490 | '"' => { | |
| 1491 | try buf.appendNTimes('\\', backslashes); | |
| 1492 | try buf.append('"'); | |
| 1493 | backslashes = 0; | |
| 1494 | }, | |
| 1495 | // Replace `%` with `%%cd:~,%`. | |
| 1496 | // | |
| 1497 | // cmd.exe allows extracting a substring from an environment | |
| 1498 | // variable with the syntax: `%foo:~<start_index>,<end_index>%`. | |
| 1499 | // Therefore, `%cd:~,%` will always expand to an empty string | |
| 1500 | // since both the start and end index are blank, and it is assumed | |
| 1501 | // that `%cd%` is always available since it is a built-in variable | |
| 1502 | // that corresponds to the current directory. | |
| 1503 | // | |
| 1504 | // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%` | |
| 1505 | // will stop `%foo%` from being expanded and *after* expansion | |
| 1506 | // we'll still be left with `%foo%` (the literal string). | |
| 1507 | '%' => { | |
| 1508 | // the trailing `%` is appended outside the switch | |
| 1509 | try buf.appendSlice("%%cd:~,"); | |
| 1510 | backslashes = 0; | |
| 1511 | }, | |
| 1512 | else => { | |
| 1513 | backslashes = 0; | |
| 1514 | }, | |
| 1515 | } | |
| 1516 | try buf.append(c); | |
| 1517 | } | |
| 1518 | if (needs_quotes) { | |
| 1519 | try buf.appendNTimes('\\', backslashes); | |
| 1520 | try buf.append('"'); | |
| 1521 | } | |
| 1522 | } | |
| 1523 | ||
| 1524 | try buf.append('"'); | |
| 1525 | ||
| 1526 | return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | |
| 1527 | } | |
| 1528 | ||
| 1529 | pub const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 }; | |
| 1530 | ||
| 1531 | /// Serializes `argv` to a Windows command-line string suitable for passing to a child process and | |
| 1532 | /// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice. | |
| 1533 | pub fn argvToCommandLineWindows( | |
| 1534 | allocator: mem.Allocator, | |
| 1535 | argv: []const []const u8, | |
| 1536 | ) ArgvToCommandLineError![:0]u16 { | |
| 1537 | var buf = std.ArrayList(u8).init(allocator); | |
| 1538 | defer buf.deinit(); | |
| 1539 | ||
| 1540 | if (argv.len != 0) { | |
| 1541 | const arg0 = argv[0]; | |
| 1542 | ||
| 1543 | // The first argument must be quoted if it contains spaces or ASCII control characters | |
| 1544 | // (excluding DEL). It also follows special quoting rules where backslashes have no special | |
| 1545 | // interpretation, which makes it impossible to pass certain first arguments containing | |
| 1546 | // double quotes to a child process without characters from the first argument leaking into | |
| 1547 | // subsequent ones (which could have security implications). | |
| 1548 | // | |
| 1549 | // Empty arguments technically don't need quotes, but we quote them anyway for maximum | |
| 1550 | // compatibility with different implementations of the 'CommandLineToArgvW' algorithm. | |
| 1551 | // | |
| 1552 | // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject | |
| 1553 | // all first arguments containing double quotes, even ones that we could theoretically | |
| 1554 | // serialize in unquoted form. | |
| 1555 | var needs_quotes = arg0.len == 0; | |
| 1556 | for (arg0) |c| { | |
| 1557 | if (c <= ' ') { | |
| 1558 | needs_quotes = true; | |
| 1559 | } else if (c == '"') { | |
| 1560 | return error.InvalidArg0; | |
| 1561 | } | |
| 1562 | } | |
| 1563 | if (needs_quotes) { | |
| 1564 | try buf.append('"'); | |
| 1565 | try buf.appendSlice(arg0); | |
| 1566 | try buf.append('"'); | |
| 1567 | } else { | |
| 1568 | try buf.appendSlice(arg0); | |
| 1569 | } | |
| 1570 | ||
| 1571 | for (argv[1..]) |arg| { | |
| 1572 | try buf.append(' '); | |
| 1573 | ||
| 1574 | // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes, | |
| 1575 | // or if they are empty. For simplicity and for maximum compatibility with different | |
| 1576 | // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII | |
| 1577 | // control characters (again, excluding DEL). | |
| 1578 | needs_quotes = for (arg) |c| { | |
| 1579 | if (c <= ' ' or c == '"') { | |
| 1580 | break true; | |
| 1581 | } | |
| 1582 | } else arg.len == 0; | |
| 1583 | if (!needs_quotes) { | |
| 1584 | try buf.appendSlice(arg); | |
| 1585 | continue; | |
| 1586 | } | |
| 1587 | ||
| 1588 | try buf.append('"'); | |
| 1589 | var backslash_count: usize = 0; | |
| 1590 | for (arg) |byte| { | |
| 1591 | switch (byte) { | |
| 1592 | '\\' => { | |
| 1593 | backslash_count += 1; | |
| 1594 | }, | |
| 1595 | '"' => { | |
| 1596 | try buf.appendNTimes('\\', backslash_count * 2 + 1); | |
| 1597 | try buf.append('"'); | |
| 1598 | backslash_count = 0; | |
| 1599 | }, | |
| 1600 | else => { | |
| 1601 | try buf.appendNTimes('\\', backslash_count); | |
| 1602 | try buf.append(byte); | |
| 1603 | backslash_count = 0; | |
| 1604 | }, | |
| 1605 | } | |
| 1606 | } | |
| 1607 | try buf.appendNTimes('\\', backslash_count * 2); | |
| 1608 | try buf.append('"'); | |
| 1609 | } | |
| 1610 | } | |
| 1611 | ||
| 1612 | return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | |
| 1613 | } | |
| 1614 | ||
| 1615 | test argvToCommandLineWindows { | |
| 1616 | const t = testArgvToCommandLineWindows; | |
| 1617 | ||
| 1618 | try t(&.{ | |
| 1619 | \\C:\Program Files\zig\zig.exe | |
| 1620 | , | |
| 1621 | \\run | |
| 1622 | , | |
| 1623 | \\.\src\main.zig | |
| 1624 | , | |
| 1625 | \\-target | |
| 1626 | , | |
| 1627 | \\x86_64-windows-gnu | |
| 1628 | , | |
| 1629 | \\-O | |
| 1630 | , | |
| 1631 | \\ReleaseSafe | |
| 1632 | , | |
| 1633 | \\-- | |
| 1634 | , | |
| 1635 | \\--emoji=🗿 | |
| 1636 | , | |
| 1637 | \\--eval=new Regex("Dwayne \"The Rock\" Johnson") | |
| 1638 | , | |
| 1639 | }, | |
| 1640 | \\"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\")" | |
| 1641 | ); | |
| 1642 | ||
| 1643 | try t(&.{}, ""); | |
| 1644 | try t(&.{""}, "\"\""); | |
| 1645 | try t(&.{" "}, "\" \""); | |
| 1646 | try t(&.{"\t"}, "\"\t\""); | |
| 1647 | try t(&.{"\x07"}, "\"\x07\""); | |
| 1648 | try t(&.{"🦎"}, "🦎"); | |
| 1649 | ||
| 1650 | try t( | |
| 1651 | &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" }, | |
| 1652 | "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee", | |
| 1653 | ); | |
| 1654 | ||
| 1655 | try t( | |
| 1656 | &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" }, | |
| 1657 | "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"", | |
| 1658 | ); | |
| 1659 | ||
| 1660 | try std.testing.expectError( | |
| 1661 | error.InvalidArg0, | |
| 1662 | argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}), | |
| 1663 | ); | |
| 1664 | try std.testing.expectError( | |
| 1665 | error.InvalidArg0, | |
| 1666 | argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}), | |
| 1667 | ); | |
| 1668 | try std.testing.expectError( | |
| 1669 | error.InvalidArg0, | |
| 1670 | argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}), | |
| 1671 | ); | |
| 1672 | } | |
| 1673 | ||
| 1674 | fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void { | |
| 1675 | const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); | |
| 1676 | defer std.testing.allocator.free(cmd_line_w); | |
| 1677 | ||
| 1678 | const cmd_line = try unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w); | |
| 1679 | defer std.testing.allocator.free(cmd_line); | |
| 1680 | ||
| 1681 | try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); | |
| 1682 | } | |
| 1683 | ||
| 1684 | fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { | |
| 1685 | if (rd) |h| posix.close(h); | |
| 1686 | if (wr) |h| posix.close(h); | |
| 1687 | } | |
| 1688 | ||
| 1689 | fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | |
| 1690 | var rd_h: windows.HANDLE = undefined; | |
| 1691 | var wr_h: windows.HANDLE = undefined; | |
| 1692 | try windows.CreatePipe(&rd_h, &wr_h, sattr); | |
| 1693 | errdefer windowsDestroyPipe(rd_h, wr_h); | |
| 1694 | try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); | |
| 1695 | rd.* = rd_h; | |
| 1696 | wr.* = wr_h; | |
| 1697 | } | |
| 1698 | ||
| 1699 | var pipe_name_counter = std.atomic.Value(u32).init(1); | |
| 1700 | ||
| 1701 | fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | |
| 1702 | var tmp_bufw: [128]u16 = undefined; | |
| 1703 | ||
| 1704 | // Anonymous pipes are built upon Named pipes. | |
| 1705 | // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe | |
| 1706 | // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. | |
| 1707 | // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations | |
| 1708 | const pipe_path = blk: { | |
| 1709 | var tmp_buf: [128]u8 = undefined; | |
| 1710 | // Forge a random path for the pipe. | |
| 1711 | const pipe_path = std.fmt.bufPrintZ( | |
| 1712 | &tmp_buf, | |
| 1713 | "\\\\.\\pipe\\zig-childprocess-{d}-{d}", | |
| 1714 | .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, | |
| 1715 | ) catch unreachable; | |
| 1716 | const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable; | |
| 1717 | tmp_bufw[len] = 0; | |
| 1718 | break :blk tmp_bufw[0..len :0]; | |
| 1719 | }; | |
| 1720 | ||
| 1721 | // Create the read handle that can be used with overlapped IO ops. | |
| 1722 | const read_handle = windows.kernel32.CreateNamedPipeW( | |
| 1723 | pipe_path.ptr, | |
| 1724 | windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, | |
| 1725 | windows.PIPE_TYPE_BYTE, | |
| 1726 | 1, | |
| 1727 | 4096, | |
| 1728 | 4096, | |
| 1729 | 0, | |
| 1730 | sattr, | |
| 1731 | ); | |
| 1732 | if (read_handle == windows.INVALID_HANDLE_VALUE) { | |
| 1733 | switch (windows.kernel32.GetLastError()) { | |
| 1734 | else => |err| return windows.unexpectedError(err), | |
| 1735 | } | |
| 1736 | } | |
| 1737 | errdefer posix.close(read_handle); | |
| 1738 | ||
| 1739 | var sattr_copy = sattr.*; | |
| 1740 | const write_handle = windows.kernel32.CreateFileW( | |
| 1741 | pipe_path.ptr, | |
| 1742 | windows.GENERIC_WRITE, | |
| 1743 | 0, | |
| 1744 | &sattr_copy, | |
| 1745 | windows.OPEN_EXISTING, | |
| 1746 | windows.FILE_ATTRIBUTE_NORMAL, | |
| 1747 | null, | |
| 1748 | ); | |
| 1749 | if (write_handle == windows.INVALID_HANDLE_VALUE) { | |
| 1750 | switch (windows.kernel32.GetLastError()) { | |
| 1751 | else => |err| return windows.unexpectedError(err), | |
| 1752 | } | |
| 1753 | } | |
| 1754 | errdefer posix.close(write_handle); | |
| 1755 | ||
| 1756 | try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); | |
| 1757 | ||
| 1758 | rd.* = read_handle; | |
| 1759 | wr.* = write_handle; | |
| 1760 | } | |
| 1761 | ||
| 1762 | fn destroyPipe(pipe: [2]posix.fd_t) void { | |
| 1763 | posix.close(pipe[0]); | |
| 1764 | if (pipe[0] != pipe[1]) posix.close(pipe[1]); | |
| 1765 | } | |
| 1766 | ||
| 1767 | // Child of fork calls this to report an error to the fork parent. | |
| 1768 | // Then the child exits. | |
| 1769 | fn forkChildErrReport(fd: i32, err: ChildProcess.SpawnError) noreturn { | |
| 1770 | writeIntFd(fd, @as(ErrInt, @intFromError(err))) catch {}; | |
| 1771 | // If we're linking libc, some naughty applications may have registered atexit handlers | |
| 1772 | // which we really do not want to run in the fork child. I caught LLVM doing this and | |
| 1773 | // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne, | |
| 1774 | // "Why'd you have to go and make things so complicated?" | |
| 1775 | if (builtin.link_libc) { | |
| 1776 | // The _exit(2) function does nothing but make the exit syscall, unlike exit(3) | |
| 1777 | std.c._exit(1); | |
| 1778 | } | |
| 1779 | posix.exit(1); | |
| 1780 | } | |
| 1781 | ||
| 1782 | const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); | |
| 1783 | ||
| 1784 | fn writeIntFd(fd: i32, value: ErrInt) !void { | |
| 1785 | const file = File{ .handle = fd }; | |
| 1786 | file.writer().writeInt(u64, @intCast(value), .little) catch return error.SystemResources; | |
| 1787 | } | |
| 1788 | ||
| 1789 | fn readIntFd(fd: i32) !ErrInt { | |
| 1790 | const file = File{ .handle = fd }; | |
| 1791 | return @as(ErrInt, @intCast(file.reader().readInt(u64, .little) catch return error.SystemResources)); | |
| 1792 | } | |
| 1793 | ||
| 1794 | /// Caller must free result. | |
| 1795 | pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 { | |
| 1796 | // count bytes needed | |
| 1797 | const max_chars_needed = x: { | |
| 1798 | var max_chars_needed: usize = 4; // 4 for the final 4 null bytes | |
| 1799 | var it = env_map.iterator(); | |
| 1800 | while (it.next()) |pair| { | |
| 1801 | // +1 for '=' | |
| 1802 | // +1 for null byte | |
| 1803 | max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2; | |
| 1804 | } | |
| 1805 | break :x max_chars_needed; | |
| 1806 | }; | |
| 1807 | const result = try allocator.alloc(u16, max_chars_needed); | |
| 1808 | errdefer allocator.free(result); | |
| 1809 | ||
| 1810 | var it = env_map.iterator(); | |
| 1811 | var i: usize = 0; | |
| 1812 | while (it.next()) |pair| { | |
| 1813 | i += try unicode.wtf8ToWtf16Le(result[i..], pair.key_ptr.*); | |
| 1814 | result[i] = '='; | |
| 1815 | i += 1; | |
| 1816 | i += try unicode.wtf8ToWtf16Le(result[i..], pair.value_ptr.*); | |
| 1817 | result[i] = 0; | |
| 1818 | i += 1; | |
| 1819 | } | |
| 1820 | result[i] = 0; | |
| 1821 | i += 1; | |
| 1822 | result[i] = 0; | |
| 1823 | i += 1; | |
| 1824 | result[i] = 0; | |
| 1825 | i += 1; | |
| 1826 | result[i] = 0; | |
| 1827 | i += 1; | |
| 1828 | return try allocator.realloc(result, i); | |
| 1829 | } | |
| 1830 | ||
| 1831 | pub fn createNullDelimitedEnvMap(arena: mem.Allocator, env_map: *const EnvMap) ![:null]?[*:0]u8 { | |
| 1832 | const envp_count = env_map.count(); | |
| 1833 | const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null); | |
| 1834 | { | |
| 1835 | var it = env_map.iterator(); | |
| 1836 | var i: usize = 0; | |
| 1837 | while (it.next()) |pair| : (i += 1) { | |
| 1838 | const env_buf = try arena.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.len + 1, 0); | |
| 1839 | @memcpy(env_buf[0..pair.key_ptr.len], pair.key_ptr.*); | |
| 1840 | env_buf[pair.key_ptr.len] = '='; | |
| 1841 | @memcpy(env_buf[pair.key_ptr.len + 1 ..][0..pair.value_ptr.len], pair.value_ptr.*); | |
| 1842 | envp_buf[i] = env_buf.ptr; | |
| 1843 | } | |
| 1844 | assert(i == envp_count); | |
| 1845 | } | |
| 1846 | return envp_buf; | |
| 1847 | } | |
| 1848 | ||
| 1849 | test createNullDelimitedEnvMap { | |
| 1850 | const testing = std.testing; | |
| 1851 | const allocator = testing.allocator; | |
| 1852 | var envmap = EnvMap.init(allocator); | |
| 1853 | defer envmap.deinit(); | |
| 1854 | ||
| 1855 | try envmap.put("HOME", "/home/ifreund"); | |
| 1856 | try envmap.put("WAYLAND_DISPLAY", "wayland-1"); | |
| 1857 | try envmap.put("DISPLAY", ":1"); | |
| 1858 | try envmap.put("DEBUGINFOD_URLS", " "); | |
| 1859 | try envmap.put("XCURSOR_SIZE", "24"); | |
| 1860 | ||
| 1861 | var arena = std.heap.ArenaAllocator.init(allocator); | |
| 1862 | defer arena.deinit(); | |
| 1863 | const environ = try createNullDelimitedEnvMap(arena.allocator(), &envmap); | |
| 1864 | ||
| 1865 | try testing.expectEqual(@as(usize, 5), environ.len); | |
| 1866 | ||
| 1867 | inline for (.{ | |
| 1868 | "HOME=/home/ifreund", | |
| 1869 | "WAYLAND_DISPLAY=wayland-1", | |
| 1870 | "DISPLAY=:1", | |
| 1871 | "DEBUGINFOD_URLS= ", | |
| 1872 | "XCURSOR_SIZE=24", | |
| 1873 | }) |target| { | |
| 1874 | for (environ) |variable| { | |
| 1875 | if (mem.eql(u8, mem.span(variable orelse continue), target)) break; | |
| 1876 | } else { | |
| 1877 | try testing.expect(false); // Environment variable not found | |
| 1878 | } | |
| 1879 | } | |
| 1880 | } |
lib/std/process.zig+111-24| ... | ... | @@ -6,12 +6,12 @@ const math = std.math; |
| 6 | 6 | const Allocator = mem.Allocator; |
| 7 | 7 | const assert = std.debug.assert; |
| 8 | 8 | const testing = std.testing; |
| 9 | const child_process = @import("child_process.zig"); | |
| 10 | 9 | const native_os = builtin.os.tag; |
| 11 | 10 | const posix = std.posix; |
| 12 | 11 | const windows = std.os.windows; |
| 12 | const unicode = std.unicode; | |
| 13 | 13 | |
| 14 | pub const Child = child_process.ChildProcess; | |
| 14 | pub const Child = @import("process/Child.zig"); | |
| 15 | 15 | pub const abort = posix.abort; |
| 16 | 16 | pub const exit = posix.exit; |
| 17 | 17 | pub const changeCurDir = posix.chdir; |
| ... | ... | @@ -86,7 +86,7 @@ pub const EnvMap = struct { |
| 86 | 86 | _ = self; |
| 87 | 87 | if (native_os == .windows) { |
| 88 | 88 | var h = std.hash.Wyhash.init(0); |
| 89 | var it = std.unicode.Wtf8View.initUnchecked(s).iterator(); | |
| 89 | var it = unicode.Wtf8View.initUnchecked(s).iterator(); | |
| 90 | 90 | while (it.nextCodepoint()) |cp| { |
| 91 | 91 | const cp_upper = upcase(cp); |
| 92 | 92 | h.update(&[_]u8{ |
| ... | ... | @@ -103,8 +103,8 @@ pub const EnvMap = struct { |
| 103 | 103 | pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { |
| 104 | 104 | _ = self; |
| 105 | 105 | if (native_os == .windows) { |
| 106 | var it_a = std.unicode.Wtf8View.initUnchecked(a).iterator(); | |
| 107 | var it_b = std.unicode.Wtf8View.initUnchecked(b).iterator(); | |
| 106 | var it_a = unicode.Wtf8View.initUnchecked(a).iterator(); | |
| 107 | var it_b = unicode.Wtf8View.initUnchecked(b).iterator(); | |
| 108 | 108 | while (true) { |
| 109 | 109 | const c_a = it_a.nextCodepoint() orelse break; |
| 110 | 110 | const c_b = it_b.nextCodepoint() orelse return false; |
| ... | ... | @@ -141,7 +141,7 @@ pub const EnvMap = struct { |
| 141 | 141 | /// If `putMove` fails, the ownership of key and value does not transfer. |
| 142 | 142 | /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. |
| 143 | 143 | pub fn putMove(self: *EnvMap, key: []u8, value: []u8) !void { |
| 144 | assert(std.unicode.wtf8ValidateSlice(key)); | |
| 144 | assert(unicode.wtf8ValidateSlice(key)); | |
| 145 | 145 | const get_or_put = try self.hash_map.getOrPut(key); |
| 146 | 146 | if (get_or_put.found_existing) { |
| 147 | 147 | self.free(get_or_put.key_ptr.*); |
| ... | ... | @@ -154,7 +154,7 @@ pub const EnvMap = struct { |
| 154 | 154 | /// `key` and `value` are copied into the EnvMap. |
| 155 | 155 | /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. |
| 156 | 156 | pub fn put(self: *EnvMap, key: []const u8, value: []const u8) !void { |
| 157 | assert(std.unicode.wtf8ValidateSlice(key)); | |
| 157 | assert(unicode.wtf8ValidateSlice(key)); | |
| 158 | 158 | const value_copy = try self.copy(value); |
| 159 | 159 | errdefer self.free(value_copy); |
| 160 | 160 | const get_or_put = try self.hash_map.getOrPut(key); |
| ... | ... | @@ -173,7 +173,7 @@ pub const EnvMap = struct { |
| 173 | 173 | /// The returned pointer is invalidated if the map resizes. |
| 174 | 174 | /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. |
| 175 | 175 | pub fn getPtr(self: EnvMap, key: []const u8) ?*[]const u8 { |
| 176 | assert(std.unicode.wtf8ValidateSlice(key)); | |
| 176 | assert(unicode.wtf8ValidateSlice(key)); | |
| 177 | 177 | return self.hash_map.getPtr(key); |
| 178 | 178 | } |
| 179 | 179 | |
| ... | ... | @@ -182,7 +182,7 @@ pub const EnvMap = struct { |
| 182 | 182 | /// key is removed from the map. |
| 183 | 183 | /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. |
| 184 | 184 | pub fn get(self: EnvMap, key: []const u8) ?[]const u8 { |
| 185 | assert(std.unicode.wtf8ValidateSlice(key)); | |
| 185 | assert(unicode.wtf8ValidateSlice(key)); | |
| 186 | 186 | return self.hash_map.get(key); |
| 187 | 187 | } |
| 188 | 188 | |
| ... | ... | @@ -190,7 +190,7 @@ pub const EnvMap = struct { |
| 190 | 190 | /// This invalidates the value returned by get() for this key. |
| 191 | 191 | /// On Windows `key` must be a valid [WTF-8](https://simonsapin.github.io/wtf-8/) string. |
| 192 | 192 | pub fn remove(self: *EnvMap, key: []const u8) void { |
| 193 | assert(std.unicode.wtf8ValidateSlice(key)); | |
| 193 | assert(unicode.wtf8ValidateSlice(key)); | |
| 194 | 194 | const kv = self.hash_map.fetchRemove(key) orelse return; |
| 195 | 195 | self.free(kv.key); |
| 196 | 196 | self.free(kv.value); |
| ... | ... | @@ -260,7 +260,7 @@ test EnvMap { |
| 260 | 260 | try testing.expectEqualStrings("something else", env.get("кириллица").?); |
| 261 | 261 | |
| 262 | 262 | // and WTF-8 that's not valid UTF-8 |
| 263 | const wtf8_with_surrogate_pair = try std.unicode.wtf16LeToWtf8Alloc(testing.allocator, &[_]u16{ | |
| 263 | const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(testing.allocator, &[_]u16{ | |
| 264 | 264 | std.mem.nativeToLittle(u16, 0xD83D), // unpaired high surrogate |
| 265 | 265 | }); |
| 266 | 266 | defer testing.allocator.free(wtf8_with_surrogate_pair); |
| ... | ... | @@ -300,7 +300,7 @@ pub fn getEnvMap(allocator: Allocator) GetEnvMapError!EnvMap { |
| 300 | 300 | |
| 301 | 301 | while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {} |
| 302 | 302 | const key_w = ptr[key_start..i]; |
| 303 | const key = try std.unicode.wtf16LeToWtf8Alloc(allocator, key_w); | |
| 303 | const key = try unicode.wtf16LeToWtf8Alloc(allocator, key_w); | |
| 304 | 304 | errdefer allocator.free(key); |
| 305 | 305 | |
| 306 | 306 | if (ptr[i] == '=') i += 1; |
| ... | ... | @@ -308,7 +308,7 @@ pub fn getEnvMap(allocator: Allocator) GetEnvMapError!EnvMap { |
| 308 | 308 | const value_start = i; |
| 309 | 309 | while (ptr[i] != 0) : (i += 1) {} |
| 310 | 310 | const value_w = ptr[value_start..i]; |
| 311 | const value = try std.unicode.wtf16LeToWtf8Alloc(allocator, value_w); | |
| 311 | const value = try unicode.wtf16LeToWtf8Alloc(allocator, value_w); | |
| 312 | 312 | errdefer allocator.free(value); |
| 313 | 313 | |
| 314 | 314 | i += 1; // skip over null byte |
| ... | ... | @@ -401,13 +401,13 @@ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError |
| 401 | 401 | const result_w = blk: { |
| 402 | 402 | var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator); |
| 403 | 403 | const stack_allocator = stack_alloc.get(); |
| 404 | const key_w = try std.unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); | |
| 404 | const key_w = try unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); | |
| 405 | 405 | defer stack_allocator.free(key_w); |
| 406 | 406 | |
| 407 | 407 | break :blk getenvW(key_w) orelse return error.EnvironmentVariableNotFound; |
| 408 | 408 | }; |
| 409 | 409 | // wtf16LeToWtf8Alloc can only fail with OutOfMemory |
| 410 | return std.unicode.wtf16LeToWtf8Alloc(allocator, result_w); | |
| 410 | return unicode.wtf16LeToWtf8Alloc(allocator, result_w); | |
| 411 | 411 | } else if (native_os == .wasi and !builtin.link_libc) { |
| 412 | 412 | var envmap = getEnvMap(allocator) catch return error.OutOfMemory; |
| 413 | 413 | defer envmap.deinit(); |
| ... | ... | @@ -422,7 +422,7 @@ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError |
| 422 | 422 | /// On Windows, `key` must be valid UTF-8. |
| 423 | 423 | pub fn hasEnvVarConstant(comptime key: []const u8) bool { |
| 424 | 424 | if (native_os == .windows) { |
| 425 | const key_w = comptime std.unicode.utf8ToUtf16LeStringLiteral(key); | |
| 425 | const key_w = comptime unicode.utf8ToUtf16LeStringLiteral(key); | |
| 426 | 426 | return getenvW(key_w) != null; |
| 427 | 427 | } else if (native_os == .wasi and !builtin.link_libc) { |
| 428 | 428 | @compileError("hasEnvVarConstant is not supported for WASI without libc"); |
| ... | ... | @@ -445,7 +445,7 @@ pub fn hasEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!bool { |
| 445 | 445 | if (native_os == .windows) { |
| 446 | 446 | var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator); |
| 447 | 447 | const stack_allocator = stack_alloc.get(); |
| 448 | const key_w = try std.unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); | |
| 448 | const key_w = try unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key); | |
| 449 | 449 | defer stack_allocator.free(key_w); |
| 450 | 450 | return getenvW(key_w) != null; |
| 451 | 451 | } else if (native_os == .wasi and !builtin.link_libc) { |
| ... | ... | @@ -659,7 +659,7 @@ pub const ArgIteratorWindows = struct { |
| 659 | 659 | /// The iterator makes a copy of `cmd_line_w` converted WTF-8 and keeps it; it does *not* take |
| 660 | 660 | /// ownership of `cmd_line_w`. |
| 661 | 661 | pub fn init(allocator: Allocator, cmd_line_w: [*:0]const u16) InitError!ArgIteratorWindows { |
| 662 | const cmd_line = try std.unicode.wtf16LeToWtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)); | |
| 662 | const cmd_line = try unicode.wtf16LeToWtf8Alloc(allocator, mem.sliceTo(cmd_line_w, 0)); | |
| 663 | 663 | errdefer allocator.free(cmd_line); |
| 664 | 664 | |
| 665 | 665 | const buffer = try allocator.alloc(u8, cmd_line.len + 1); |
| ... | ... | @@ -728,17 +728,17 @@ pub const ArgIteratorWindows = struct { |
| 728 | 728 | // must be 6 bytes for a surrogate pair to exist. |
| 729 | 729 | if (self.end - self.start >= 6) { |
| 730 | 730 | const window = self.buffer[self.end - 6 .. self.end]; |
| 731 | const view = std.unicode.Wtf8View.init(window) catch return; | |
| 731 | const view = unicode.Wtf8View.init(window) catch return; | |
| 732 | 732 | var it = view.iterator(); |
| 733 | 733 | var pair: [2]u16 = undefined; |
| 734 | 734 | pair[0] = std.mem.nativeToLittle(u16, std.math.cast(u16, it.nextCodepoint().?) orelse return); |
| 735 | if (!std.unicode.utf16IsHighSurrogate(std.mem.littleToNative(u16, pair[0]))) return; | |
| 735 | if (!unicode.utf16IsHighSurrogate(std.mem.littleToNative(u16, pair[0]))) return; | |
| 736 | 736 | pair[1] = std.mem.nativeToLittle(u16, std.math.cast(u16, it.nextCodepoint().?) orelse return); |
| 737 | if (!std.unicode.utf16IsLowSurrogate(std.mem.littleToNative(u16, pair[1]))) return; | |
| 737 | if (!unicode.utf16IsLowSurrogate(std.mem.littleToNative(u16, pair[1]))) return; | |
| 738 | 738 | // We know we have a valid surrogate pair, so convert |
| 739 | 739 | // it to UTF-8, overwriting the surrogate pair's bytes |
| 740 | 740 | // and then chop off the extra bytes. |
| 741 | const len = std.unicode.utf16LeToUtf8(window, &pair) catch unreachable; | |
| 741 | const len = unicode.utf16LeToUtf8(window, &pair) catch unreachable; | |
| 742 | 742 | const delta = 6 - len; |
| 743 | 743 | self.end -= delta; |
| 744 | 744 | } |
| ... | ... | @@ -1341,7 +1341,7 @@ test ArgIteratorWindows { |
| 1341 | 1341 | } |
| 1342 | 1342 | |
| 1343 | 1343 | fn testArgIteratorWindows(cmd_line: []const u8, expected_args: []const []const u8) !void { |
| 1344 | const cmd_line_w = try std.unicode.wtf8ToWtf16LeAllocZ(testing.allocator, cmd_line); | |
| 1344 | const cmd_line_w = try unicode.wtf8ToWtf16LeAllocZ(testing.allocator, cmd_line); | |
| 1345 | 1345 | defer testing.allocator.free(cmd_line_w); |
| 1346 | 1346 | |
| 1347 | 1347 | // next |
| ... | ... | @@ -1642,7 +1642,7 @@ pub fn execve( |
| 1642 | 1642 | |
| 1643 | 1643 | const envp = m: { |
| 1644 | 1644 | if (env_map) |m| { |
| 1645 | const envp_buf = try child_process.createNullDelimitedEnvMap(arena, m); | |
| 1645 | const envp_buf = try createNullDelimitedEnvMap(arena, m); | |
| 1646 | 1646 | break :m envp_buf.ptr; |
| 1647 | 1647 | } else if (builtin.link_libc) { |
| 1648 | 1648 | break :m std.c.environ; |
| ... | ... | @@ -1789,3 +1789,90 @@ pub fn raiseFileDescriptorLimit() void { |
| 1789 | 1789 | test raiseFileDescriptorLimit { |
| 1790 | 1790 | raiseFileDescriptorLimit(); |
| 1791 | 1791 | } |
| 1792 | ||
| 1793 | pub fn createNullDelimitedEnvMap(arena: mem.Allocator, env_map: *const EnvMap) ![:null]?[*:0]u8 { | |
| 1794 | const envp_count = env_map.count(); | |
| 1795 | const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null); | |
| 1796 | { | |
| 1797 | var it = env_map.iterator(); | |
| 1798 | var i: usize = 0; | |
| 1799 | while (it.next()) |pair| : (i += 1) { | |
| 1800 | const env_buf = try arena.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.len + 1, 0); | |
| 1801 | @memcpy(env_buf[0..pair.key_ptr.len], pair.key_ptr.*); | |
| 1802 | env_buf[pair.key_ptr.len] = '='; | |
| 1803 | @memcpy(env_buf[pair.key_ptr.len + 1 ..][0..pair.value_ptr.len], pair.value_ptr.*); | |
| 1804 | envp_buf[i] = env_buf.ptr; | |
| 1805 | } | |
| 1806 | assert(i == envp_count); | |
| 1807 | } | |
| 1808 | return envp_buf; | |
| 1809 | } | |
| 1810 | ||
| 1811 | test createNullDelimitedEnvMap { | |
| 1812 | const allocator = testing.allocator; | |
| 1813 | var envmap = EnvMap.init(allocator); | |
| 1814 | defer envmap.deinit(); | |
| 1815 | ||
| 1816 | try envmap.put("HOME", "/home/ifreund"); | |
| 1817 | try envmap.put("WAYLAND_DISPLAY", "wayland-1"); | |
| 1818 | try envmap.put("DISPLAY", ":1"); | |
| 1819 | try envmap.put("DEBUGINFOD_URLS", " "); | |
| 1820 | try envmap.put("XCURSOR_SIZE", "24"); | |
| 1821 | ||
| 1822 | var arena = std.heap.ArenaAllocator.init(allocator); | |
| 1823 | defer arena.deinit(); | |
| 1824 | const environ = try createNullDelimitedEnvMap(arena.allocator(), &envmap); | |
| 1825 | ||
| 1826 | try testing.expectEqual(@as(usize, 5), environ.len); | |
| 1827 | ||
| 1828 | inline for (.{ | |
| 1829 | "HOME=/home/ifreund", | |
| 1830 | "WAYLAND_DISPLAY=wayland-1", | |
| 1831 | "DISPLAY=:1", | |
| 1832 | "DEBUGINFOD_URLS= ", | |
| 1833 | "XCURSOR_SIZE=24", | |
| 1834 | }) |target| { | |
| 1835 | for (environ) |variable| { | |
| 1836 | if (mem.eql(u8, mem.span(variable orelse continue), target)) break; | |
| 1837 | } else { | |
| 1838 | try testing.expect(false); // Environment variable not found | |
| 1839 | } | |
| 1840 | } | |
| 1841 | } | |
| 1842 | ||
| 1843 | /// Caller must free result. | |
| 1844 | pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 { | |
| 1845 | // count bytes needed | |
| 1846 | const max_chars_needed = x: { | |
| 1847 | var max_chars_needed: usize = 4; // 4 for the final 4 null bytes | |
| 1848 | var it = env_map.iterator(); | |
| 1849 | while (it.next()) |pair| { | |
| 1850 | // +1 for '=' | |
| 1851 | // +1 for null byte | |
| 1852 | max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2; | |
| 1853 | } | |
| 1854 | break :x max_chars_needed; | |
| 1855 | }; | |
| 1856 | const result = try allocator.alloc(u16, max_chars_needed); | |
| 1857 | errdefer allocator.free(result); | |
| 1858 | ||
| 1859 | var it = env_map.iterator(); | |
| 1860 | var i: usize = 0; | |
| 1861 | while (it.next()) |pair| { | |
| 1862 | i += try unicode.wtf8ToWtf16Le(result[i..], pair.key_ptr.*); | |
| 1863 | result[i] = '='; | |
| 1864 | i += 1; | |
| 1865 | i += try unicode.wtf8ToWtf16Le(result[i..], pair.value_ptr.*); | |
| 1866 | result[i] = 0; | |
| 1867 | i += 1; | |
| 1868 | } | |
| 1869 | result[i] = 0; | |
| 1870 | i += 1; | |
| 1871 | result[i] = 0; | |
| 1872 | i += 1; | |
| 1873 | result[i] = 0; | |
| 1874 | i += 1; | |
| 1875 | result[i] = 0; | |
| 1876 | i += 1; | |
| 1877 | return try allocator.realloc(result, i); | |
| 1878 | } |
lib/std/process/Child.zig created+1810| ... | ... | @@ -0,0 +1,1810 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const unicode = std.unicode; | |
| 4 | const fs = std.fs; | |
| 5 | const process = std.process; | |
| 6 | const File = std.fs.File; | |
| 7 | const windows = std.os.windows; | |
| 8 | const linux = std.os.linux; | |
| 9 | const posix = std.posix; | |
| 10 | const mem = std.mem; | |
| 11 | const EnvMap = std.process.EnvMap; | |
| 12 | const maxInt = std.math.maxInt; | |
| 13 | const assert = std.debug.assert; | |
| 14 | const native_os = builtin.os.tag; | |
| 15 | const ChildProcess = @This(); | |
| 16 | ||
| 17 | pub const Id = switch (native_os) { | |
| 18 | .windows => windows.HANDLE, | |
| 19 | .wasi => void, | |
| 20 | else => posix.pid_t, | |
| 21 | }; | |
| 22 | ||
| 23 | /// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`. | |
| 24 | /// On Windows this is the hProcess. | |
| 25 | /// On POSIX this is the pid. | |
| 26 | id: Id, | |
| 27 | thread_handle: if (native_os == .windows) windows.HANDLE else void, | |
| 28 | ||
| 29 | allocator: mem.Allocator, | |
| 30 | ||
| 31 | /// The writing end of the child process's standard input pipe. | |
| 32 | /// Usage requires `stdin_behavior == StdIo.Pipe`. | |
| 33 | /// Available after calling `spawn()`. | |
| 34 | stdin: ?File, | |
| 35 | ||
| 36 | /// The reading end of the child process's standard output pipe. | |
| 37 | /// Usage requires `stdout_behavior == StdIo.Pipe`. | |
| 38 | /// Available after calling `spawn()`. | |
| 39 | stdout: ?File, | |
| 40 | ||
| 41 | /// The reading end of the child process's standard error pipe. | |
| 42 | /// Usage requires `stderr_behavior == StdIo.Pipe`. | |
| 43 | /// Available after calling `spawn()`. | |
| 44 | stderr: ?File, | |
| 45 | ||
| 46 | /// Terminated state of the child process. | |
| 47 | /// Available after calling `wait()`. | |
| 48 | term: ?(SpawnError!Term), | |
| 49 | ||
| 50 | argv: []const []const u8, | |
| 51 | ||
| 52 | /// Leave as null to use the current env map using the supplied allocator. | |
| 53 | env_map: ?*const EnvMap, | |
| 54 | ||
| 55 | stdin_behavior: StdIo, | |
| 56 | stdout_behavior: StdIo, | |
| 57 | stderr_behavior: StdIo, | |
| 58 | ||
| 59 | /// Set to change the user id when spawning the child process. | |
| 60 | uid: if (native_os == .windows or native_os == .wasi) void else ?posix.uid_t, | |
| 61 | ||
| 62 | /// Set to change the group id when spawning the child process. | |
| 63 | gid: if (native_os == .windows or native_os == .wasi) void else ?posix.gid_t, | |
| 64 | ||
| 65 | /// Set to change the current working directory when spawning the child process. | |
| 66 | cwd: ?[]const u8, | |
| 67 | /// Set to change the current working directory when spawning the child process. | |
| 68 | /// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190 | |
| 69 | /// Once that is done, `cwd` will be deprecated in favor of this field. | |
| 70 | cwd_dir: ?fs.Dir = null, | |
| 71 | ||
| 72 | err_pipe: ?if (native_os == .windows) void else [2]posix.fd_t, | |
| 73 | ||
| 74 | expand_arg0: Arg0Expand, | |
| 75 | ||
| 76 | /// Darwin-only. Disable ASLR for the child process. | |
| 77 | disable_aslr: bool = false, | |
| 78 | ||
| 79 | /// Darwin-only. Start child process in suspended state as if SIGSTOP was sent. | |
| 80 | start_suspended: bool = false, | |
| 81 | ||
| 82 | /// Set to true to obtain rusage information for the child process. | |
| 83 | /// Depending on the target platform and implementation status, the | |
| 84 | /// requested statistics may or may not be available. If they are | |
| 85 | /// available, then the `resource_usage_statistics` field will be populated | |
| 86 | /// after calling `wait`. | |
| 87 | /// On Linux and Darwin, this obtains rusage statistics from wait4(). | |
| 88 | request_resource_usage_statistics: bool = false, | |
| 89 | ||
| 90 | /// This is available after calling wait if | |
| 91 | /// `request_resource_usage_statistics` was set to `true` before calling | |
| 92 | /// `spawn`. | |
| 93 | resource_usage_statistics: ResourceUsageStatistics = .{}, | |
| 94 | ||
| 95 | pub const ResourceUsageStatistics = struct { | |
| 96 | rusage: @TypeOf(rusage_init) = rusage_init, | |
| 97 | ||
| 98 | /// Returns the peak resident set size of the child process, in bytes, | |
| 99 | /// if available. | |
| 100 | pub inline fn getMaxRss(rus: ResourceUsageStatistics) ?usize { | |
| 101 | switch (native_os) { | |
| 102 | .linux => { | |
| 103 | if (rus.rusage) |ru| { | |
| 104 | return @as(usize, @intCast(ru.maxrss)) * 1024; | |
| 105 | } else { | |
| 106 | return null; | |
| 107 | } | |
| 108 | }, | |
| 109 | .windows => { | |
| 110 | if (rus.rusage) |ru| { | |
| 111 | return ru.PeakWorkingSetSize; | |
| 112 | } else { | |
| 113 | return null; | |
| 114 | } | |
| 115 | }, | |
| 116 | .macos, .ios => { | |
| 117 | if (rus.rusage) |ru| { | |
| 118 | // Darwin oddly reports in bytes instead of kilobytes. | |
| 119 | return @as(usize, @intCast(ru.maxrss)); | |
| 120 | } else { | |
| 121 | return null; | |
| 122 | } | |
| 123 | }, | |
| 124 | else => return null, | |
| 125 | } | |
| 126 | } | |
| 127 | ||
| 128 | const rusage_init = switch (native_os) { | |
| 129 | .linux, .macos, .ios => @as(?posix.rusage, null), | |
| 130 | .windows => @as(?windows.VM_COUNTERS, null), | |
| 131 | else => {}, | |
| 132 | }; | |
| 133 | }; | |
| 134 | ||
| 135 | pub const Arg0Expand = posix.Arg0Expand; | |
| 136 | ||
| 137 | pub const SpawnError = error{ | |
| 138 | OutOfMemory, | |
| 139 | ||
| 140 | /// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV. | |
| 141 | NoDevice, | |
| 142 | ||
| 143 | /// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8. | |
| 144 | /// https://simonsapin.github.io/wtf-8/ | |
| 145 | InvalidWtf8, | |
| 146 | ||
| 147 | /// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process. | |
| 148 | CurrentWorkingDirectoryUnlinked, | |
| 149 | ||
| 150 | /// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | |
| 151 | /// within arguments when executing a `.bat`/`.cmd` script. | |
| 152 | /// - NUL/LF signifiies end of arguments, so anything afterwards | |
| 153 | /// would be lost after execution. | |
| 154 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | |
| 155 | /// would be lost after execution. | |
| 156 | InvalidBatchScriptArg, | |
| 157 | } || | |
| 158 | posix.ExecveError || | |
| 159 | posix.SetIdError || | |
| 160 | posix.ChangeCurDirError || | |
| 161 | windows.CreateProcessError || | |
| 162 | windows.GetProcessMemoryInfoError || | |
| 163 | windows.WaitForSingleObjectError; | |
| 164 | ||
| 165 | pub const Term = union(enum) { | |
| 166 | Exited: u8, | |
| 167 | Signal: u32, | |
| 168 | Stopped: u32, | |
| 169 | Unknown: u32, | |
| 170 | }; | |
| 171 | ||
| 172 | /// Behavior of the child process's standard input, output, and error | |
| 173 | /// streams. | |
| 174 | pub const StdIo = enum { | |
| 175 | /// Inherit the stream from the parent process. | |
| 176 | Inherit, | |
| 177 | ||
| 178 | /// Pass a null stream to the child process. | |
| 179 | /// This is /dev/null on POSIX and NUL on Windows. | |
| 180 | Ignore, | |
| 181 | ||
| 182 | /// Create a pipe for the stream. | |
| 183 | /// The corresponding field (`stdout`, `stderr`, or `stdin`) | |
| 184 | /// will be assigned a `File` object that can be used | |
| 185 | /// to read from or write to the pipe. | |
| 186 | Pipe, | |
| 187 | ||
| 188 | /// Close the stream after the child process spawns. | |
| 189 | Close, | |
| 190 | }; | |
| 191 | ||
| 192 | /// First argument in argv is the executable. | |
| 193 | pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess { | |
| 194 | return .{ | |
| 195 | .allocator = allocator, | |
| 196 | .argv = argv, | |
| 197 | .id = undefined, | |
| 198 | .thread_handle = undefined, | |
| 199 | .err_pipe = null, | |
| 200 | .term = null, | |
| 201 | .env_map = null, | |
| 202 | .cwd = null, | |
| 203 | .uid = if (native_os == .windows or native_os == .wasi) {} else null, | |
| 204 | .gid = if (native_os == .windows or native_os == .wasi) {} else null, | |
| 205 | .stdin = null, | |
| 206 | .stdout = null, | |
| 207 | .stderr = null, | |
| 208 | .stdin_behavior = StdIo.Inherit, | |
| 209 | .stdout_behavior = StdIo.Inherit, | |
| 210 | .stderr_behavior = StdIo.Inherit, | |
| 211 | .expand_arg0 = .no_expand, | |
| 212 | }; | |
| 213 | } | |
| 214 | ||
| 215 | pub fn setUserName(self: *ChildProcess, name: []const u8) !void { | |
| 216 | const user_info = try process.getUserInfo(name); | |
| 217 | self.uid = user_info.uid; | |
| 218 | self.gid = user_info.gid; | |
| 219 | } | |
| 220 | ||
| 221 | /// On success must call `kill` or `wait`. | |
| 222 | /// After spawning the `id` is available. | |
| 223 | pub fn spawn(self: *ChildProcess) SpawnError!void { | |
| 224 | if (!process.can_spawn) { | |
| 225 | @compileError("the target operating system cannot spawn processes"); | |
| 226 | } | |
| 227 | ||
| 228 | if (native_os == .windows) { | |
| 229 | return self.spawnWindows(); | |
| 230 | } else { | |
| 231 | return self.spawnPosix(); | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | pub fn spawnAndWait(self: *ChildProcess) SpawnError!Term { | |
| 236 | try self.spawn(); | |
| 237 | return self.wait(); | |
| 238 | } | |
| 239 | ||
| 240 | /// Forcibly terminates child process and then cleans up all resources. | |
| 241 | pub fn kill(self: *ChildProcess) !Term { | |
| 242 | if (native_os == .windows) { | |
| 243 | return self.killWindows(1); | |
| 244 | } else { | |
| 245 | return self.killPosix(); | |
| 246 | } | |
| 247 | } | |
| 248 | ||
| 249 | pub fn killWindows(self: *ChildProcess, exit_code: windows.UINT) !Term { | |
| 250 | if (self.term) |term| { | |
| 251 | self.cleanupStreams(); | |
| 252 | return term; | |
| 253 | } | |
| 254 | ||
| 255 | windows.TerminateProcess(self.id, exit_code) catch |err| switch (err) { | |
| 256 | error.PermissionDenied => { | |
| 257 | // Usually when TerminateProcess triggers a ACCESS_DENIED error, it | |
| 258 | // indicates that the process has already exited, but there may be | |
| 259 | // some rare edge cases where our process handle no longer has the | |
| 260 | // PROCESS_TERMINATE access right, so let's do another check to make | |
| 261 | // sure the process is really no longer running: | |
| 262 | windows.WaitForSingleObjectEx(self.id, 0, false) catch return err; | |
| 263 | return error.AlreadyTerminated; | |
| 264 | }, | |
| 265 | else => return err, | |
| 266 | }; | |
| 267 | try self.waitUnwrappedWindows(); | |
| 268 | return self.term.?; | |
| 269 | } | |
| 270 | ||
| 271 | pub fn killPosix(self: *ChildProcess) !Term { | |
| 272 | if (self.term) |term| { | |
| 273 | self.cleanupStreams(); | |
| 274 | return term; | |
| 275 | } | |
| 276 | posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { | |
| 277 | error.ProcessNotFound => return error.AlreadyTerminated, | |
| 278 | else => return err, | |
| 279 | }; | |
| 280 | try self.waitUnwrapped(); | |
| 281 | return self.term.?; | |
| 282 | } | |
| 283 | ||
| 284 | /// Blocks until child process terminates and then cleans up all resources. | |
| 285 | pub fn wait(self: *ChildProcess) !Term { | |
| 286 | const term = if (native_os == .windows) | |
| 287 | try self.waitWindows() | |
| 288 | else | |
| 289 | try self.waitPosix(); | |
| 290 | ||
| 291 | self.id = undefined; | |
| 292 | ||
| 293 | return term; | |
| 294 | } | |
| 295 | ||
| 296 | pub const RunResult = struct { | |
| 297 | term: Term, | |
| 298 | stdout: []u8, | |
| 299 | stderr: []u8, | |
| 300 | }; | |
| 301 | ||
| 302 | fn fifoToOwnedArrayList(fifo: *std.io.PollFifo) std.ArrayList(u8) { | |
| 303 | if (fifo.head > 0) { | |
| 304 | @memcpy(fifo.buf[0..fifo.count], fifo.buf[fifo.head..][0..fifo.count]); | |
| 305 | } | |
| 306 | const result = std.ArrayList(u8){ | |
| 307 | .items = fifo.buf[0..fifo.count], | |
| 308 | .capacity = fifo.buf.len, | |
| 309 | .allocator = fifo.allocator, | |
| 310 | }; | |
| 311 | fifo.* = std.io.PollFifo.init(fifo.allocator); | |
| 312 | return result; | |
| 313 | } | |
| 314 | ||
| 315 | /// Collect the output from the process's stdout and stderr. Will return once all output | |
| 316 | /// has been collected. This does not mean that the process has ended. `wait` should still | |
| 317 | /// be called to wait for and clean up the process. | |
| 318 | /// | |
| 319 | /// The process must be started with stdout_behavior and stderr_behavior == .Pipe | |
| 320 | pub fn collectOutput( | |
| 321 | child: ChildProcess, | |
| 322 | stdout: *std.ArrayList(u8), | |
| 323 | stderr: *std.ArrayList(u8), | |
| 324 | max_output_bytes: usize, | |
| 325 | ) !void { | |
| 326 | assert(child.stdout_behavior == .Pipe); | |
| 327 | assert(child.stderr_behavior == .Pipe); | |
| 328 | ||
| 329 | // we could make this work with multiple allocators but YAGNI | |
| 330 | if (stdout.allocator.ptr != stderr.allocator.ptr or | |
| 331 | stdout.allocator.vtable != stderr.allocator.vtable) | |
| 332 | { | |
| 333 | unreachable; // ChildProcess.collectOutput only supports 1 allocator | |
| 334 | } | |
| 335 | ||
| 336 | var poller = std.io.poll(stdout.allocator, enum { stdout, stderr }, .{ | |
| 337 | .stdout = child.stdout.?, | |
| 338 | .stderr = child.stderr.?, | |
| 339 | }); | |
| 340 | defer poller.deinit(); | |
| 341 | ||
| 342 | while (try poller.poll()) { | |
| 343 | if (poller.fifo(.stdout).count > max_output_bytes) | |
| 344 | return error.StdoutStreamTooLong; | |
| 345 | if (poller.fifo(.stderr).count > max_output_bytes) | |
| 346 | return error.StderrStreamTooLong; | |
| 347 | } | |
| 348 | ||
| 349 | stdout.* = fifoToOwnedArrayList(poller.fifo(.stdout)); | |
| 350 | stderr.* = fifoToOwnedArrayList(poller.fifo(.stderr)); | |
| 351 | } | |
| 352 | ||
| 353 | pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{ | |
| 354 | StdoutStreamTooLong, | |
| 355 | StderrStreamTooLong, | |
| 356 | }; | |
| 357 | ||
| 358 | /// Spawns a child process, waits for it, collecting stdout and stderr, and then returns. | |
| 359 | /// If it succeeds, the caller owns result.stdout and result.stderr memory. | |
| 360 | pub fn run(args: struct { | |
| 361 | allocator: mem.Allocator, | |
| 362 | argv: []const []const u8, | |
| 363 | cwd: ?[]const u8 = null, | |
| 364 | cwd_dir: ?fs.Dir = null, | |
| 365 | env_map: ?*const EnvMap = null, | |
| 366 | max_output_bytes: usize = 50 * 1024, | |
| 367 | expand_arg0: Arg0Expand = .no_expand, | |
| 368 | }) RunError!RunResult { | |
| 369 | var child = ChildProcess.init(args.argv, args.allocator); | |
| 370 | child.stdin_behavior = .Ignore; | |
| 371 | child.stdout_behavior = .Pipe; | |
| 372 | child.stderr_behavior = .Pipe; | |
| 373 | child.cwd = args.cwd; | |
| 374 | child.cwd_dir = args.cwd_dir; | |
| 375 | child.env_map = args.env_map; | |
| 376 | child.expand_arg0 = args.expand_arg0; | |
| 377 | ||
| 378 | var stdout = std.ArrayList(u8).init(args.allocator); | |
| 379 | var stderr = std.ArrayList(u8).init(args.allocator); | |
| 380 | errdefer { | |
| 381 | stdout.deinit(); | |
| 382 | stderr.deinit(); | |
| 383 | } | |
| 384 | ||
| 385 | try child.spawn(); | |
| 386 | try child.collectOutput(&stdout, &stderr, args.max_output_bytes); | |
| 387 | ||
| 388 | return RunResult{ | |
| 389 | .term = try child.wait(), | |
| 390 | .stdout = try stdout.toOwnedSlice(), | |
| 391 | .stderr = try stderr.toOwnedSlice(), | |
| 392 | }; | |
| 393 | } | |
| 394 | ||
| 395 | fn waitWindows(self: *ChildProcess) !Term { | |
| 396 | if (self.term) |term| { | |
| 397 | self.cleanupStreams(); | |
| 398 | return term; | |
| 399 | } | |
| 400 | ||
| 401 | try self.waitUnwrappedWindows(); | |
| 402 | return self.term.?; | |
| 403 | } | |
| 404 | ||
| 405 | fn waitPosix(self: *ChildProcess) !Term { | |
| 406 | if (self.term) |term| { | |
| 407 | self.cleanupStreams(); | |
| 408 | return term; | |
| 409 | } | |
| 410 | ||
| 411 | try self.waitUnwrapped(); | |
| 412 | return self.term.?; | |
| 413 | } | |
| 414 | ||
| 415 | fn waitUnwrappedWindows(self: *ChildProcess) !void { | |
| 416 | const result = windows.WaitForSingleObjectEx(self.id, windows.INFINITE, false); | |
| 417 | ||
| 418 | self.term = @as(SpawnError!Term, x: { | |
| 419 | var exit_code: windows.DWORD = undefined; | |
| 420 | if (windows.kernel32.GetExitCodeProcess(self.id, &exit_code) == 0) { | |
| 421 | break :x Term{ .Unknown = 0 }; | |
| 422 | } else { | |
| 423 | break :x Term{ .Exited = @as(u8, @truncate(exit_code)) }; | |
| 424 | } | |
| 425 | }); | |
| 426 | ||
| 427 | if (self.request_resource_usage_statistics) { | |
| 428 | self.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(self.id); | |
| 429 | } | |
| 430 | ||
| 431 | posix.close(self.id); | |
| 432 | posix.close(self.thread_handle); | |
| 433 | self.cleanupStreams(); | |
| 434 | return result; | |
| 435 | } | |
| 436 | ||
| 437 | fn waitUnwrapped(self: *ChildProcess) !void { | |
| 438 | const res: posix.WaitPidResult = res: { | |
| 439 | if (self.request_resource_usage_statistics) { | |
| 440 | switch (native_os) { | |
| 441 | .linux, .macos, .ios => { | |
| 442 | var ru: posix.rusage = undefined; | |
| 443 | const res = posix.wait4(self.id, 0, &ru); | |
| 444 | self.resource_usage_statistics.rusage = ru; | |
| 445 | break :res res; | |
| 446 | }, | |
| 447 | else => {}, | |
| 448 | } | |
| 449 | } | |
| 450 | ||
| 451 | break :res posix.waitpid(self.id, 0); | |
| 452 | }; | |
| 453 | const status = res.status; | |
| 454 | self.cleanupStreams(); | |
| 455 | self.handleWaitResult(status); | |
| 456 | } | |
| 457 | ||
| 458 | fn handleWaitResult(self: *ChildProcess, status: u32) void { | |
| 459 | self.term = self.cleanupAfterWait(status); | |
| 460 | } | |
| 461 | ||
| 462 | fn cleanupStreams(self: *ChildProcess) void { | |
| 463 | if (self.stdin) |*stdin| { | |
| 464 | stdin.close(); | |
| 465 | self.stdin = null; | |
| 466 | } | |
| 467 | if (self.stdout) |*stdout| { | |
| 468 | stdout.close(); | |
| 469 | self.stdout = null; | |
| 470 | } | |
| 471 | if (self.stderr) |*stderr| { | |
| 472 | stderr.close(); | |
| 473 | self.stderr = null; | |
| 474 | } | |
| 475 | } | |
| 476 | ||
| 477 | fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term { | |
| 478 | if (self.err_pipe) |err_pipe| { | |
| 479 | defer destroyPipe(err_pipe); | |
| 480 | ||
| 481 | if (native_os == .linux) { | |
| 482 | var fd = [1]posix.pollfd{posix.pollfd{ | |
| 483 | .fd = err_pipe[0], | |
| 484 | .events = posix.POLL.IN, | |
| 485 | .revents = undefined, | |
| 486 | }}; | |
| 487 | ||
| 488 | // Check if the eventfd buffer stores a non-zero value by polling | |
| 489 | // it, that's the error code returned by the child process. | |
| 490 | _ = posix.poll(&fd, 0) catch unreachable; | |
| 491 | ||
| 492 | // According to eventfd(2) the descriptor is readable if the counter | |
| 493 | // has a value greater than 0 | |
| 494 | if ((fd[0].revents & posix.POLL.IN) != 0) { | |
| 495 | const err_int = try readIntFd(err_pipe[0]); | |
| 496 | return @as(SpawnError, @errorCast(@errorFromInt(err_int))); | |
| 497 | } | |
| 498 | } else { | |
| 499 | // Write maxInt(ErrInt) to the write end of the err_pipe. This is after | |
| 500 | // waitpid, so this write is guaranteed to be after the child | |
| 501 | // pid potentially wrote an error. This way we can do a blocking | |
| 502 | // read on the error pipe and either get maxInt(ErrInt) (no error) or | |
| 503 | // an error code. | |
| 504 | try writeIntFd(err_pipe[1], maxInt(ErrInt)); | |
| 505 | const err_int = try readIntFd(err_pipe[0]); | |
| 506 | // Here we potentially return the fork child's error from the parent | |
| 507 | // pid. | |
| 508 | if (err_int != maxInt(ErrInt)) { | |
| 509 | return @as(SpawnError, @errorCast(@errorFromInt(err_int))); | |
| 510 | } | |
| 511 | } | |
| 512 | } | |
| 513 | ||
| 514 | return statusToTerm(status); | |
| 515 | } | |
| 516 | ||
| 517 | fn statusToTerm(status: u32) Term { | |
| 518 | return if (posix.W.IFEXITED(status)) | |
| 519 | Term{ .Exited = posix.W.EXITSTATUS(status) } | |
| 520 | else if (posix.W.IFSIGNALED(status)) | |
| 521 | Term{ .Signal = posix.W.TERMSIG(status) } | |
| 522 | else if (posix.W.IFSTOPPED(status)) | |
| 523 | Term{ .Stopped = posix.W.STOPSIG(status) } | |
| 524 | else | |
| 525 | Term{ .Unknown = status }; | |
| 526 | } | |
| 527 | ||
| 528 | fn spawnPosix(self: *ChildProcess) SpawnError!void { | |
| 529 | const pipe_flags: posix.O = .{}; | |
| 530 | const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try posix.pipe2(pipe_flags) else undefined; | |
| 531 | errdefer if (self.stdin_behavior == StdIo.Pipe) { | |
| 532 | destroyPipe(stdin_pipe); | |
| 533 | }; | |
| 534 | ||
| 535 | const stdout_pipe = if (self.stdout_behavior == StdIo.Pipe) try posix.pipe2(pipe_flags) else undefined; | |
| 536 | errdefer if (self.stdout_behavior == StdIo.Pipe) { | |
| 537 | destroyPipe(stdout_pipe); | |
| 538 | }; | |
| 539 | ||
| 540 | const stderr_pipe = if (self.stderr_behavior == StdIo.Pipe) try posix.pipe2(pipe_flags) else undefined; | |
| 541 | errdefer if (self.stderr_behavior == StdIo.Pipe) { | |
| 542 | destroyPipe(stderr_pipe); | |
| 543 | }; | |
| 544 | ||
| 545 | const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); | |
| 546 | const dev_null_fd = if (any_ignore) | |
| 547 | posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) { | |
| 548 | error.PathAlreadyExists => unreachable, | |
| 549 | error.NoSpaceLeft => unreachable, | |
| 550 | error.FileTooBig => unreachable, | |
| 551 | error.DeviceBusy => unreachable, | |
| 552 | error.FileLocksNotSupported => unreachable, | |
| 553 | error.BadPathName => unreachable, // Windows-only | |
| 554 | error.WouldBlock => unreachable, | |
| 555 | error.NetworkNotFound => unreachable, // Windows-only | |
| 556 | else => |e| return e, | |
| 557 | } | |
| 558 | else | |
| 559 | undefined; | |
| 560 | defer { | |
| 561 | if (any_ignore) posix.close(dev_null_fd); | |
| 562 | } | |
| 563 | ||
| 564 | var arena_allocator = std.heap.ArenaAllocator.init(self.allocator); | |
| 565 | defer arena_allocator.deinit(); | |
| 566 | const arena = arena_allocator.allocator(); | |
| 567 | ||
| 568 | // The POSIX standard does not allow malloc() between fork() and execve(), | |
| 569 | // and `self.allocator` may be a libc allocator. | |
| 570 | // I have personally observed the child process deadlocking when it tries | |
| 571 | // to call malloc() due to a heap allocation between fork() and execve(), | |
| 572 | // in musl v1.1.24. | |
| 573 | // Additionally, we want to reduce the number of possible ways things | |
| 574 | // can fail between fork() and execve(). | |
| 575 | // Therefore, we do all the allocation for the execve() before the fork(). | |
| 576 | // This means we must do the null-termination of argv and env vars here. | |
| 577 | const argv_buf = try arena.allocSentinel(?[*:0]const u8, self.argv.len, null); | |
| 578 | for (self.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeZ(u8, arg)).ptr; | |
| 579 | ||
| 580 | const envp = m: { | |
| 581 | if (self.env_map) |env_map| { | |
| 582 | const envp_buf = try process.createNullDelimitedEnvMap(arena, env_map); | |
| 583 | break :m envp_buf.ptr; | |
| 584 | } else if (builtin.link_libc) { | |
| 585 | break :m std.c.environ; | |
| 586 | } else if (builtin.output_mode == .Exe) { | |
| 587 | // Then we have Zig start code and this works. | |
| 588 | // TODO type-safety for null-termination of `os.environ`. | |
| 589 | break :m @as([*:null]const ?[*:0]const u8, @ptrCast(std.os.environ.ptr)); | |
| 590 | } else { | |
| 591 | // TODO come up with a solution for this. | |
| 592 | @compileError("missing std lib enhancement: ChildProcess implementation has no way to collect the environment variables to forward to the child process"); | |
| 593 | } | |
| 594 | }; | |
| 595 | ||
| 596 | // This pipe is used to communicate errors between the time of fork | |
| 597 | // and execve from the child process to the parent process. | |
| 598 | const err_pipe = blk: { | |
| 599 | if (native_os == .linux) { | |
| 600 | const fd = try posix.eventfd(0, linux.EFD.CLOEXEC); | |
| 601 | // There's no distinction between the readable and the writeable | |
| 602 | // end with eventfd | |
| 603 | break :blk [2]posix.fd_t{ fd, fd }; | |
| 604 | } else { | |
| 605 | break :blk try posix.pipe2(.{ .CLOEXEC = true }); | |
| 606 | } | |
| 607 | }; | |
| 608 | errdefer destroyPipe(err_pipe); | |
| 609 | ||
| 610 | const pid_result = try posix.fork(); | |
| 611 | if (pid_result == 0) { | |
| 612 | // we are the child | |
| 613 | setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 614 | setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 615 | setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 616 | ||
| 617 | if (self.stdin_behavior == .Pipe) { | |
| 618 | posix.close(stdin_pipe[0]); | |
| 619 | posix.close(stdin_pipe[1]); | |
| 620 | } | |
| 621 | if (self.stdout_behavior == .Pipe) { | |
| 622 | posix.close(stdout_pipe[0]); | |
| 623 | posix.close(stdout_pipe[1]); | |
| 624 | } | |
| 625 | if (self.stderr_behavior == .Pipe) { | |
| 626 | posix.close(stderr_pipe[0]); | |
| 627 | posix.close(stderr_pipe[1]); | |
| 628 | } | |
| 629 | ||
| 630 | if (self.cwd_dir) |cwd| { | |
| 631 | posix.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 632 | } else if (self.cwd) |cwd| { | |
| 633 | posix.chdir(cwd) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 634 | } | |
| 635 | ||
| 636 | if (self.gid) |gid| { | |
| 637 | posix.setregid(gid, gid) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 638 | } | |
| 639 | ||
| 640 | if (self.uid) |uid| { | |
| 641 | posix.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err); | |
| 642 | } | |
| 643 | ||
| 644 | const err = switch (self.expand_arg0) { | |
| 645 | .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), | |
| 646 | .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), | |
| 647 | }; | |
| 648 | forkChildErrReport(err_pipe[1], err); | |
| 649 | } | |
| 650 | ||
| 651 | // we are the parent | |
| 652 | const pid: i32 = @intCast(pid_result); | |
| 653 | if (self.stdin_behavior == StdIo.Pipe) { | |
| 654 | self.stdin = File{ .handle = stdin_pipe[1] }; | |
| 655 | } else { | |
| 656 | self.stdin = null; | |
| 657 | } | |
| 658 | if (self.stdout_behavior == StdIo.Pipe) { | |
| 659 | self.stdout = File{ .handle = stdout_pipe[0] }; | |
| 660 | } else { | |
| 661 | self.stdout = null; | |
| 662 | } | |
| 663 | if (self.stderr_behavior == StdIo.Pipe) { | |
| 664 | self.stderr = File{ .handle = stderr_pipe[0] }; | |
| 665 | } else { | |
| 666 | self.stderr = null; | |
| 667 | } | |
| 668 | ||
| 669 | self.id = pid; | |
| 670 | self.err_pipe = err_pipe; | |
| 671 | self.term = null; | |
| 672 | ||
| 673 | if (self.stdin_behavior == StdIo.Pipe) { | |
| 674 | posix.close(stdin_pipe[0]); | |
| 675 | } | |
| 676 | if (self.stdout_behavior == StdIo.Pipe) { | |
| 677 | posix.close(stdout_pipe[1]); | |
| 678 | } | |
| 679 | if (self.stderr_behavior == StdIo.Pipe) { | |
| 680 | posix.close(stderr_pipe[1]); | |
| 681 | } | |
| 682 | } | |
| 683 | ||
| 684 | fn spawnWindows(self: *ChildProcess) SpawnError!void { | |
| 685 | var saAttr = windows.SECURITY_ATTRIBUTES{ | |
| 686 | .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), | |
| 687 | .bInheritHandle = windows.TRUE, | |
| 688 | .lpSecurityDescriptor = null, | |
| 689 | }; | |
| 690 | ||
| 691 | const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); | |
| 692 | ||
| 693 | const nul_handle = if (any_ignore) | |
| 694 | // "\Device\Null" or "\??\NUL" | |
| 695 | windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{ | |
| 696 | .access_mask = windows.GENERIC_READ | windows.GENERIC_WRITE | windows.SYNCHRONIZE, | |
| 697 | .share_access = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, | |
| 698 | .sa = &saAttr, | |
| 699 | .creation = windows.OPEN_EXISTING, | |
| 700 | }) catch |err| switch (err) { | |
| 701 | error.PathAlreadyExists => return error.Unexpected, // not possible for "NUL" | |
| 702 | error.PipeBusy => return error.Unexpected, // not possible for "NUL" | |
| 703 | error.FileNotFound => return error.Unexpected, // not possible for "NUL" | |
| 704 | error.AccessDenied => return error.Unexpected, // not possible for "NUL" | |
| 705 | error.NameTooLong => return error.Unexpected, // not possible for "NUL" | |
| 706 | error.WouldBlock => return error.Unexpected, // not possible for "NUL" | |
| 707 | error.NetworkNotFound => return error.Unexpected, // not possible for "NUL" | |
| 708 | error.AntivirusInterference => return error.Unexpected, // not possible for "NUL" | |
| 709 | else => |e| return e, | |
| 710 | } | |
| 711 | else | |
| 712 | undefined; | |
| 713 | defer { | |
| 714 | if (any_ignore) posix.close(nul_handle); | |
| 715 | } | |
| 716 | ||
| 717 | var g_hChildStd_IN_Rd: ?windows.HANDLE = null; | |
| 718 | var g_hChildStd_IN_Wr: ?windows.HANDLE = null; | |
| 719 | switch (self.stdin_behavior) { | |
| 720 | StdIo.Pipe => { | |
| 721 | try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); | |
| 722 | }, | |
| 723 | StdIo.Ignore => { | |
| 724 | g_hChildStd_IN_Rd = nul_handle; | |
| 725 | }, | |
| 726 | StdIo.Inherit => { | |
| 727 | g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; | |
| 728 | }, | |
| 729 | StdIo.Close => { | |
| 730 | g_hChildStd_IN_Rd = null; | |
| 731 | }, | |
| 732 | } | |
| 733 | errdefer if (self.stdin_behavior == StdIo.Pipe) { | |
| 734 | windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); | |
| 735 | }; | |
| 736 | ||
| 737 | var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; | |
| 738 | var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; | |
| 739 | switch (self.stdout_behavior) { | |
| 740 | StdIo.Pipe => { | |
| 741 | try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); | |
| 742 | }, | |
| 743 | StdIo.Ignore => { | |
| 744 | g_hChildStd_OUT_Wr = nul_handle; | |
| 745 | }, | |
| 746 | StdIo.Inherit => { | |
| 747 | g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; | |
| 748 | }, | |
| 749 | StdIo.Close => { | |
| 750 | g_hChildStd_OUT_Wr = null; | |
| 751 | }, | |
| 752 | } | |
| 753 | errdefer if (self.stdout_behavior == StdIo.Pipe) { | |
| 754 | windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); | |
| 755 | }; | |
| 756 | ||
| 757 | var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; | |
| 758 | var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; | |
| 759 | switch (self.stderr_behavior) { | |
| 760 | StdIo.Pipe => { | |
| 761 | try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); | |
| 762 | }, | |
| 763 | StdIo.Ignore => { | |
| 764 | g_hChildStd_ERR_Wr = nul_handle; | |
| 765 | }, | |
| 766 | StdIo.Inherit => { | |
| 767 | g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; | |
| 768 | }, | |
| 769 | StdIo.Close => { | |
| 770 | g_hChildStd_ERR_Wr = null; | |
| 771 | }, | |
| 772 | } | |
| 773 | errdefer if (self.stderr_behavior == StdIo.Pipe) { | |
| 774 | windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); | |
| 775 | }; | |
| 776 | ||
| 777 | var siStartInfo = windows.STARTUPINFOW{ | |
| 778 | .cb = @sizeOf(windows.STARTUPINFOW), | |
| 779 | .hStdError = g_hChildStd_ERR_Wr, | |
| 780 | .hStdOutput = g_hChildStd_OUT_Wr, | |
| 781 | .hStdInput = g_hChildStd_IN_Rd, | |
| 782 | .dwFlags = windows.STARTF_USESTDHANDLES, | |
| 783 | ||
| 784 | .lpReserved = null, | |
| 785 | .lpDesktop = null, | |
| 786 | .lpTitle = null, | |
| 787 | .dwX = 0, | |
| 788 | .dwY = 0, | |
| 789 | .dwXSize = 0, | |
| 790 | .dwYSize = 0, | |
| 791 | .dwXCountChars = 0, | |
| 792 | .dwYCountChars = 0, | |
| 793 | .dwFillAttribute = 0, | |
| 794 | .wShowWindow = 0, | |
| 795 | .cbReserved2 = 0, | |
| 796 | .lpReserved2 = null, | |
| 797 | }; | |
| 798 | var piProcInfo: windows.PROCESS_INFORMATION = undefined; | |
| 799 | ||
| 800 | const cwd_w = if (self.cwd) |cwd| try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd) else null; | |
| 801 | defer if (cwd_w) |cwd| self.allocator.free(cwd); | |
| 802 | const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; | |
| 803 | ||
| 804 | const maybe_envp_buf = if (self.env_map) |env_map| try process.createWindowsEnvBlock(self.allocator, env_map) else null; | |
| 805 | defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf); | |
| 806 | const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; | |
| 807 | ||
| 808 | const app_name_wtf8 = self.argv[0]; | |
| 809 | const app_name_is_absolute = fs.path.isAbsolute(app_name_wtf8); | |
| 810 | ||
| 811 | // the cwd set in ChildProcess is in effect when choosing the executable path | |
| 812 | // to match posix semantics | |
| 813 | var cwd_path_w_needs_free = false; | |
| 814 | const cwd_path_w = x: { | |
| 815 | // If the app name is absolute, then we need to use its dirname as the cwd | |
| 816 | if (app_name_is_absolute) { | |
| 817 | cwd_path_w_needs_free = true; | |
| 818 | const dir = fs.path.dirname(app_name_wtf8).?; | |
| 819 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, dir); | |
| 820 | } else if (self.cwd) |cwd| { | |
| 821 | cwd_path_w_needs_free = true; | |
| 822 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd); | |
| 823 | } else { | |
| 824 | break :x &[_:0]u16{}; // empty for cwd | |
| 825 | } | |
| 826 | }; | |
| 827 | defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); | |
| 828 | ||
| 829 | // If the app name has more than just a filename, then we need to separate that | |
| 830 | // into the basename and dirname and use the dirname as an addition to the cwd | |
| 831 | // path. This is because NtQueryDirectoryFile cannot accept FileName params with | |
| 832 | // path separators. | |
| 833 | const app_basename_wtf8 = fs.path.basename(app_name_wtf8); | |
| 834 | // If the app name is absolute, then the cwd will already have the app's dirname in it, | |
| 835 | // so only populate app_dirname if app name is a relative path with > 0 path separators. | |
| 836 | const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_wtf8) else null; | |
| 837 | const app_dirname_w: ?[:0]u16 = x: { | |
| 838 | if (maybe_app_dirname_wtf8) |app_dirname_wtf8| { | |
| 839 | break :x try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_dirname_wtf8); | |
| 840 | } | |
| 841 | break :x null; | |
| 842 | }; | |
| 843 | defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); | |
| 844 | ||
| 845 | const app_name_w = try unicode.wtf8ToWtf16LeAllocZ(self.allocator, app_basename_wtf8); | |
| 846 | defer self.allocator.free(app_name_w); | |
| 847 | ||
| 848 | run: { | |
| 849 | const PATH: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; | |
| 850 | const PATHEXT: [:0]const u16 = process.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; | |
| 851 | ||
| 852 | // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules | |
| 853 | // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously | |
| 854 | // constructed arguments. | |
| 855 | // | |
| 856 | // We'll need to wait until we're actually trying to run the command to know for sure | |
| 857 | // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually | |
| 858 | // serializing the command line until we determine how it should be serialized. | |
| 859 | var cmd_line_cache = WindowsCommandLineCache.init(self.allocator, self.argv); | |
| 860 | defer cmd_line_cache.deinit(); | |
| 861 | ||
| 862 | var app_buf = std.ArrayListUnmanaged(u16){}; | |
| 863 | defer app_buf.deinit(self.allocator); | |
| 864 | ||
| 865 | try app_buf.appendSlice(self.allocator, app_name_w); | |
| 866 | ||
| 867 | var dir_buf = std.ArrayListUnmanaged(u16){}; | |
| 868 | defer dir_buf.deinit(self.allocator); | |
| 869 | ||
| 870 | if (cwd_path_w.len > 0) { | |
| 871 | try dir_buf.appendSlice(self.allocator, cwd_path_w); | |
| 872 | } | |
| 873 | if (app_dirname_w) |app_dir| { | |
| 874 | if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); | |
| 875 | try dir_buf.appendSlice(self.allocator, app_dir); | |
| 876 | } | |
| 877 | if (dir_buf.items.len > 0) { | |
| 878 | // Need to normalize the path, openDirW can't handle things like double backslashes | |
| 879 | const normalized_len = windows.normalizePath(u16, dir_buf.items) catch return error.BadPathName; | |
| 880 | dir_buf.shrinkRetainingCapacity(normalized_len); | |
| 881 | } | |
| 882 | ||
| 883 | windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| { | |
| 884 | const original_err = switch (no_path_err) { | |
| 885 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | |
| 886 | error.InvalidArg0 => return error.FileNotFound, | |
| 887 | error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, | |
| 888 | error.UnrecoverableInvalidExe => return error.InvalidExe, | |
| 889 | else => |e| return e, | |
| 890 | }; | |
| 891 | ||
| 892 | // If the app name had path separators, that disallows PATH searching, | |
| 893 | // and there's no need to search the PATH if the app name is absolute. | |
| 894 | // We still search the path if the cwd is absolute because of the | |
| 895 | // "cwd set in ChildProcess is in effect when choosing the executable path | |
| 896 | // to match posix semantics" behavior--we don't want to skip searching | |
| 897 | // the PATH just because we were trying to set the cwd of the child process. | |
| 898 | if (app_dirname_w != null or app_name_is_absolute) { | |
| 899 | return original_err; | |
| 900 | } | |
| 901 | ||
| 902 | var it = mem.tokenizeScalar(u16, PATH, ';'); | |
| 903 | while (it.next()) |search_path| { | |
| 904 | dir_buf.clearRetainingCapacity(); | |
| 905 | try dir_buf.appendSlice(self.allocator, search_path); | |
| 906 | // Need to normalize the path, some PATH values can contain things like double | |
| 907 | // backslashes which openDirW can't handle | |
| 908 | const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue; | |
| 909 | dir_buf.shrinkRetainingCapacity(normalized_len); | |
| 910 | ||
| 911 | if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) { | |
| 912 | break :run; | |
| 913 | } else |err| switch (err) { | |
| 914 | // argv[0] contains unsupported characters that will never resolve to a valid exe. | |
| 915 | error.InvalidArg0 => return error.FileNotFound, | |
| 916 | error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, | |
| 917 | error.UnrecoverableInvalidExe => return error.InvalidExe, | |
| 918 | else => |e| return e, | |
| 919 | } | |
| 920 | } else { | |
| 921 | return original_err; | |
| 922 | } | |
| 923 | }; | |
| 924 | } | |
| 925 | ||
| 926 | if (g_hChildStd_IN_Wr) |h| { | |
| 927 | self.stdin = File{ .handle = h }; | |
| 928 | } else { | |
| 929 | self.stdin = null; | |
| 930 | } | |
| 931 | if (g_hChildStd_OUT_Rd) |h| { | |
| 932 | self.stdout = File{ .handle = h }; | |
| 933 | } else { | |
| 934 | self.stdout = null; | |
| 935 | } | |
| 936 | if (g_hChildStd_ERR_Rd) |h| { | |
| 937 | self.stderr = File{ .handle = h }; | |
| 938 | } else { | |
| 939 | self.stderr = null; | |
| 940 | } | |
| 941 | ||
| 942 | self.id = piProcInfo.hProcess; | |
| 943 | self.thread_handle = piProcInfo.hThread; | |
| 944 | self.term = null; | |
| 945 | ||
| 946 | if (self.stdin_behavior == StdIo.Pipe) { | |
| 947 | posix.close(g_hChildStd_IN_Rd.?); | |
| 948 | } | |
| 949 | if (self.stderr_behavior == StdIo.Pipe) { | |
| 950 | posix.close(g_hChildStd_ERR_Wr.?); | |
| 951 | } | |
| 952 | if (self.stdout_behavior == StdIo.Pipe) { | |
| 953 | posix.close(g_hChildStd_OUT_Wr.?); | |
| 954 | } | |
| 955 | } | |
| 956 | ||
| 957 | fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { | |
| 958 | switch (stdio) { | |
| 959 | .Pipe => try posix.dup2(pipe_fd, std_fileno), | |
| 960 | .Close => posix.close(std_fileno), | |
| 961 | .Inherit => {}, | |
| 962 | .Ignore => try posix.dup2(dev_null_fd, std_fileno), | |
| 963 | } | |
| 964 | } | |
| 965 | ||
| 966 | fn destroyPipe(pipe: [2]posix.fd_t) void { | |
| 967 | posix.close(pipe[0]); | |
| 968 | if (pipe[0] != pipe[1]) posix.close(pipe[1]); | |
| 969 | } | |
| 970 | ||
| 971 | // Child of fork calls this to report an error to the fork parent. | |
| 972 | // Then the child exits. | |
| 973 | fn forkChildErrReport(fd: i32, err: ChildProcess.SpawnError) noreturn { | |
| 974 | writeIntFd(fd, @as(ErrInt, @intFromError(err))) catch {}; | |
| 975 | // If we're linking libc, some naughty applications may have registered atexit handlers | |
| 976 | // which we really do not want to run in the fork child. I caught LLVM doing this and | |
| 977 | // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne, | |
| 978 | // "Why'd you have to go and make things so complicated?" | |
| 979 | if (builtin.link_libc) { | |
| 980 | // The _exit(2) function does nothing but make the exit syscall, unlike exit(3) | |
| 981 | std.c._exit(1); | |
| 982 | } | |
| 983 | posix.exit(1); | |
| 984 | } | |
| 985 | ||
| 986 | fn writeIntFd(fd: i32, value: ErrInt) !void { | |
| 987 | const file: File = .{ .handle = fd }; | |
| 988 | file.writer().writeInt(u64, @intCast(value), .little) catch return error.SystemResources; | |
| 989 | } | |
| 990 | ||
| 991 | fn readIntFd(fd: i32) !ErrInt { | |
| 992 | const file: File = .{ .handle = fd }; | |
| 993 | return @intCast(file.reader().readInt(u64, .little) catch return error.SystemResources); | |
| 994 | } | |
| 995 | ||
| 996 | const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); | |
| 997 | ||
| 998 | /// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. | |
| 999 | /// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. | |
| 1000 | /// Note: `app_buf` should not contain any leading path separators. | |
| 1001 | /// Note: If the dir is the cwd, dir_buf should be empty (len = 0). | |
| 1002 | fn windowsCreateProcessPathExt( | |
| 1003 | allocator: mem.Allocator, | |
| 1004 | dir_buf: *std.ArrayListUnmanaged(u16), | |
| 1005 | app_buf: *std.ArrayListUnmanaged(u16), | |
| 1006 | pathext: [:0]const u16, | |
| 1007 | cmd_line_cache: *WindowsCommandLineCache, | |
| 1008 | envp_ptr: ?[*]u16, | |
| 1009 | cwd_ptr: ?[*:0]u16, | |
| 1010 | lpStartupInfo: *windows.STARTUPINFOW, | |
| 1011 | lpProcessInformation: *windows.PROCESS_INFORMATION, | |
| 1012 | ) !void { | |
| 1013 | const app_name_len = app_buf.items.len; | |
| 1014 | const dir_path_len = dir_buf.items.len; | |
| 1015 | ||
| 1016 | if (app_name_len == 0) return error.FileNotFound; | |
| 1017 | ||
| 1018 | defer app_buf.shrinkRetainingCapacity(app_name_len); | |
| 1019 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | |
| 1020 | ||
| 1021 | // The name of the game here is to avoid CreateProcessW calls at all costs, | |
| 1022 | // and only ever try calling it when we have a real candidate for execution. | |
| 1023 | // Secondarily, we want to minimize the number of syscalls used when checking | |
| 1024 | // for each PATHEXT-appended version of the app name. | |
| 1025 | // | |
| 1026 | // An overview of the technique used: | |
| 1027 | // - Open the search directory for iteration (either cwd or a path from PATH) | |
| 1028 | // - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to | |
| 1029 | // check if anything that could possibly match either the unappended version | |
| 1030 | // of the app name or any of the versions with a PATHEXT value appended exists. | |
| 1031 | // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early | |
| 1032 | // without needing to use PATHEXT at all. | |
| 1033 | // | |
| 1034 | // This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence | |
| 1035 | // for any directory that doesn't contain any possible matches, instead of having | |
| 1036 | // to use a separate look up for each individual filename combination (unappended + | |
| 1037 | // each PATHEXT appended). For directories where the wildcard *does* match something, | |
| 1038 | // we iterate the matches and take note of any that are either the unappended version, | |
| 1039 | // or a version with a supported PATHEXT appended. We then try calling CreateProcessW | |
| 1040 | // with the found versions in the appropriate order. | |
| 1041 | ||
| 1042 | var dir = dir: { | |
| 1043 | // needs to be null-terminated | |
| 1044 | try dir_buf.append(allocator, 0); | |
| 1045 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); | |
| 1046 | const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | |
| 1047 | const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); | |
| 1048 | break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch | |
| 1049 | return error.FileNotFound; | |
| 1050 | }; | |
| 1051 | defer dir.close(); | |
| 1052 | ||
| 1053 | // Add wildcard and null-terminator | |
| 1054 | try app_buf.append(allocator, '*'); | |
| 1055 | try app_buf.append(allocator, 0); | |
| 1056 | const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; | |
| 1057 | ||
| 1058 | // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries | |
| 1059 | // returned per NtQueryDirectoryFile call. | |
| 1060 | var file_information_buf: [2048]u8 align(@alignOf(windows.FILE_DIRECTORY_INFORMATION)) = undefined; | |
| 1061 | const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); | |
| 1062 | if (file_information_buf.len < file_info_maximum_single_entry_size) { | |
| 1063 | @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); | |
| 1064 | } | |
| 1065 | var io_status: windows.IO_STATUS_BLOCK = undefined; | |
| 1066 | ||
| 1067 | const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; | |
| 1068 | var pathext_seen = [_]bool{false} ** num_supported_pathext; | |
| 1069 | var any_pathext_seen = false; | |
| 1070 | var unappended_exists = false; | |
| 1071 | ||
| 1072 | // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions | |
| 1073 | // of the app_name we should try to spawn. | |
| 1074 | // Note: This is necessary because the order of the files returned is filesystem-dependent: | |
| 1075 | // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. | |
| 1076 | // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. | |
| 1077 | while (true) { | |
| 1078 | const app_name_len_bytes = std.math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; | |
| 1079 | var app_name_unicode_string = windows.UNICODE_STRING{ | |
| 1080 | .Length = app_name_len_bytes, | |
| 1081 | .MaximumLength = app_name_len_bytes, | |
| 1082 | .Buffer = @constCast(app_name_wildcard.ptr), | |
| 1083 | }; | |
| 1084 | const rc = windows.ntdll.NtQueryDirectoryFile( | |
| 1085 | dir.fd, | |
| 1086 | null, | |
| 1087 | null, | |
| 1088 | null, | |
| 1089 | &io_status, | |
| 1090 | &file_information_buf, | |
| 1091 | file_information_buf.len, | |
| 1092 | .FileDirectoryInformation, | |
| 1093 | windows.FALSE, // single result | |
| 1094 | &app_name_unicode_string, | |
| 1095 | windows.FALSE, // restart iteration | |
| 1096 | ); | |
| 1097 | ||
| 1098 | // If we get nothing with the wildcard, then we can just bail out | |
| 1099 | // as we know appending PATHEXT will not yield anything. | |
| 1100 | switch (rc) { | |
| 1101 | .SUCCESS => {}, | |
| 1102 | .NO_SUCH_FILE => return error.FileNotFound, | |
| 1103 | .NO_MORE_FILES => break, | |
| 1104 | .ACCESS_DENIED => return error.AccessDenied, | |
| 1105 | else => return windows.unexpectedStatus(rc), | |
| 1106 | } | |
| 1107 | ||
| 1108 | // According to the docs, this can only happen if there is not enough room in the | |
| 1109 | // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. | |
| 1110 | // Therefore, this condition should not be possible to hit with the buffer size we use. | |
| 1111 | std.debug.assert(io_status.Information != 0); | |
| 1112 | ||
| 1113 | var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; | |
| 1114 | while (it.next()) |info| { | |
| 1115 | // Skip directories | |
| 1116 | if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; | |
| 1117 | const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; | |
| 1118 | // Because all results start with the app_name since we're using the wildcard `app_name*`, | |
| 1119 | // if the length is equal to app_name then this is an exact match | |
| 1120 | if (filename.len == app_name_len) { | |
| 1121 | // Note: We can't break early here because it's possible that the unappended version | |
| 1122 | // fails to spawn, in which case we still want to try the PATHEXT appended versions. | |
| 1123 | unappended_exists = true; | |
| 1124 | } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { | |
| 1125 | pathext_seen[@intFromEnum(pathext_ext)] = true; | |
| 1126 | any_pathext_seen = true; | |
| 1127 | } | |
| 1128 | } | |
| 1129 | } | |
| 1130 | ||
| 1131 | const unappended_err = unappended: { | |
| 1132 | if (unappended_exists) { | |
| 1133 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | |
| 1134 | '/', '\\' => {}, | |
| 1135 | else => try dir_buf.append(allocator, fs.path.sep), | |
| 1136 | }; | |
| 1137 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | |
| 1138 | try dir_buf.append(allocator, 0); | |
| 1139 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | |
| 1140 | ||
| 1141 | const is_bat_or_cmd = bat_or_cmd: { | |
| 1142 | const app_name = app_buf.items[0..app_name_len]; | |
| 1143 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :bat_or_cmd false; | |
| 1144 | const ext = app_name[ext_start..]; | |
| 1145 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse break :bat_or_cmd false; | |
| 1146 | switch (ext_enum) { | |
| 1147 | .cmd, .bat => break :bat_or_cmd true, | |
| 1148 | else => break :bat_or_cmd false, | |
| 1149 | } | |
| 1150 | }; | |
| 1151 | const cmd_line_w = if (is_bat_or_cmd) | |
| 1152 | try cmd_line_cache.scriptCommandLine(full_app_name) | |
| 1153 | else | |
| 1154 | try cmd_line_cache.commandLine(); | |
| 1155 | const app_name_w = if (is_bat_or_cmd) | |
| 1156 | try cmd_line_cache.cmdExePath() | |
| 1157 | else | |
| 1158 | full_app_name; | |
| 1159 | ||
| 1160 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { | |
| 1161 | return; | |
| 1162 | } else |err| switch (err) { | |
| 1163 | error.FileNotFound, | |
| 1164 | error.AccessDenied, | |
| 1165 | => break :unappended err, | |
| 1166 | error.InvalidExe => { | |
| 1167 | // On InvalidExe, if the extension of the app name is .exe then | |
| 1168 | // it's treated as an unrecoverable error. Otherwise, it'll be | |
| 1169 | // skipped as normal. | |
| 1170 | const app_name = app_buf.items[0..app_name_len]; | |
| 1171 | const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; | |
| 1172 | const ext = app_name[ext_start..]; | |
| 1173 | if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | |
| 1174 | return error.UnrecoverableInvalidExe; | |
| 1175 | } | |
| 1176 | break :unappended err; | |
| 1177 | }, | |
| 1178 | else => return err, | |
| 1179 | } | |
| 1180 | } | |
| 1181 | break :unappended error.FileNotFound; | |
| 1182 | }; | |
| 1183 | ||
| 1184 | if (!any_pathext_seen) return unappended_err; | |
| 1185 | ||
| 1186 | // Now try any PATHEXT appended versions that we've seen | |
| 1187 | var ext_it = mem.tokenizeScalar(u16, pathext, ';'); | |
| 1188 | while (ext_it.next()) |ext| { | |
| 1189 | const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; | |
| 1190 | if (!pathext_seen[@intFromEnum(ext_enum)]) continue; | |
| 1191 | ||
| 1192 | dir_buf.shrinkRetainingCapacity(dir_path_len); | |
| 1193 | if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { | |
| 1194 | '/', '\\' => {}, | |
| 1195 | else => try dir_buf.append(allocator, fs.path.sep), | |
| 1196 | }; | |
| 1197 | try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); | |
| 1198 | try dir_buf.appendSlice(allocator, ext); | |
| 1199 | try dir_buf.append(allocator, 0); | |
| 1200 | const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; | |
| 1201 | ||
| 1202 | const is_bat_or_cmd = switch (ext_enum) { | |
| 1203 | .cmd, .bat => true, | |
| 1204 | else => false, | |
| 1205 | }; | |
| 1206 | const cmd_line_w = if (is_bat_or_cmd) | |
| 1207 | try cmd_line_cache.scriptCommandLine(full_app_name) | |
| 1208 | else | |
| 1209 | try cmd_line_cache.commandLine(); | |
| 1210 | const app_name_w = if (is_bat_or_cmd) | |
| 1211 | try cmd_line_cache.cmdExePath() | |
| 1212 | else | |
| 1213 | full_app_name; | |
| 1214 | ||
| 1215 | if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { | |
| 1216 | return; | |
| 1217 | } else |err| switch (err) { | |
| 1218 | error.FileNotFound => continue, | |
| 1219 | error.AccessDenied => continue, | |
| 1220 | error.InvalidExe => { | |
| 1221 | // On InvalidExe, if the extension of the app name is .exe then | |
| 1222 | // it's treated as an unrecoverable error. Otherwise, it'll be | |
| 1223 | // skipped as normal. | |
| 1224 | if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { | |
| 1225 | return error.UnrecoverableInvalidExe; | |
| 1226 | } | |
| 1227 | continue; | |
| 1228 | }, | |
| 1229 | else => return err, | |
| 1230 | } | |
| 1231 | } | |
| 1232 | ||
| 1233 | return unappended_err; | |
| 1234 | } | |
| 1235 | ||
| 1236 | fn windowsCreateProcess( | |
| 1237 | app_name: [*:0]u16, | |
| 1238 | cmd_line: [*:0]u16, | |
| 1239 | envp_ptr: ?[*]u16, | |
| 1240 | cwd_ptr: ?[*:0]u16, | |
| 1241 | lpStartupInfo: *windows.STARTUPINFOW, | |
| 1242 | lpProcessInformation: *windows.PROCESS_INFORMATION, | |
| 1243 | ) !void { | |
| 1244 | // TODO the docs for environment pointer say: | |
| 1245 | // > A pointer to the environment block for the new process. If this parameter | |
| 1246 | // > is NULL, the new process uses the environment of the calling process. | |
| 1247 | // > ... | |
| 1248 | // > An environment block can contain either Unicode or ANSI characters. If | |
| 1249 | // > the environment block pointed to by lpEnvironment contains Unicode | |
| 1250 | // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. | |
| 1251 | // > If this parameter is NULL and the environment block of the parent process | |
| 1252 | // > contains Unicode characters, you must also ensure that dwCreationFlags | |
| 1253 | // > includes CREATE_UNICODE_ENVIRONMENT. | |
| 1254 | // This seems to imply that we have to somehow know whether our process parent passed | |
| 1255 | // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter. | |
| 1256 | // Since we do not know this information that would imply that we must not pass NULL | |
| 1257 | // for the parameter. | |
| 1258 | // However this would imply that programs compiled with -DUNICODE could not pass | |
| 1259 | // environment variables to programs that were not, which seems unlikely. | |
| 1260 | // More investigation is needed. | |
| 1261 | return windows.CreateProcessW( | |
| 1262 | app_name, | |
| 1263 | cmd_line, | |
| 1264 | null, | |
| 1265 | null, | |
| 1266 | windows.TRUE, | |
| 1267 | windows.CREATE_UNICODE_ENVIRONMENT, | |
| 1268 | @as(?*anyopaque, @ptrCast(envp_ptr)), | |
| 1269 | cwd_ptr, | |
| 1270 | lpStartupInfo, | |
| 1271 | lpProcessInformation, | |
| 1272 | ); | |
| 1273 | } | |
| 1274 | ||
| 1275 | fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | |
| 1276 | var rd_h: windows.HANDLE = undefined; | |
| 1277 | var wr_h: windows.HANDLE = undefined; | |
| 1278 | try windows.CreatePipe(&rd_h, &wr_h, sattr); | |
| 1279 | errdefer windowsDestroyPipe(rd_h, wr_h); | |
| 1280 | try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); | |
| 1281 | rd.* = rd_h; | |
| 1282 | wr.* = wr_h; | |
| 1283 | } | |
| 1284 | ||
| 1285 | fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { | |
| 1286 | if (rd) |h| posix.close(h); | |
| 1287 | if (wr) |h| posix.close(h); | |
| 1288 | } | |
| 1289 | ||
| 1290 | fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { | |
| 1291 | var tmp_bufw: [128]u16 = undefined; | |
| 1292 | ||
| 1293 | // Anonymous pipes are built upon Named pipes. | |
| 1294 | // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe | |
| 1295 | // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. | |
| 1296 | // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations | |
| 1297 | const pipe_path = blk: { | |
| 1298 | var tmp_buf: [128]u8 = undefined; | |
| 1299 | // Forge a random path for the pipe. | |
| 1300 | const pipe_path = std.fmt.bufPrintZ( | |
| 1301 | &tmp_buf, | |
| 1302 | "\\\\.\\pipe\\zig-childprocess-{d}-{d}", | |
| 1303 | .{ windows.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .monotonic) }, | |
| 1304 | ) catch unreachable; | |
| 1305 | const len = std.unicode.wtf8ToWtf16Le(&tmp_bufw, pipe_path) catch unreachable; | |
| 1306 | tmp_bufw[len] = 0; | |
| 1307 | break :blk tmp_bufw[0..len :0]; | |
| 1308 | }; | |
| 1309 | ||
| 1310 | // Create the read handle that can be used with overlapped IO ops. | |
| 1311 | const read_handle = windows.kernel32.CreateNamedPipeW( | |
| 1312 | pipe_path.ptr, | |
| 1313 | windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, | |
| 1314 | windows.PIPE_TYPE_BYTE, | |
| 1315 | 1, | |
| 1316 | 4096, | |
| 1317 | 4096, | |
| 1318 | 0, | |
| 1319 | sattr, | |
| 1320 | ); | |
| 1321 | if (read_handle == windows.INVALID_HANDLE_VALUE) { | |
| 1322 | switch (windows.kernel32.GetLastError()) { | |
| 1323 | else => |err| return windows.unexpectedError(err), | |
| 1324 | } | |
| 1325 | } | |
| 1326 | errdefer posix.close(read_handle); | |
| 1327 | ||
| 1328 | var sattr_copy = sattr.*; | |
| 1329 | const write_handle = windows.kernel32.CreateFileW( | |
| 1330 | pipe_path.ptr, | |
| 1331 | windows.GENERIC_WRITE, | |
| 1332 | 0, | |
| 1333 | &sattr_copy, | |
| 1334 | windows.OPEN_EXISTING, | |
| 1335 | windows.FILE_ATTRIBUTE_NORMAL, | |
| 1336 | null, | |
| 1337 | ); | |
| 1338 | if (write_handle == windows.INVALID_HANDLE_VALUE) { | |
| 1339 | switch (windows.kernel32.GetLastError()) { | |
| 1340 | else => |err| return windows.unexpectedError(err), | |
| 1341 | } | |
| 1342 | } | |
| 1343 | errdefer posix.close(write_handle); | |
| 1344 | ||
| 1345 | try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); | |
| 1346 | ||
| 1347 | rd.* = read_handle; | |
| 1348 | wr.* = write_handle; | |
| 1349 | } | |
| 1350 | ||
| 1351 | var pipe_name_counter = std.atomic.Value(u32).init(1); | |
| 1352 | ||
| 1353 | // Should be kept in sync with `windowsCreateProcessSupportsExtension` | |
| 1354 | const CreateProcessSupportedExtension = enum { | |
| 1355 | bat, | |
| 1356 | cmd, | |
| 1357 | com, | |
| 1358 | exe, | |
| 1359 | }; | |
| 1360 | ||
| 1361 | /// Case-insensitive WTF-16 lookup | |
| 1362 | fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { | |
| 1363 | if (ext.len != 4) return null; | |
| 1364 | const State = enum { | |
| 1365 | start, | |
| 1366 | dot, | |
| 1367 | b, | |
| 1368 | ba, | |
| 1369 | c, | |
| 1370 | cm, | |
| 1371 | co, | |
| 1372 | e, | |
| 1373 | ex, | |
| 1374 | }; | |
| 1375 | var state: State = .start; | |
| 1376 | for (ext) |c| switch (state) { | |
| 1377 | .start => switch (c) { | |
| 1378 | '.' => state = .dot, | |
| 1379 | else => return null, | |
| 1380 | }, | |
| 1381 | .dot => switch (c) { | |
| 1382 | 'b', 'B' => state = .b, | |
| 1383 | 'c', 'C' => state = .c, | |
| 1384 | 'e', 'E' => state = .e, | |
| 1385 | else => return null, | |
| 1386 | }, | |
| 1387 | .b => switch (c) { | |
| 1388 | 'a', 'A' => state = .ba, | |
| 1389 | else => return null, | |
| 1390 | }, | |
| 1391 | .c => switch (c) { | |
| 1392 | 'm', 'M' => state = .cm, | |
| 1393 | 'o', 'O' => state = .co, | |
| 1394 | else => return null, | |
| 1395 | }, | |
| 1396 | .e => switch (c) { | |
| 1397 | 'x', 'X' => state = .ex, | |
| 1398 | else => return null, | |
| 1399 | }, | |
| 1400 | .ba => switch (c) { | |
| 1401 | 't', 'T' => return .bat, | |
| 1402 | else => return null, | |
| 1403 | }, | |
| 1404 | .cm => switch (c) { | |
| 1405 | 'd', 'D' => return .cmd, | |
| 1406 | else => return null, | |
| 1407 | }, | |
| 1408 | .co => switch (c) { | |
| 1409 | 'm', 'M' => return .com, | |
| 1410 | else => return null, | |
| 1411 | }, | |
| 1412 | .ex => switch (c) { | |
| 1413 | 'e', 'E' => return .exe, | |
| 1414 | else => return null, | |
| 1415 | }, | |
| 1416 | }; | |
| 1417 | return null; | |
| 1418 | } | |
| 1419 | ||
| 1420 | test windowsCreateProcessSupportsExtension { | |
| 1421 | try std.testing.expectEqual(CreateProcessSupportedExtension.exe, windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e' }).?); | |
| 1422 | try std.testing.expect(windowsCreateProcessSupportsExtension(&[_]u16{ '.', 'e', 'X', 'e', 'c' }) == null); | |
| 1423 | } | |
| 1424 | ||
| 1425 | /// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW. | |
| 1426 | /// | |
| 1427 | /// Serialization is done on-demand and the result is cached in order to allow for: | |
| 1428 | /// - Only serializing the particular type of command line needed (`.bat`/`.cmd` | |
| 1429 | /// command line serialization is different from `.exe`/etc) | |
| 1430 | /// - Reusing the serialized command lines if necessary (i.e. if the execution | |
| 1431 | /// of a command fails and the PATH is going to be continued to be searched | |
| 1432 | /// for more candidates) | |
| 1433 | const WindowsCommandLineCache = struct { | |
| 1434 | cmd_line: ?[:0]u16 = null, | |
| 1435 | script_cmd_line: ?[:0]u16 = null, | |
| 1436 | cmd_exe_path: ?[:0]u16 = null, | |
| 1437 | argv: []const []const u8, | |
| 1438 | allocator: mem.Allocator, | |
| 1439 | ||
| 1440 | fn init(allocator: mem.Allocator, argv: []const []const u8) WindowsCommandLineCache { | |
| 1441 | return .{ | |
| 1442 | .allocator = allocator, | |
| 1443 | .argv = argv, | |
| 1444 | }; | |
| 1445 | } | |
| 1446 | ||
| 1447 | fn deinit(self: *WindowsCommandLineCache) void { | |
| 1448 | if (self.cmd_line) |cmd_line| self.allocator.free(cmd_line); | |
| 1449 | if (self.script_cmd_line) |script_cmd_line| self.allocator.free(script_cmd_line); | |
| 1450 | if (self.cmd_exe_path) |cmd_exe_path| self.allocator.free(cmd_exe_path); | |
| 1451 | } | |
| 1452 | ||
| 1453 | fn commandLine(self: *WindowsCommandLineCache) ![:0]u16 { | |
| 1454 | if (self.cmd_line == null) { | |
| 1455 | self.cmd_line = try argvToCommandLineWindows(self.allocator, self.argv); | |
| 1456 | } | |
| 1457 | return self.cmd_line.?; | |
| 1458 | } | |
| 1459 | ||
| 1460 | /// Not cached, since the path to the batch script will change during PATH searching. | |
| 1461 | /// `script_path` should be as qualified as possible, e.g. if the PATH is being searched, | |
| 1462 | /// then script_path should include both the search path and the script filename | |
| 1463 | /// (this allows avoiding cmd.exe having to search the PATH again). | |
| 1464 | fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 { | |
| 1465 | if (self.script_cmd_line) |v| self.allocator.free(v); | |
| 1466 | self.script_cmd_line = try argvToScriptCommandLineWindows( | |
| 1467 | self.allocator, | |
| 1468 | script_path, | |
| 1469 | self.argv[1..], | |
| 1470 | ); | |
| 1471 | return self.script_cmd_line.?; | |
| 1472 | } | |
| 1473 | ||
| 1474 | fn cmdExePath(self: *WindowsCommandLineCache) ![:0]u16 { | |
| 1475 | if (self.cmd_exe_path == null) { | |
| 1476 | self.cmd_exe_path = try windowsCmdExePath(self.allocator); | |
| 1477 | } | |
| 1478 | return self.cmd_exe_path.?; | |
| 1479 | } | |
| 1480 | }; | |
| 1481 | ||
| 1482 | /// Returns the absolute path of `cmd.exe` within the Windows system directory. | |
| 1483 | /// The caller owns the returned slice. | |
| 1484 | fn windowsCmdExePath(allocator: mem.Allocator) error{ OutOfMemory, Unexpected }![:0]u16 { | |
| 1485 | var buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 128); | |
| 1486 | errdefer buf.deinit(allocator); | |
| 1487 | while (true) { | |
| 1488 | const unused_slice = buf.unusedCapacitySlice(); | |
| 1489 | // TODO: Get the system directory from PEB.ReadOnlyStaticServerData | |
| 1490 | const len = windows.kernel32.GetSystemDirectoryW(@ptrCast(unused_slice), @intCast(unused_slice.len)); | |
| 1491 | if (len == 0) { | |
| 1492 | switch (windows.kernel32.GetLastError()) { | |
| 1493 | else => |err| return windows.unexpectedError(err), | |
| 1494 | } | |
| 1495 | } | |
| 1496 | if (len > unused_slice.len) { | |
| 1497 | try buf.ensureUnusedCapacity(allocator, len); | |
| 1498 | } else { | |
| 1499 | buf.items.len = len; | |
| 1500 | break; | |
| 1501 | } | |
| 1502 | } | |
| 1503 | switch (buf.items[buf.items.len - 1]) { | |
| 1504 | '/', '\\' => {}, | |
| 1505 | else => try buf.append(allocator, fs.path.sep), | |
| 1506 | } | |
| 1507 | try buf.appendSlice(allocator, unicode.utf8ToUtf16LeStringLiteral("cmd.exe")); | |
| 1508 | return try buf.toOwnedSliceSentinel(allocator, 0); | |
| 1509 | } | |
| 1510 | ||
| 1511 | const ArgvToCommandLineError = error{ OutOfMemory, InvalidWtf8, InvalidArg0 }; | |
| 1512 | ||
| 1513 | /// Serializes `argv` to a Windows command-line string suitable for passing to a child process and | |
| 1514 | /// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice. | |
| 1515 | /// | |
| 1516 | /// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts. | |
| 1517 | /// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | |
| 1518 | /// | |
| 1519 | /// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead. | |
| 1520 | fn argvToCommandLineWindows( | |
| 1521 | allocator: mem.Allocator, | |
| 1522 | argv: []const []const u8, | |
| 1523 | ) ArgvToCommandLineError![:0]u16 { | |
| 1524 | var buf = std.ArrayList(u8).init(allocator); | |
| 1525 | defer buf.deinit(); | |
| 1526 | ||
| 1527 | if (argv.len != 0) { | |
| 1528 | const arg0 = argv[0]; | |
| 1529 | ||
| 1530 | // The first argument must be quoted if it contains spaces or ASCII control characters | |
| 1531 | // (excluding DEL). It also follows special quoting rules where backslashes have no special | |
| 1532 | // interpretation, which makes it impossible to pass certain first arguments containing | |
| 1533 | // double quotes to a child process without characters from the first argument leaking into | |
| 1534 | // subsequent ones (which could have security implications). | |
| 1535 | // | |
| 1536 | // Empty arguments technically don't need quotes, but we quote them anyway for maximum | |
| 1537 | // compatibility with different implementations of the 'CommandLineToArgvW' algorithm. | |
| 1538 | // | |
| 1539 | // Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject | |
| 1540 | // all first arguments containing double quotes, even ones that we could theoretically | |
| 1541 | // serialize in unquoted form. | |
| 1542 | var needs_quotes = arg0.len == 0; | |
| 1543 | for (arg0) |c| { | |
| 1544 | if (c <= ' ') { | |
| 1545 | needs_quotes = true; | |
| 1546 | } else if (c == '"') { | |
| 1547 | return error.InvalidArg0; | |
| 1548 | } | |
| 1549 | } | |
| 1550 | if (needs_quotes) { | |
| 1551 | try buf.append('"'); | |
| 1552 | try buf.appendSlice(arg0); | |
| 1553 | try buf.append('"'); | |
| 1554 | } else { | |
| 1555 | try buf.appendSlice(arg0); | |
| 1556 | } | |
| 1557 | ||
| 1558 | for (argv[1..]) |arg| { | |
| 1559 | try buf.append(' '); | |
| 1560 | ||
| 1561 | // Subsequent arguments must be quoted if they contain spaces, tabs or double quotes, | |
| 1562 | // or if they are empty. For simplicity and for maximum compatibility with different | |
| 1563 | // implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII | |
| 1564 | // control characters (again, excluding DEL). | |
| 1565 | needs_quotes = for (arg) |c| { | |
| 1566 | if (c <= ' ' or c == '"') { | |
| 1567 | break true; | |
| 1568 | } | |
| 1569 | } else arg.len == 0; | |
| 1570 | if (!needs_quotes) { | |
| 1571 | try buf.appendSlice(arg); | |
| 1572 | continue; | |
| 1573 | } | |
| 1574 | ||
| 1575 | try buf.append('"'); | |
| 1576 | var backslash_count: usize = 0; | |
| 1577 | for (arg) |byte| { | |
| 1578 | switch (byte) { | |
| 1579 | '\\' => { | |
| 1580 | backslash_count += 1; | |
| 1581 | }, | |
| 1582 | '"' => { | |
| 1583 | try buf.appendNTimes('\\', backslash_count * 2 + 1); | |
| 1584 | try buf.append('"'); | |
| 1585 | backslash_count = 0; | |
| 1586 | }, | |
| 1587 | else => { | |
| 1588 | try buf.appendNTimes('\\', backslash_count); | |
| 1589 | try buf.append(byte); | |
| 1590 | backslash_count = 0; | |
| 1591 | }, | |
| 1592 | } | |
| 1593 | } | |
| 1594 | try buf.appendNTimes('\\', backslash_count * 2); | |
| 1595 | try buf.append('"'); | |
| 1596 | } | |
| 1597 | } | |
| 1598 | ||
| 1599 | return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | |
| 1600 | } | |
| 1601 | ||
| 1602 | test argvToCommandLineWindows { | |
| 1603 | const t = testArgvToCommandLineWindows; | |
| 1604 | ||
| 1605 | try t(&.{ | |
| 1606 | \\C:\Program Files\zig\zig.exe | |
| 1607 | , | |
| 1608 | \\run | |
| 1609 | , | |
| 1610 | \\.\src\main.zig | |
| 1611 | , | |
| 1612 | \\-target | |
| 1613 | , | |
| 1614 | \\x86_64-windows-gnu | |
| 1615 | , | |
| 1616 | \\-O | |
| 1617 | , | |
| 1618 | \\ReleaseSafe | |
| 1619 | , | |
| 1620 | \\-- | |
| 1621 | , | |
| 1622 | \\--emoji=🗿 | |
| 1623 | , | |
| 1624 | \\--eval=new Regex("Dwayne \"The Rock\" Johnson") | |
| 1625 | , | |
| 1626 | }, | |
| 1627 | \\"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\")" | |
| 1628 | ); | |
| 1629 | ||
| 1630 | try t(&.{}, ""); | |
| 1631 | try t(&.{""}, "\"\""); | |
| 1632 | try t(&.{" "}, "\" \""); | |
| 1633 | try t(&.{"\t"}, "\"\t\""); | |
| 1634 | try t(&.{"\x07"}, "\"\x07\""); | |
| 1635 | try t(&.{"🦎"}, "🦎"); | |
| 1636 | ||
| 1637 | try t( | |
| 1638 | &.{ "zig", "aa aa", "bb\tbb", "cc\ncc", "dd\r\ndd", "ee\x7Fee" }, | |
| 1639 | "zig \"aa aa\" \"bb\tbb\" \"cc\ncc\" \"dd\r\ndd\" ee\x7Fee", | |
| 1640 | ); | |
| 1641 | ||
| 1642 | try t( | |
| 1643 | &.{ "\\\\foo bar\\foo bar\\", "\\\\zig zag\\zig zag\\" }, | |
| 1644 | "\"\\\\foo bar\\foo bar\\\" \"\\\\zig zag\\zig zag\\\\\"", | |
| 1645 | ); | |
| 1646 | ||
| 1647 | try std.testing.expectError( | |
| 1648 | error.InvalidArg0, | |
| 1649 | argvToCommandLineWindows(std.testing.allocator, &.{"\"quotes\"quotes\""}), | |
| 1650 | ); | |
| 1651 | try std.testing.expectError( | |
| 1652 | error.InvalidArg0, | |
| 1653 | argvToCommandLineWindows(std.testing.allocator, &.{"quotes\"quotes"}), | |
| 1654 | ); | |
| 1655 | try std.testing.expectError( | |
| 1656 | error.InvalidArg0, | |
| 1657 | argvToCommandLineWindows(std.testing.allocator, &.{"q u o t e s \" q u o t e s"}), | |
| 1658 | ); | |
| 1659 | } | |
| 1660 | ||
| 1661 | fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []const u8) !void { | |
| 1662 | const cmd_line_w = try argvToCommandLineWindows(std.testing.allocator, argv); | |
| 1663 | defer std.testing.allocator.free(cmd_line_w); | |
| 1664 | ||
| 1665 | const cmd_line = try unicode.wtf16LeToWtf8Alloc(std.testing.allocator, cmd_line_w); | |
| 1666 | defer std.testing.allocator.free(cmd_line); | |
| 1667 | ||
| 1668 | try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); | |
| 1669 | } | |
| 1670 | ||
| 1671 | const ArgvToScriptCommandLineError = error{ | |
| 1672 | OutOfMemory, | |
| 1673 | InvalidWtf8, | |
| 1674 | /// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed | |
| 1675 | /// within arguments when executing a `.bat`/`.cmd` script. | |
| 1676 | /// - NUL/LF signifiies end of arguments, so anything afterwards | |
| 1677 | /// would be lost after execution. | |
| 1678 | /// - CR is stripped by `cmd.exe`, so any CR codepoints | |
| 1679 | /// would be lost after execution. | |
| 1680 | InvalidBatchScriptArg, | |
| 1681 | }; | |
| 1682 | ||
| 1683 | /// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific | |
| 1684 | /// escaping rules. The caller owns the returned slice. | |
| 1685 | /// | |
| 1686 | /// Escapes `argv` using the suggested mitigation against arbitrary command execution from: | |
| 1687 | /// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | |
| 1688 | /// | |
| 1689 | /// The return of this function will look like | |
| 1690 | /// `cmd.exe /d /e:ON /v:OFF /c "<escaped command line>"` | |
| 1691 | /// and should be used as the `lpCommandLine` of `CreateProcessW`, while the | |
| 1692 | /// return of `windowsCmdExePath` should be used as `lpApplicationName`. | |
| 1693 | /// | |
| 1694 | /// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise. | |
| 1695 | /// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem. | |
| 1696 | fn argvToScriptCommandLineWindows( | |
| 1697 | allocator: mem.Allocator, | |
| 1698 | /// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD. | |
| 1699 | /// The script must have been verified to exist at this path before calling this function. | |
| 1700 | script_path: []const u16, | |
| 1701 | /// Arguments, not including the script name itself. Expected to be encoded as WTF-8. | |
| 1702 | script_args: []const []const u8, | |
| 1703 | ) ArgvToScriptCommandLineError![:0]u16 { | |
| 1704 | var buf = try std.ArrayList(u8).initCapacity(allocator, 64); | |
| 1705 | defer buf.deinit(); | |
| 1706 | ||
| 1707 | // `/d` disables execution of AutoRun commands. | |
| 1708 | // `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation: | |
| 1709 | // > If delayed expansion is enabled via the registry value DelayedExpansion, | |
| 1710 | // > it must be disabled by explicitly calling cmd.exe with the /V:OFF option. | |
| 1711 | // > Escaping for % requires the command extension to be enabled. | |
| 1712 | // > If it’s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option. | |
| 1713 | // https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/ | |
| 1714 | buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \""); | |
| 1715 | ||
| 1716 | // Always quote the path to the script arg | |
| 1717 | buf.appendAssumeCapacity('"'); | |
| 1718 | // We always want the path to the batch script to include a path separator in order to | |
| 1719 | // avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary | |
| 1720 | // command execution mitigation, we just know exactly what script we want to execute | |
| 1721 | // at this point, and potentially making cmd.exe re-find it is unnecessary. | |
| 1722 | // | |
| 1723 | // If the script path does not have a path separator, then we know its relative to CWD and | |
| 1724 | // we can just put `.\` in the front. | |
| 1725 | if (mem.indexOfAny(u16, script_path, &[_]u16{ mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/') }) == null) { | |
| 1726 | try buf.appendSlice(".\\"); | |
| 1727 | } | |
| 1728 | // Note that we don't do any escaping/mitigations for this argument, since the relevant | |
| 1729 | // characters (", %, etc) are illegal in file paths and this function should only be called | |
| 1730 | // with script paths that have been verified to exist. | |
| 1731 | try unicode.wtf16LeToWtf8ArrayList(&buf, script_path); | |
| 1732 | buf.appendAssumeCapacity('"'); | |
| 1733 | ||
| 1734 | for (script_args) |arg| { | |
| 1735 | // Literal carriage returns get stripped when run through cmd.exe | |
| 1736 | // and NUL/newlines act as 'end of command.' Because of this, it's basically | |
| 1737 | // always a mistake to include these characters in argv, so it's | |
| 1738 | // an error condition in order to ensure that the return of this | |
| 1739 | // function can always roundtrip through cmd.exe. | |
| 1740 | if (std.mem.indexOfAny(u8, arg, "\x00\r\n") != null) { | |
| 1741 | return error.InvalidBatchScriptArg; | |
| 1742 | } | |
| 1743 | ||
| 1744 | // Separate args with a space. | |
| 1745 | try buf.append(' '); | |
| 1746 | ||
| 1747 | // Need to quote if the argument is empty (otherwise the arg would just be lost) | |
| 1748 | // or if the last character is a `\`, since then something like "%~2" in a .bat | |
| 1749 | // script would cause the closing " to be escaped which we don't want. | |
| 1750 | var needs_quotes = arg.len == 0 or arg[arg.len - 1] == '\\'; | |
| 1751 | if (!needs_quotes) { | |
| 1752 | for (arg) |c| { | |
| 1753 | switch (c) { | |
| 1754 | // Known good characters that don't need to be quoted | |
| 1755 | 'A'...'Z', 'a'...'z', '0'...'9', '#', '$', '*', '+', '-', '.', '/', ':', '?', '@', '\\', '_' => {}, | |
| 1756 | // When in doubt, quote | |
| 1757 | else => { | |
| 1758 | needs_quotes = true; | |
| 1759 | break; | |
| 1760 | }, | |
| 1761 | } | |
| 1762 | } | |
| 1763 | } | |
| 1764 | if (needs_quotes) { | |
| 1765 | try buf.append('"'); | |
| 1766 | } | |
| 1767 | var backslashes: usize = 0; | |
| 1768 | for (arg) |c| { | |
| 1769 | switch (c) { | |
| 1770 | '\\' => { | |
| 1771 | backslashes += 1; | |
| 1772 | }, | |
| 1773 | '"' => { | |
| 1774 | try buf.appendNTimes('\\', backslashes); | |
| 1775 | try buf.append('"'); | |
| 1776 | backslashes = 0; | |
| 1777 | }, | |
| 1778 | // Replace `%` with `%%cd:~,%`. | |
| 1779 | // | |
| 1780 | // cmd.exe allows extracting a substring from an environment | |
| 1781 | // variable with the syntax: `%foo:~<start_index>,<end_index>%`. | |
| 1782 | // Therefore, `%cd:~,%` will always expand to an empty string | |
| 1783 | // since both the start and end index are blank, and it is assumed | |
| 1784 | // that `%cd%` is always available since it is a built-in variable | |
| 1785 | // that corresponds to the current directory. | |
| 1786 | // | |
| 1787 | // This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%` | |
| 1788 | // will stop `%foo%` from being expanded and *after* expansion | |
| 1789 | // we'll still be left with `%foo%` (the literal string). | |
| 1790 | '%' => { | |
| 1791 | // the trailing `%` is appended outside the switch | |
| 1792 | try buf.appendSlice("%%cd:~,"); | |
| 1793 | backslashes = 0; | |
| 1794 | }, | |
| 1795 | else => { | |
| 1796 | backslashes = 0; | |
| 1797 | }, | |
| 1798 | } | |
| 1799 | try buf.append(c); | |
| 1800 | } | |
| 1801 | if (needs_quotes) { | |
| 1802 | try buf.appendNTimes('\\', backslashes); | |
| 1803 | try buf.append('"'); | |
| 1804 | } | |
| 1805 | } | |
| 1806 | ||
| 1807 | try buf.append('"'); | |
| 1808 | ||
| 1809 | return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items); | |
| 1810 | } |
lib/std/std.zig-2| ... | ... | @@ -14,8 +14,6 @@ pub const BoundedArrayAligned = @import("bounded_array.zig").BoundedArrayAligned |
| 14 | 14 | pub const Build = @import("Build.zig"); |
| 15 | 15 | pub const BufMap = @import("buf_map.zig").BufMap; |
| 16 | 16 | pub const BufSet = @import("buf_set.zig").BufSet; |
| 17 | /// Deprecated: use `process.Child`. | |
| 18 | pub const ChildProcess = @import("child_process.zig").ChildProcess; | |
| 19 | 17 | pub const StaticStringMap = static_string_map.StaticStringMap; |
| 20 | 18 | pub const StaticStringMapWithEql = static_string_map.StaticStringMapWithEql; |
| 21 | 19 | pub const DoublyLinkedList = @import("linked_list.zig").DoublyLinkedList; |
lib/std/zig/LibCInstallation.zig+2-2| ... | ... | @@ -261,7 +261,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, args: FindNativeOptions) F |
| 261 | 261 | dev_null, |
| 262 | 262 | }); |
| 263 | 263 | |
| 264 | const run_res = std.ChildProcess.run(.{ | |
| 264 | const run_res = std.process.Child.run(.{ | |
| 265 | 265 | .allocator = allocator, |
| 266 | 266 | .argv = argv.items, |
| 267 | 267 | .max_output_bytes = 1024 * 1024, |
| ... | ... | @@ -588,7 +588,7 @@ fn ccPrintFileName(args: CCPrintFileNameOptions) ![:0]u8 { |
| 588 | 588 | try appendCcExe(&argv, skip_cc_env_var); |
| 589 | 589 | try argv.append(arg1); |
| 590 | 590 | |
| 591 | const run_res = std.ChildProcess.run(.{ | |
| 591 | const run_res = std.process.Child.run(.{ | |
| 592 | 592 | .allocator = allocator, |
| 593 | 593 | .argv = argv.items, |
| 594 | 594 | .max_output_bytes = 1024 * 1024, |
src/Compilation.zig+3-3| ... | ... | @@ -1935,7 +1935,7 @@ pub fn getTarget(self: Compilation) Target { |
| 1935 | 1935 | pub fn hotCodeSwap( |
| 1936 | 1936 | comp: *Compilation, |
| 1937 | 1937 | prog_node: *std.Progress.Node, |
| 1938 | pid: std.ChildProcess.Id, | |
| 1938 | pid: std.process.Child.Id, | |
| 1939 | 1939 | ) !void { |
| 1940 | 1940 | const lf = comp.bin_file.?; |
| 1941 | 1941 | lf.child_pid = pid; |
| ... | ... | @@ -4615,7 +4615,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P |
| 4615 | 4615 | log.warn("failed to delete '{s}': {s}", .{ dep_file_path, @errorName(err) }); |
| 4616 | 4616 | }; |
| 4617 | 4617 | if (std.process.can_spawn) { |
| 4618 | var child = std.ChildProcess.init(argv.items, arena); | |
| 4618 | var child = std.process.Child.init(argv.items, arena); | |
| 4619 | 4619 | if (comp.clang_passthrough_mode) { |
| 4620 | 4620 | child.stdin_behavior = .Inherit; |
| 4621 | 4621 | child.stdout_behavior = .Inherit; |
| ... | ... | @@ -4974,7 +4974,7 @@ fn spawnZigRc( |
| 4974 | 4974 | var node_name: std.ArrayListUnmanaged(u8) = .{}; |
| 4975 | 4975 | defer node_name.deinit(arena); |
| 4976 | 4976 | |
| 4977 | var child = std.ChildProcess.init(argv, arena); | |
| 4977 | var child = std.process.Child.init(argv, arena); | |
| 4978 | 4978 | child.stdin_behavior = .Ignore; |
| 4979 | 4979 | child.stdout_behavior = .Pipe; |
| 4980 | 4980 | child.stderr_behavior = .Pipe; |
src/link.zig+2-2| ... | ... | @@ -72,7 +72,7 @@ pub const File = struct { |
| 72 | 72 | /// Prevents other processes from clobbering files in the output directory |
| 73 | 73 | /// of this linking operation. |
| 74 | 74 | lock: ?Cache.Lock = null, |
| 75 | child_pid: ?std.ChildProcess.Id = null, | |
| 75 | child_pid: ?std.process.Child.Id = null, | |
| 76 | 76 | |
| 77 | 77 | pub const OpenOptions = struct { |
| 78 | 78 | symbol_count_hint: u64 = 32, |
| ... | ... | @@ -529,7 +529,7 @@ pub const File = struct { |
| 529 | 529 | } || |
| 530 | 530 | fs.File.WriteFileError || |
| 531 | 531 | fs.File.OpenError || |
| 532 | std.ChildProcess.SpawnError || | |
| 532 | std.process.Child.SpawnError || | |
| 533 | 533 | fs.Dir.CopyFileError; |
| 534 | 534 | |
| 535 | 535 | /// Commit pending changes and write headers. Takes into account final output mode |
src/link/Coff.zig+5-5| ... | ... | @@ -893,7 +893,7 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { |
| 893 | 893 | } |
| 894 | 894 | } |
| 895 | 895 | |
| 896 | fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | |
| 896 | fn debugMem(allocator: Allocator, handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | |
| 897 | 897 | const buffer = try allocator.alloc(u8, code.len); |
| 898 | 898 | defer allocator.free(buffer); |
| 899 | 899 | const memread = try std.os.windows.ReadProcessMemory(handle, pvaddr, buffer); |
| ... | ... | @@ -901,7 +901,7 @@ fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, pvaddr: std.os.wi |
| 901 | 901 | log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(memread)}); |
| 902 | 902 | } |
| 903 | 903 | |
| 904 | fn writeMemProtected(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | |
| 904 | fn writeMemProtected(handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | |
| 905 | 905 | const old_prot = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY); |
| 906 | 906 | try writeMem(handle, pvaddr, code); |
| 907 | 907 | // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. |
| ... | ... | @@ -909,7 +909,7 @@ fn writeMemProtected(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, |
| 909 | 909 | _ = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, old_prot); |
| 910 | 910 | } |
| 911 | 911 | |
| 912 | fn writeMem(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | |
| 912 | fn writeMem(handle: std.process.Child.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | |
| 913 | 913 | const amt = try std.os.windows.WriteProcessMemory(handle, pvaddr, code); |
| 914 | 914 | if (amt != code.len) return error.InputOutput; |
| 915 | 915 | } |
| ... | ... | @@ -1031,7 +1031,7 @@ fn resolveRelocs(self: *Coff, atom_index: Atom.Index, relocs: []*const Relocatio |
| 1031 | 1031 | } |
| 1032 | 1032 | } |
| 1033 | 1033 | |
| 1034 | pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { | |
| 1034 | pub fn ptraceAttach(self: *Coff, handle: std.process.Child.Id) !void { | |
| 1035 | 1035 | if (!is_hot_update_compatible) return; |
| 1036 | 1036 | |
| 1037 | 1037 | log.debug("attaching to process with handle {*}", .{handle}); |
| ... | ... | @@ -1041,7 +1041,7 @@ pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { |
| 1041 | 1041 | }; |
| 1042 | 1042 | } |
| 1043 | 1043 | |
| 1044 | pub fn ptraceDetach(self: *Coff, handle: std.ChildProcess.Id) void { | |
| 1044 | pub fn ptraceDetach(self: *Coff, handle: std.process.Child.Id) void { | |
| 1045 | 1045 | if (!is_hot_update_compatible) return; |
| 1046 | 1046 | |
| 1047 | 1047 | log.debug("detaching from process with handle {*}", .{handle}); |
src/link/Wasm.zig+1-1| ... | ... | @@ -3637,7 +3637,7 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node) !vo |
| 3637 | 3637 | // If possible, we run LLD as a child process because it does not always |
| 3638 | 3638 | // behave properly as a library, unfortunately. |
| 3639 | 3639 | // https://github.com/ziglang/zig/issues/3825 |
| 3640 | var child = std.ChildProcess.init(argv.items, arena); | |
| 3640 | var child = std.process.Child.init(argv.items, arena); | |
| 3641 | 3641 | if (comp.clang_passthrough_mode) { |
| 3642 | 3642 | child.stdin_behavior = .Inherit; |
| 3643 | 3643 | child.stdout_behavior = .Inherit; |
src/main.zig+6-6| ... | ... | @@ -4026,7 +4026,7 @@ fn serve( |
| 4026 | 4026 | }); |
| 4027 | 4027 | defer server.deinit(); |
| 4028 | 4028 | |
| 4029 | var child_pid: ?std.ChildProcess.Id = null; | |
| 4029 | var child_pid: ?std.process.Child.Id = null; | |
| 4030 | 4030 | |
| 4031 | 4031 | var progress: std.Progress = .{ |
| 4032 | 4032 | .terminal = null, |
| ... | ... | @@ -4316,7 +4316,7 @@ fn runOrTest( |
| 4316 | 4316 | const cmd = try std.mem.join(arena, " ", argv.items); |
| 4317 | 4317 | fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd }); |
| 4318 | 4318 | } else if (process.can_spawn) { |
| 4319 | var child = std.ChildProcess.init(argv.items, gpa); | |
| 4319 | var child = std.process.Child.init(argv.items, gpa); | |
| 4320 | 4320 | child.env_map = &env_map; |
| 4321 | 4321 | child.stdin_behavior = .Inherit; |
| 4322 | 4322 | child.stdout_behavior = .Inherit; |
| ... | ... | @@ -4379,7 +4379,7 @@ fn runOrTestHotSwap( |
| 4379 | 4379 | arg_mode: ArgMode, |
| 4380 | 4380 | all_args: []const []const u8, |
| 4381 | 4381 | runtime_args_start: ?usize, |
| 4382 | ) !std.ChildProcess.Id { | |
| 4382 | ) !std.process.Child.Id { | |
| 4383 | 4383 | const lf = comp.bin_file.?; |
| 4384 | 4384 | |
| 4385 | 4385 | const exe_path = switch (builtin.target.os.tag) { |
| ... | ... | @@ -4456,7 +4456,7 @@ fn runOrTestHotSwap( |
| 4456 | 4456 | return pid; |
| 4457 | 4457 | }, |
| 4458 | 4458 | else => { |
| 4459 | var child = std.ChildProcess.init(argv.items, gpa); | |
| 4459 | var child = std.process.Child.init(argv.items, gpa); | |
| 4460 | 4460 | |
| 4461 | 4461 | child.stdin_behavior = .Inherit; |
| 4462 | 4462 | child.stdout_behavior = .Inherit; |
| ... | ... | @@ -5245,7 +5245,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { |
| 5245 | 5245 | } |
| 5246 | 5246 | |
| 5247 | 5247 | if (process.can_spawn) { |
| 5248 | var child = std.ChildProcess.init(child_argv.items, gpa); | |
| 5248 | var child = std.process.Child.init(child_argv.items, gpa); | |
| 5249 | 5249 | child.stdin_behavior = .Inherit; |
| 5250 | 5250 | child.stdout_behavior = .Inherit; |
| 5251 | 5251 | child.stderr_behavior = .Inherit; |
| ... | ... | @@ -5549,7 +5549,7 @@ fn jitCmd( |
| 5549 | 5549 | }); |
| 5550 | 5550 | } |
| 5551 | 5551 | |
| 5552 | var child = std.ChildProcess.init(child_argv.items, gpa); | |
| 5552 | var child = std.process.Child.init(child_argv.items, gpa); | |
| 5553 | 5553 | child.stdin_behavior = .Inherit; |
| 5554 | 5554 | child.stdout_behavior = if (options.capture == null) .Inherit else .Pipe; |
| 5555 | 5555 | child.stderr_behavior = .Inherit; |
test/src/Cases.zig+1-1| ... | ... | @@ -1839,7 +1839,7 @@ fn runOneCase( |
| 1839 | 1839 | try comp.makeBinFileExecutable(); |
| 1840 | 1840 | |
| 1841 | 1841 | while (true) { |
| 1842 | break :x std.ChildProcess.run(.{ | |
| 1842 | break :x std.process.Child.run(.{ | |
| 1843 | 1843 | .allocator = allocator, |
| 1844 | 1844 | .argv = argv.items, |
| 1845 | 1845 | .cwd_dir = tmp.dir, |
test/standalone/child_process/main.zig+1-1| ... | ... | @@ -13,7 +13,7 @@ pub fn main() !void { |
| 13 | 13 | _ = it.next() orelse unreachable; // skip binary name |
| 14 | 14 | const child_path = it.next() orelse unreachable; |
| 15 | 15 | |
| 16 | var child = std.ChildProcess.init(&.{ child_path, "hello arg" }, gpa); | |
| 16 | var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa); | |
| 17 | 17 | child.stdin_behavior = .Pipe; |
| 18 | 18 | child.stdout_behavior = .Pipe; |
| 19 | 19 | child.stderr_behavior = .Inherit; |
test/standalone/windows_bat_args/fuzz.zig+1-1| ... | ... | @@ -87,7 +87,7 @@ fn testExecBat(allocator: std.mem.Allocator, bat: []const u8, args: []const []co |
| 87 | 87 | |
| 88 | 88 | const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat"); |
| 89 | 89 | |
| 90 | const result = try std.ChildProcess.run(.{ | |
| 90 | const result = try std.process.Child.run(.{ | |
| 91 | 91 | .allocator = allocator, |
| 92 | 92 | .env_map = env, |
| 93 | 93 | .argv = argv.items, |
test/standalone/windows_bat_args/test.zig+1-1| ... | ... | @@ -109,7 +109,7 @@ fn testExecBat(allocator: std.mem.Allocator, bat: []const u8, args: []const []co |
| 109 | 109 | |
| 110 | 110 | const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat"); |
| 111 | 111 | |
| 112 | const result = try std.ChildProcess.run(.{ | |
| 112 | const result = try std.process.Child.run(.{ | |
| 113 | 113 | .allocator = allocator, |
| 114 | 114 | .env_map = env, |
| 115 | 115 | .argv = argv.items, |
test/standalone/windows_spawn/main.zig+1-1| ... | ... | @@ -158,7 +158,7 @@ fn testExec(allocator: std.mem.Allocator, command: []const u8, expected_stdout: |
| 158 | 158 | } |
| 159 | 159 | |
| 160 | 160 | fn testExecWithCwd(allocator: std.mem.Allocator, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void { |
| 161 | const result = try std.ChildProcess.run(.{ | |
| 161 | const result = try std.process.Child.run(.{ | |
| 162 | 162 | .allocator = allocator, |
| 163 | 163 | .argv = &[_][]const u8{command}, |
| 164 | 164 | .cwd = cwd, |
tools/docgen.zig+1-1| ... | ... | @@ -3,7 +3,7 @@ const builtin = @import("builtin"); |
| 3 | 3 | const io = std.io; |
| 4 | 4 | const fs = std.fs; |
| 5 | 5 | const process = std.process; |
| 6 | const ChildProcess = std.ChildProcess; | |
| 6 | const ChildProcess = std.process.Child; | |
| 7 | 7 | const Progress = std.Progress; |
| 8 | 8 | const print = std.debug.print; |
| 9 | 9 | const mem = std.mem; |
tools/generate_linux_syscalls.zig+2-2| ... | ... | @@ -257,7 +257,7 @@ pub fn main() !void { |
| 257 | 257 | "arch/arm64/include/uapi/asm/unistd.h", |
| 258 | 258 | }; |
| 259 | 259 | |
| 260 | const child_result = try std.ChildProcess.run(.{ | |
| 260 | const child_result = try std.process.Child.run(.{ | |
| 261 | 261 | .allocator = allocator, |
| 262 | 262 | .argv = &child_args, |
| 263 | 263 | .cwd = linux_path, |
| ... | ... | @@ -318,7 +318,7 @@ pub fn main() !void { |
| 318 | 318 | "arch/riscv/include/uapi/asm/unistd.h", |
| 319 | 319 | }; |
| 320 | 320 | |
| 321 | const child_result = try std.ChildProcess.run(.{ | |
| 321 | const child_result = try std.process.Child.run(.{ | |
| 322 | 322 | .allocator = allocator, |
| 323 | 323 | .argv = &child_args, |
| 324 | 324 | .cwd = linux_path, |
tools/update_clang_options.zig+1-1| ... | ... | @@ -624,7 +624,7 @@ pub fn main() anyerror!void { |
| 624 | 624 | try std.fmt.allocPrint(allocator, "-I={s}/clang/include/clang/Driver", .{llvm_src_root}), |
| 625 | 625 | }; |
| 626 | 626 | |
| 627 | const child_result = try std.ChildProcess.run(.{ | |
| 627 | const child_result = try std.process.Child.run(.{ | |
| 628 | 628 | .allocator = allocator, |
| 629 | 629 | .argv = &child_args, |
| 630 | 630 | .max_output_bytes = 100 * 1024 * 1024, |
tools/update_cpu_features.zig+1-1| ... | ... | @@ -1106,7 +1106,7 @@ fn processOneTarget(job: Job) anyerror!void { |
| 1106 | 1106 | }), |
| 1107 | 1107 | }; |
| 1108 | 1108 | |
| 1109 | const child_result = try std.ChildProcess.run(.{ | |
| 1109 | const child_result = try std.process.Child.run(.{ | |
| 1110 | 1110 | .allocator = arena, |
| 1111 | 1111 | .argv = &child_args, |
| 1112 | 1112 | .max_output_bytes = 400 * 1024 * 1024, |