2024-05-23 11:25:41 -07:00
const std = @import ( " ../std.zig " ) ;
const builtin = @import ( " builtin " ) ;
const unicode = std . unicode ;
const fs = std . fs ;
const process = std . process ;
const File = std . fs . File ;
const windows = std . os . windows ;
const linux = std . os . linux ;
const posix = std . posix ;
const mem = std . mem ;
const EnvMap = std . process . EnvMap ;
const maxInt = std . math . maxInt ;
const assert = std . debug . assert ;
const native_os = builtin . os . tag ;
2024-05-23 14:10:03 -07:00
const Allocator = std . mem . Allocator ;
2024-05-23 11:25:41 -07:00
const ChildProcess = @This ( ) ;
pub const Id = switch ( native_os ) {
. windows = > windows . HANDLE ,
. wasi = > void ,
else = > posix . pid_t ,
} ;
/// Available after calling `spawn()`. This becomes `undefined` after calling `wait()`.
/// On Windows this is the hProcess.
/// On POSIX this is the pid.
id : Id ,
thread_handle : if ( native_os == . windows ) windows . HANDLE else void ,
allocator : mem . Allocator ,
/// The writing end of the child process's standard input pipe.
/// Usage requires `stdin_behavior == StdIo.Pipe`.
/// Available after calling `spawn()`.
stdin : ? File ,
/// The reading end of the child process's standard output pipe.
/// Usage requires `stdout_behavior == StdIo.Pipe`.
/// Available after calling `spawn()`.
stdout : ? File ,
/// The reading end of the child process's standard error pipe.
/// Usage requires `stderr_behavior == StdIo.Pipe`.
/// Available after calling `spawn()`.
stderr : ? File ,
/// Terminated state of the child process.
/// Available after calling `wait()`.
term : ? ( SpawnError ! Term ) ,
argv : [ ] const [ ] const u8 ,
/// Leave as null to use the current env map using the supplied allocator.
env_map : ? * const EnvMap ,
stdin_behavior : StdIo ,
stdout_behavior : StdIo ,
stderr_behavior : StdIo ,
/// Set to change the user id when spawning the child process.
uid : if ( native_os == . windows or native_os == . wasi ) void else ? posix . uid_t ,
/// Set to change the group id when spawning the child process.
gid : if ( native_os == . windows or native_os == . wasi ) void else ? posix . gid_t ,
2024-07-09 21:30:46 +09:00
/// Set to change the process group id when spawning the child process.
pgid : if ( native_os == . windows or native_os == . wasi ) void else ? posix . pid_t ,
2024-05-23 11:25:41 -07:00
/// Set to change the current working directory when spawning the child process.
cwd : ? [ ] const u8 ,
/// Set to change the current working directory when spawning the child process.
/// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190
/// Once that is done, `cwd` will be deprecated in favor of this field.
cwd_dir : ? fs . Dir = null ,
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
err_pipe : if ( native_os == . windows ) void else ? posix . fd_t ,
2024-05-23 11:25:41 -07:00
expand_arg0 : Arg0Expand ,
/// Darwin-only. Disable ASLR for the child process.
disable_aslr : bool = false ,
2025-03-02 20:27:17 -07:00
/// Darwin and Windows only. Start child process in suspended state. For Darwin it's started
/// as if SIGSTOP was sent.
2024-05-23 11:25:41 -07:00
start_suspended : bool = false ,
2025-03-02 20:27:17 -07:00
/// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess.
create_no_window : bool = false ,
2024-05-23 11:25:41 -07:00
/// Set to true to obtain rusage information for the child process.
/// Depending on the target platform and implementation status, the
/// requested statistics may or may not be available. If they are
/// available, then the `resource_usage_statistics` field will be populated
/// after calling `wait`.
/// On Linux and Darwin, this obtains rusage statistics from wait4().
request_resource_usage_statistics : bool = false ,
/// This is available after calling wait if
/// `request_resource_usage_statistics` was set to `true` before calling
/// `spawn`.
resource_usage_statistics : ResourceUsageStatistics = . { } ,
2024-05-23 14:10:03 -07:00
/// When populated, a pipe will be created for the child process to
/// communicate progress back to the parent. The file descriptor of the
/// write end of the pipe will be specified in the `ZIG_PROGRESS`
/// environment variable inside the child process. The progress reported by
/// the child will be attached to this progress node in the parent process.
2024-05-23 17:01:06 -07:00
///
/// The child's progress tree will be grafted into the parent's progress tree,
/// by substituting this node with the child's root node.
2024-06-16 19:28:13 -07:00
progress_node : std . Progress . Node = std . Progress . Node . none ,
2024-05-23 14:10:03 -07:00
2024-05-23 11:25:41 -07:00
pub const ResourceUsageStatistics = struct {
rusage : @TypeOf ( rusage_init ) = rusage_init ,
/// Returns the peak resident set size of the child process, in bytes,
/// if available.
pub inline fn getMaxRss ( rus : ResourceUsageStatistics ) ? usize {
switch ( native_os ) {
. linux = > {
if ( rus . rusage ) | ru | {
return @as ( usize , @intCast ( ru . maxrss ) ) * 1024 ;
} else {
return null ;
}
} ,
. windows = > {
if ( rus . rusage ) | ru | {
return ru . PeakWorkingSetSize ;
} else {
return null ;
}
} ,
. macos , . ios = > {
if ( rus . rusage ) | ru | {
// Darwin oddly reports in bytes instead of kilobytes.
return @as ( usize , @intCast ( ru . maxrss ) ) ;
} else {
return null ;
}
} ,
else = > return null ,
}
}
const rusage_init = switch ( native_os ) {
. linux , . macos , . ios = > @as ( ? posix . rusage , null ) ,
. windows = > @as ( ? windows . VM_COUNTERS , null ) ,
else = > { } ,
} ;
} ;
pub const Arg0Expand = posix . Arg0Expand ;
pub const SpawnError = error {
OutOfMemory ,
/// POSIX-only. `StdIo.Ignore` was selected and opening `/dev/null` returned ENODEV.
NoDevice ,
/// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8.
/// https://simonsapin.github.io/wtf-8/
InvalidWtf8 ,
/// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process.
CurrentWorkingDirectoryUnlinked ,
/// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
/// within arguments when executing a `.bat`/`.cmd` script.
/// - NUL/LF signifiies end of arguments, so anything afterwards
/// would be lost after execution.
/// - CR is stripped by `cmd.exe`, so any CR codepoints
/// would be lost after execution.
InvalidBatchScriptArg ,
} ||
posix . ExecveError ||
posix . SetIdError ||
2024-07-09 21:30:46 +09:00
posix . SetPgidError ||
2024-05-23 11:25:41 -07:00
posix . ChangeCurDirError ||
windows . CreateProcessError ||
windows . GetProcessMemoryInfoError ||
windows . WaitForSingleObjectError ;
pub const Term = union ( enum ) {
Exited : u8 ,
Signal : u32 ,
Stopped : u32 ,
Unknown : u32 ,
} ;
/// Behavior of the child process's standard input, output, and error
/// streams.
pub const StdIo = enum {
/// Inherit the stream from the parent process.
Inherit ,
/// Pass a null stream to the child process.
/// This is /dev/null on POSIX and NUL on Windows.
Ignore ,
/// Create a pipe for the stream.
/// The corresponding field (`stdout`, `stderr`, or `stdin`)
/// will be assigned a `File` object that can be used
/// to read from or write to the pipe.
Pipe ,
/// Close the stream after the child process spawns.
Close ,
} ;
/// First argument in argv is the executable.
pub fn init ( argv : [ ] const [ ] const u8 , allocator : mem . Allocator ) ChildProcess {
return . {
. allocator = allocator ,
. argv = argv ,
. id = undefined ,
. thread_handle = undefined ,
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
. err_pipe = if ( native_os == . windows ) { } else null ,
2024-05-23 11:25:41 -07:00
. term = null ,
. env_map = null ,
. cwd = null ,
. uid = if ( native_os == . windows or native_os == . wasi ) { } else null ,
. gid = if ( native_os == . windows or native_os == . wasi ) { } else null ,
2024-07-09 21:30:46 +09:00
. pgid = if ( native_os == . windows or native_os == . wasi ) { } else null ,
2024-05-23 11:25:41 -07:00
. stdin = null ,
. stdout = null ,
. stderr = null ,
2024-05-23 20:22:58 -07:00
. stdin_behavior = . Inherit ,
. stdout_behavior = . Inherit ,
. stderr_behavior = . Inherit ,
2024-05-23 11:25:41 -07:00
. expand_arg0 = . no_expand ,
} ;
}
pub fn setUserName ( self : * ChildProcess , name : [ ] const u8 ) ! void {
const user_info = try process . getUserInfo ( name ) ;
self . uid = user_info . uid ;
self . gid = user_info . gid ;
}
/// On success must call `kill` or `wait`.
/// After spawning the `id` is available.
pub fn spawn ( self : * ChildProcess ) SpawnError ! void {
if ( ! process . can_spawn ) {
@compileError ( " the target operating system cannot spawn processes " ) ;
}
if ( native_os == . windows ) {
return self . spawnWindows ( ) ;
} else {
return self . spawnPosix ( ) ;
}
}
pub fn spawnAndWait ( self : * ChildProcess ) SpawnError ! Term {
try self . spawn ( ) ;
return self . wait ( ) ;
}
/// Forcibly terminates child process and then cleans up all resources.
pub fn kill ( self : * ChildProcess ) ! Term {
if ( native_os == . windows ) {
return self . killWindows ( 1 ) ;
} else {
return self . killPosix ( ) ;
}
}
pub fn killWindows ( self : * ChildProcess , exit_code : windows . UINT ) ! Term {
if ( self . term ) | term | {
self . cleanupStreams ( ) ;
return term ;
}
windows . TerminateProcess ( self . id , exit_code ) catch | err | switch ( err ) {
2025-02-18 17:12:51 -08:00
error . AccessDenied = > {
2024-05-23 11:25:41 -07:00
// Usually when TerminateProcess triggers a ACCESS_DENIED error, it
// indicates that the process has already exited, but there may be
// some rare edge cases where our process handle no longer has the
// PROCESS_TERMINATE access right, so let's do another check to make
// sure the process is really no longer running:
windows . WaitForSingleObjectEx ( self . id , 0 , false ) catch return err ;
return error . AlreadyTerminated ;
} ,
else = > return err ,
} ;
try self . waitUnwrappedWindows ( ) ;
return self . term .? ;
}
pub fn killPosix ( self : * ChildProcess ) ! Term {
if ( self . term ) | term | {
self . cleanupStreams ( ) ;
return term ;
}
posix . kill ( self . id , posix . SIG . TERM ) catch | err | switch ( err ) {
error . ProcessNotFound = > return error . AlreadyTerminated ,
else = > return err ,
} ;
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
self . waitUnwrappedPosix ( ) ;
2024-05-23 11:25:41 -07:00
return self . term .? ;
}
2024-11-25 12:39:57 -08:00
pub const WaitError = SpawnError || std . os . windows . GetProcessMemoryInfoError ;
2024-05-23 11:25:41 -07:00
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
/// On some targets, `spawn` may not report all spawn errors, such as `error.InvalidExe`.
/// This function will block until any spawn errors can be reported, and return them.
pub fn waitForSpawn ( self : * ChildProcess ) SpawnError ! void {
if ( native_os == . windows ) return ; // `spawn` reports everything
if ( self . term ) | term | {
_ = term catch | spawn_err | return spawn_err ;
return ;
}
const err_pipe = self . err_pipe orelse return ;
self . err_pipe = null ;
// Wait for the child to report any errors in or before `execvpe`.
if ( readIntFd ( err_pipe ) ) | child_err_int | {
posix . close ( err_pipe ) ;
const child_err : SpawnError = @errorCast ( @errorFromInt ( child_err_int ) ) ;
self . term = child_err ;
return child_err ;
} else | _ | {
// Write end closed by CLOEXEC at the time of the `execvpe` call, indicating success!
posix . close ( err_pipe ) ;
}
}
2024-11-25 12:39:57 -08:00
/// Blocks until child process terminates and then cleans up all resources.
pub fn wait ( self : * ChildProcess ) WaitError ! Term {
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
try self . waitForSpawn ( ) ; // report spawn errors
if ( self . term ) | term | {
self . cleanupStreams ( ) ;
return term ;
}
switch ( native_os ) {
. windows = > try self . waitUnwrappedWindows ( ) ,
else = > self . waitUnwrappedPosix ( ) ,
}
2024-05-23 11:25:41 -07:00
self . id = undefined ;
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
return self . term .? ;
2024-05-23 11:25:41 -07:00
}
pub const RunResult = struct {
term : Term ,
stdout : [ ] u8 ,
stderr : [ ] u8 ,
} ;
2025-01-30 21:35:53 -08:00
fn writeFifoDataToArrayList ( allocator : Allocator , list : * std . ArrayListUnmanaged ( u8 ) , fifo : * std . io . PollFifo ) ! void {
2024-09-14 02:42:11 +01:00
if ( fifo . head != 0 ) fifo . realign ( ) ;
2025-01-30 21:35:53 -08:00
if ( list . capacity == 0 ) {
list .* = . {
. items = fifo . buf [ 0 .. fifo . count ] ,
. capacity = fifo . buf . len ,
} ;
fifo .* = std . io . PollFifo . init ( fifo . allocator ) ;
} else {
try list . appendSlice ( allocator , fifo . buf [ 0 .. fifo . count ] ) ;
}
2024-05-23 11:25:41 -07:00
}
/// Collect the output from the process's stdout and stderr. Will return once all output
/// has been collected. This does not mean that the process has ended. `wait` should still
/// be called to wait for and clean up the process.
///
/// The process must be started with stdout_behavior and stderr_behavior == .Pipe
pub fn collectOutput (
child : ChildProcess ,
2025-01-30 21:35:53 -08:00
/// Used for `stdout` and `stderr`.
allocator : Allocator ,
stdout : * std . ArrayListUnmanaged ( u8 ) ,
stderr : * std . ArrayListUnmanaged ( u8 ) ,
2024-05-23 11:25:41 -07:00
max_output_bytes : usize ,
) ! void {
assert ( child . stdout_behavior == . Pipe ) ;
assert ( child . stderr_behavior == . Pipe ) ;
2025-01-30 21:35:53 -08:00
var poller = std . io . poll ( allocator , enum { stdout , stderr } , . {
2024-05-23 11:25:41 -07:00
. stdout = child . stdout .? ,
. stderr = child . stderr .? ,
} ) ;
defer poller . deinit ( ) ;
while ( try poller . poll ( ) ) {
if ( poller . fifo ( . stdout ) . count > max_output_bytes )
return error . StdoutStreamTooLong ;
if ( poller . fifo ( . stderr ) . count > max_output_bytes )
return error . StderrStreamTooLong ;
}
2025-01-30 21:35:53 -08:00
try writeFifoDataToArrayList ( allocator , stdout , poller . fifo ( . stdout ) ) ;
try writeFifoDataToArrayList ( allocator , stderr , poller . fifo ( . stderr ) ) ;
2024-05-23 11:25:41 -07:00
}
pub const RunError = posix . GetCwdError || posix . ReadError || SpawnError || posix . PollError || error {
StdoutStreamTooLong ,
StderrStreamTooLong ,
} ;
/// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
/// If it succeeds, the caller owns result.stdout and result.stderr memory.
pub fn run ( args : struct {
allocator : mem . Allocator ,
argv : [ ] const [ ] const u8 ,
cwd : ? [ ] const u8 = null ,
cwd_dir : ? fs . Dir = null ,
env_map : ? * const EnvMap = null ,
max_output_bytes : usize = 50 * 1024 ,
expand_arg0 : Arg0Expand = . no_expand ,
2024-06-16 19:28:13 -07:00
progress_node : std . Progress . Node = std . Progress . Node . none ,
2024-05-23 11:25:41 -07:00
} ) RunError ! RunResult {
var child = ChildProcess . init ( args . argv , args . allocator ) ;
child . stdin_behavior = . Ignore ;
child . stdout_behavior = . Pipe ;
child . stderr_behavior = . Pipe ;
child . cwd = args . cwd ;
child . cwd_dir = args . cwd_dir ;
child . env_map = args . env_map ;
child . expand_arg0 = args . expand_arg0 ;
2024-06-16 19:28:13 -07:00
child . progress_node = args . progress_node ;
2024-05-23 11:25:41 -07:00
2025-01-30 21:35:53 -08:00
var stdout : std . ArrayListUnmanaged ( u8 ) = . empty ;
errdefer stdout . deinit ( args . allocator ) ;
var stderr : std . ArrayListUnmanaged ( u8 ) = . empty ;
errdefer stderr . deinit ( args . allocator ) ;
2024-05-23 11:25:41 -07:00
try child . spawn ( ) ;
2025-01-30 12:01:53 +00:00
errdefer {
_ = child . kill ( ) catch { } ;
}
2025-01-30 21:35:53 -08:00
try child . collectOutput ( args . allocator , & stdout , & stderr , args . max_output_bytes ) ;
2024-05-23 11:25:41 -07:00
return RunResult {
2025-01-30 21:35:53 -08:00
. stdout = try stdout . toOwnedSlice ( args . allocator ) ,
. stderr = try stderr . toOwnedSlice ( args . allocator ) ,
2025-01-30 12:01:53 +00:00
. term = try child . wait ( ) ,
2024-05-23 11:25:41 -07:00
} ;
}
2024-11-25 12:39:57 -08:00
fn waitUnwrappedWindows ( self : * ChildProcess ) WaitError ! void {
2024-05-23 11:25:41 -07:00
const result = windows . WaitForSingleObjectEx ( self . id , windows . INFINITE , false ) ;
self . term = @as ( SpawnError ! Term , x : {
var exit_code : windows . DWORD = undefined ;
if ( windows . kernel32 . GetExitCodeProcess ( self . id , & exit_code ) == 0 ) {
break : x Term { . Unknown = 0 } ;
} else {
break : x Term { . Exited = @as ( u8 , @truncate ( exit_code ) ) } ;
}
} ) ;
if ( self . request_resource_usage_statistics ) {
self . resource_usage_statistics . rusage = try windows . GetProcessMemoryInfo ( self . id ) ;
}
posix . close ( self . id ) ;
posix . close ( self . thread_handle ) ;
self . cleanupStreams ( ) ;
return result ;
}
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
fn waitUnwrappedPosix ( self : * ChildProcess ) void {
2024-05-23 11:25:41 -07:00
const res : posix . WaitPidResult = res : {
if ( self . request_resource_usage_statistics ) {
switch ( native_os ) {
. linux , . macos , . ios = > {
var ru : posix . rusage = undefined ;
const res = posix . wait4 ( self . id , 0 , & ru ) ;
self . resource_usage_statistics . rusage = ru ;
break : res res ;
} ,
else = > { } ,
}
}
break : res posix . waitpid ( self . id , 0 ) ;
} ;
const status = res . status ;
self . cleanupStreams ( ) ;
self . handleWaitResult ( status ) ;
}
fn handleWaitResult ( self : * ChildProcess , status : u32 ) void {
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
self . term = statusToTerm ( status ) ;
2024-05-23 11:25:41 -07:00
}
fn cleanupStreams ( self : * ChildProcess ) void {
if ( self . stdin ) | * stdin | {
stdin . close ( ) ;
self . stdin = null ;
}
if ( self . stdout ) | * stdout | {
stdout . close ( ) ;
self . stdout = null ;
}
if ( self . stderr ) | * stderr | {
stderr . close ( ) ;
self . stderr = null ;
}
}
fn statusToTerm ( status : u32 ) Term {
return if ( posix . W . IFEXITED ( status ) )
Term { . Exited = posix . W . EXITSTATUS ( status ) }
else if ( posix . W . IFSIGNALED ( status ) )
Term { . Signal = posix . W . TERMSIG ( status ) }
else if ( posix . W . IFSTOPPED ( status ) )
Term { . Stopped = posix . W . STOPSIG ( status ) }
else
Term { . Unknown = status } ;
}
fn spawnPosix ( self : * ChildProcess ) SpawnError ! void {
2024-05-26 09:40:49 -07:00
// The child process does need to access (one end of) these pipes. However,
// we must initially set CLOEXEC to avoid a race condition. If another thread
// is racing to spawn a different child process, we don't want it to inherit
// these FDs in any scenario; that would mean that, for instance, calls to
// `poll` from the parent would not report the child's stdout as closing when
// expected, since the other child may retain a reference to the write end of
// the pipe. So, we create the pipes with CLOEXEC initially. After fork, we
// need to do something in the new child to make sure we preserve the reference
// we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it
// turns out, we `dup2` everything anyway, so there's no need!
const pipe_flags : posix . O = . { . CLOEXEC = true } ;
2024-05-23 20:22:58 -07:00
const stdin_pipe = if ( self . stdin_behavior == . Pipe ) try posix . pipe2 ( pipe_flags ) else undefined ;
errdefer if ( self . stdin_behavior == . Pipe ) {
2024-05-23 11:25:41 -07:00
destroyPipe ( stdin_pipe ) ;
} ;
2024-05-23 20:22:58 -07:00
const stdout_pipe = if ( self . stdout_behavior == . Pipe ) try posix . pipe2 ( pipe_flags ) else undefined ;
errdefer if ( self . stdout_behavior == . Pipe ) {
2024-05-23 11:25:41 -07:00
destroyPipe ( stdout_pipe ) ;
} ;
2024-05-23 20:22:58 -07:00
const stderr_pipe = if ( self . stderr_behavior == . Pipe ) try posix . pipe2 ( pipe_flags ) else undefined ;
errdefer if ( self . stderr_behavior == . Pipe ) {
2024-05-23 11:25:41 -07:00
destroyPipe ( stderr_pipe ) ;
} ;
2024-05-23 20:22:58 -07:00
const any_ignore = ( self . stdin_behavior == . Ignore or self . stdout_behavior == . Ignore or self . stderr_behavior == . Ignore ) ;
2024-05-23 11:25:41 -07:00
const dev_null_fd = if ( any_ignore )
posix . openZ ( " /dev/null " , . { . ACCMODE = . RDWR } , 0 ) catch | err | switch ( err ) {
error . PathAlreadyExists = > unreachable ,
error . NoSpaceLeft = > unreachable ,
error . FileTooBig = > unreachable ,
error . DeviceBusy = > unreachable ,
error . FileLocksNotSupported = > unreachable ,
error . BadPathName = > unreachable , // Windows-only
error . WouldBlock = > unreachable ,
error . NetworkNotFound = > unreachable , // Windows-only
else = > | e | return e ,
}
else
undefined ;
defer {
if ( any_ignore ) posix . close ( dev_null_fd ) ;
}
2024-05-23 14:10:03 -07:00
const prog_pipe : [ 2 ] posix . fd_t = p : {
2024-05-23 17:01:06 -07:00
if ( self . progress_node . index == . none ) {
2024-05-23 14:10:03 -07:00
break : p . { - 1 , - 1 } ;
} else {
2024-05-26 01:50:54 +01:00
// We use CLOEXEC for the same reason as in `pipe_flags`.
break : p try posix . pipe2 ( . { . NONBLOCK = true , . CLOEXEC = true } ) ;
2024-05-23 14:10:03 -07:00
}
} ;
errdefer destroyPipe ( prog_pipe ) ;
2024-05-23 11:25:41 -07:00
var arena_allocator = std . heap . ArenaAllocator . init ( self . allocator ) ;
defer arena_allocator . deinit ( ) ;
const arena = arena_allocator . allocator ( ) ;
// The POSIX standard does not allow malloc() between fork() and execve(),
// and `self.allocator` may be a libc allocator.
// I have personally observed the child process deadlocking when it tries
// to call malloc() due to a heap allocation between fork() and execve(),
// in musl v1.1.24.
// Additionally, we want to reduce the number of possible ways things
// can fail between fork() and execve().
// Therefore, we do all the allocation for the execve() before the fork().
// This means we must do the null-termination of argv and env vars here.
const argv_buf = try arena . allocSentinel ( ? [ * : 0 ] const u8 , self . argv . len , null ) ;
for ( self . argv , 0 .. ) | arg , i | argv_buf [ i ] = ( try arena . dupeZ ( u8 , arg ) ) . ptr ;
2024-05-23 20:22:58 -07:00
const prog_fileno = 3 ;
2024-05-26 11:10:45 -07:00
comptime assert ( @max ( posix . STDIN_FILENO , posix . STDOUT_FILENO , posix . STDERR_FILENO ) + 1 == prog_fileno ) ;
2024-05-23 20:22:58 -07:00
2024-05-23 14:10:03 -07:00
const envp : [ * : null ] const ? [ * : 0 ] const u8 = m : {
2024-05-23 20:22:58 -07:00
const prog_fd : i32 = if ( prog_pipe [ 1 ] == - 1 ) - 1 else prog_fileno ;
2024-05-23 11:25:41 -07:00
if ( self . env_map ) | env_map | {
2024-05-23 20:22:58 -07:00
break : m ( try process . createEnvironFromMap ( arena , env_map , . {
. zig_progress_fd = prog_fd ,
2024-05-23 14:10:03 -07:00
} ) ) . ptr ;
2024-05-23 11:25:41 -07:00
} else if ( builtin . link_libc ) {
2024-05-23 20:22:58 -07:00
break : m ( try process . createEnvironFromExisting ( arena , std . c . environ , . {
. zig_progress_fd = prog_fd ,
} ) ) . ptr ;
2024-05-23 11:25:41 -07:00
} else if ( builtin . output_mode == . Exe ) {
// Then we have Zig start code and this works.
2024-05-23 20:22:58 -07:00
// TODO type-safety for null-termination of `os.environ`.
break : m ( try process . createEnvironFromExisting ( arena , @ptrCast ( std . os . environ . ptr ) , . {
. zig_progress_fd = prog_fd ,
} ) ) . ptr ;
2024-05-23 11:25:41 -07:00
} else {
// TODO come up with a solution for this.
@compileError ( " missing std lib enhancement: ChildProcess implementation has no way to collect the environment variables to forward to the child process " ) ;
}
} ;
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
// This pipe communicates to the parent errors in the child between `fork` and `execvpe`.
// It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.
const err_pipe : [ 2 ] posix . fd_t = try posix . pipe2 ( . { . CLOEXEC = true } ) ;
2024-05-23 11:25:41 -07:00
errdefer destroyPipe ( err_pipe ) ;
const pid_result = try posix . fork ( ) ;
if ( pid_result == 0 ) {
// we are the child
setUpChildIo ( self . stdin_behavior , stdin_pipe [ 0 ] , posix . STDIN_FILENO , dev_null_fd ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
setUpChildIo ( self . stdout_behavior , stdout_pipe [ 1 ] , posix . STDOUT_FILENO , dev_null_fd ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
setUpChildIo ( self . stderr_behavior , stderr_pipe [ 1 ] , posix . STDERR_FILENO , dev_null_fd ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
2024-05-23 20:22:58 -07:00
2024-05-23 11:25:41 -07:00
if ( self . cwd_dir ) | cwd | {
posix . fchdir ( cwd . fd ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
} else if ( self . cwd ) | cwd | {
posix . chdir ( cwd ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
}
2024-05-27 15:55:32 -07:00
// Must happen after fchdir above, the cwd file descriptor might be
// equal to prog_fileno and be clobbered by this dup2 call.
if ( prog_pipe [ 1 ] != - 1 ) posix . dup2 ( prog_pipe [ 1 ] , prog_fileno ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
2024-05-23 11:25:41 -07:00
if ( self . gid ) | gid | {
posix . setregid ( gid , gid ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
}
if ( self . uid ) | uid | {
posix . setreuid ( uid , uid ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
}
2024-07-09 21:30:46 +09:00
if ( self . pgid ) | pid | {
posix . setpgid ( 0 , pid ) catch | err | forkChildErrReport ( err_pipe [ 1 ] , err ) ;
}
2024-05-23 11:25:41 -07:00
const err = switch ( self . expand_arg0 ) {
. expand = > posix . execvpeZ_expandArg0 ( . expand , argv_buf . ptr [ 0 ] .? , argv_buf . ptr , envp ) ,
. no_expand = > posix . execvpeZ_expandArg0 ( . no_expand , argv_buf . ptr [ 0 ] .? , argv_buf . ptr , envp ) ,
} ;
forkChildErrReport ( err_pipe [ 1 ] , err ) ;
}
// we are the parent
std.process.Child: add `waitForSpawn`
`std.Build.Step.Run` makes the very reasonable assumption that
`error.InvalidExe` will be reported on `spawn` if it will happen.
However, this property does not currently hold on POSIX targets. This
is, through a slightly convoluted series of events, partially
responsible for the sporadic `BrokenPipe` errors we've been seeing more
and more in CI runs.
Making `spawn` wait for the child to exec in the POSIX path introduces
a block of up to 400us. So, instead of doing that, we add a new API for
this particular case: `waitForSpawn`. This function is a nop on Windows,
but on POSIX it blocks until the child successfully (or otherwise) calls
`execvpe`, and reports the error if necessary. `std.Build.Step.Run`
calls this function, so that it can get `error.InvalidExe` when it wants
it.
I'm not convinced that this API is optimal. However, I think this entire
API needs to be either heavily refactored or straight-up redesigned
(related: #22504), so I'm not too worried about hitting the perfect API:
I'd rather just fix this bug for now, and figure out the long-term goal
a bit later.
2025-01-10 10:27:32 +00:00
errdefer comptime unreachable ; // The child is forked; we must not error from now on
posix . close ( err_pipe [ 1 ] ) ; // make sure only the child holds the write end open
self . err_pipe = err_pipe [ 0 ] ;
2024-05-23 11:25:41 -07:00
const pid : i32 = @intCast ( pid_result ) ;
2024-05-23 17:01:06 -07:00
if ( self . stdin_behavior == . Pipe ) {
self . stdin = . { . handle = stdin_pipe [ 1 ] } ;
2024-05-23 11:25:41 -07:00
} else {
self . stdin = null ;
}
2024-05-23 17:01:06 -07:00
if ( self . stdout_behavior == . Pipe ) {
self . stdout = . { . handle = stdout_pipe [ 0 ] } ;
2024-05-23 11:25:41 -07:00
} else {
self . stdout = null ;
}
2024-05-23 17:01:06 -07:00
if ( self . stderr_behavior == . Pipe ) {
self . stderr = . { . handle = stderr_pipe [ 0 ] } ;
2024-05-23 11:25:41 -07:00
} else {
self . stderr = null ;
}
self . id = pid ;
self . term = null ;
2024-05-23 17:01:06 -07:00
if ( self . stdin_behavior == . Pipe ) {
2024-05-23 11:25:41 -07:00
posix . close ( stdin_pipe [ 0 ] ) ;
}
2024-05-23 17:01:06 -07:00
if ( self . stdout_behavior == . Pipe ) {
2024-05-23 11:25:41 -07:00
posix . close ( stdout_pipe [ 1 ] ) ;
}
2024-05-23 17:01:06 -07:00
if ( self . stderr_behavior == . Pipe ) {
2024-05-23 11:25:41 -07:00
posix . close ( stderr_pipe [ 1 ] ) ;
}
2024-05-23 17:01:06 -07:00
2024-05-23 20:22:58 -07:00
if ( prog_pipe [ 1 ] != - 1 ) {
posix . close ( prog_pipe [ 1 ] ) ;
}
2024-05-23 17:01:06 -07:00
self . progress_node . setIpcFd ( prog_pipe [ 0 ] ) ;
2024-05-23 11:25:41 -07:00
}
fn spawnWindows ( self : * ChildProcess ) SpawnError ! void {
var saAttr = windows . SECURITY_ATTRIBUTES {
. nLength = @sizeOf ( windows . SECURITY_ATTRIBUTES ) ,
. bInheritHandle = windows . TRUE ,
. lpSecurityDescriptor = null ,
} ;
const any_ignore = ( self . stdin_behavior == StdIo . Ignore or self . stdout_behavior == StdIo . Ignore or self . stderr_behavior == StdIo . Ignore ) ;
const nul_handle = if ( any_ignore )
// "\Device\Null" or "\??\NUL"
windows . OpenFile ( & [ _ ] u16 { '\\' , 'D' , 'e' , 'v' , 'i' , 'c' , 'e' , '\\' , 'N' , 'u' , 'l' , 'l' } , . {
. access_mask = windows . GENERIC_READ | windows . GENERIC_WRITE | windows . SYNCHRONIZE ,
. share_access = windows . FILE_SHARE_READ | windows . FILE_SHARE_WRITE | windows . FILE_SHARE_DELETE ,
. sa = & saAttr ,
. creation = windows . OPEN_EXISTING ,
} ) catch | err | switch ( err ) {
error . PathAlreadyExists = > return error . Unexpected , // not possible for "NUL"
error . PipeBusy = > return error . Unexpected , // not possible for "NUL"
2024-11-27 14:33:29 -07:00
error . NoDevice = > return error . Unexpected , // not possible for "NUL"
2024-05-23 11:25:41 -07:00
error . FileNotFound = > return error . Unexpected , // not possible for "NUL"
error . AccessDenied = > return error . Unexpected , // not possible for "NUL"
error . NameTooLong = > return error . Unexpected , // not possible for "NUL"
error . WouldBlock = > return error . Unexpected , // not possible for "NUL"
error . NetworkNotFound = > return error . Unexpected , // not possible for "NUL"
error . AntivirusInterference = > return error . Unexpected , // not possible for "NUL"
else = > | e | return e ,
}
else
undefined ;
defer {
if ( any_ignore ) posix . close ( nul_handle ) ;
}
var g_hChildStd_IN_Rd : ? windows . HANDLE = null ;
var g_hChildStd_IN_Wr : ? windows . HANDLE = null ;
switch ( self . stdin_behavior ) {
StdIo . Pipe = > {
try windowsMakePipeIn ( & g_hChildStd_IN_Rd , & g_hChildStd_IN_Wr , & saAttr ) ;
} ,
StdIo . Ignore = > {
g_hChildStd_IN_Rd = nul_handle ;
} ,
StdIo . Inherit = > {
g_hChildStd_IN_Rd = windows . GetStdHandle ( windows . STD_INPUT_HANDLE ) catch null ;
} ,
StdIo . Close = > {
g_hChildStd_IN_Rd = null ;
} ,
}
errdefer if ( self . stdin_behavior == StdIo . Pipe ) {
windowsDestroyPipe ( g_hChildStd_IN_Rd , g_hChildStd_IN_Wr ) ;
} ;
var g_hChildStd_OUT_Rd : ? windows . HANDLE = null ;
var g_hChildStd_OUT_Wr : ? windows . HANDLE = null ;
switch ( self . stdout_behavior ) {
StdIo . Pipe = > {
try windowsMakeAsyncPipe ( & g_hChildStd_OUT_Rd , & g_hChildStd_OUT_Wr , & saAttr ) ;
} ,
StdIo . Ignore = > {
g_hChildStd_OUT_Wr = nul_handle ;
} ,
StdIo . Inherit = > {
g_hChildStd_OUT_Wr = windows . GetStdHandle ( windows . STD_OUTPUT_HANDLE ) catch null ;
} ,
StdIo . Close = > {
g_hChildStd_OUT_Wr = null ;
} ,
}
errdefer if ( self . stdout_behavior == StdIo . Pipe ) {
windowsDestroyPipe ( g_hChildStd_OUT_Rd , g_hChildStd_OUT_Wr ) ;
} ;
var g_hChildStd_ERR_Rd : ? windows . HANDLE = null ;
var g_hChildStd_ERR_Wr : ? windows . HANDLE = null ;
switch ( self . stderr_behavior ) {
StdIo . Pipe = > {
try windowsMakeAsyncPipe ( & g_hChildStd_ERR_Rd , & g_hChildStd_ERR_Wr , & saAttr ) ;
} ,
StdIo . Ignore = > {
g_hChildStd_ERR_Wr = nul_handle ;
} ,
StdIo . Inherit = > {
g_hChildStd_ERR_Wr = windows . GetStdHandle ( windows . STD_ERROR_HANDLE ) catch null ;
} ,
StdIo . Close = > {
g_hChildStd_ERR_Wr = null ;
} ,
}
errdefer if ( self . stderr_behavior == StdIo . Pipe ) {
windowsDestroyPipe ( g_hChildStd_ERR_Rd , g_hChildStd_ERR_Wr ) ;
} ;
var siStartInfo = windows . STARTUPINFOW {
. cb = @sizeOf ( windows . STARTUPINFOW ) ,
. hStdError = g_hChildStd_ERR_Wr ,
. hStdOutput = g_hChildStd_OUT_Wr ,
. hStdInput = g_hChildStd_IN_Rd ,
. dwFlags = windows . STARTF_USESTDHANDLES ,
. lpReserved = null ,
. lpDesktop = null ,
. lpTitle = null ,
. dwX = 0 ,
. dwY = 0 ,
. dwXSize = 0 ,
. dwYSize = 0 ,
. dwXCountChars = 0 ,
. dwYCountChars = 0 ,
. dwFillAttribute = 0 ,
. wShowWindow = 0 ,
. cbReserved2 = 0 ,
. lpReserved2 = null ,
} ;
var piProcInfo : windows . PROCESS_INFORMATION = undefined ;
const cwd_w = if ( self . cwd ) | cwd | try unicode . wtf8ToWtf16LeAllocZ ( self . allocator , cwd ) else null ;
defer if ( cwd_w ) | cwd | self . allocator . free ( cwd ) ;
const cwd_w_ptr = if ( cwd_w ) | cwd | cwd . ptr else null ;
const maybe_envp_buf = if ( self . env_map ) | env_map | try process . createWindowsEnvBlock ( self . allocator , env_map ) else null ;
defer if ( maybe_envp_buf ) | envp_buf | self . allocator . free ( envp_buf ) ;
const envp_ptr = if ( maybe_envp_buf ) | envp_buf | envp_buf . ptr else null ;
const app_name_wtf8 = self . argv [ 0 ] ;
const app_name_is_absolute = fs . path . isAbsolute ( app_name_wtf8 ) ;
// the cwd set in ChildProcess is in effect when choosing the executable path
// to match posix semantics
var cwd_path_w_needs_free = false ;
const cwd_path_w = x : {
// If the app name is absolute, then we need to use its dirname as the cwd
if ( app_name_is_absolute ) {
cwd_path_w_needs_free = true ;
const dir = fs . path . dirname ( app_name_wtf8 ) .? ;
break : x try unicode . wtf8ToWtf16LeAllocZ ( self . allocator , dir ) ;
} else if ( self . cwd ) | cwd | {
cwd_path_w_needs_free = true ;
break : x try unicode . wtf8ToWtf16LeAllocZ ( self . allocator , cwd ) ;
} else {
break : x & [ _ : 0 ] u16 { } ; // empty for cwd
}
} ;
defer if ( cwd_path_w_needs_free ) self . allocator . free ( cwd_path_w ) ;
// If the app name has more than just a filename, then we need to separate that
// into the basename and dirname and use the dirname as an addition to the cwd
// path. This is because NtQueryDirectoryFile cannot accept FileName params with
// path separators.
const app_basename_wtf8 = fs . path . basename ( app_name_wtf8 ) ;
// If the app name is absolute, then the cwd will already have the app's dirname in it,
// so only populate app_dirname if app name is a relative path with > 0 path separators.
const maybe_app_dirname_wtf8 = if ( ! app_name_is_absolute ) fs . path . dirname ( app_name_wtf8 ) else null ;
const app_dirname_w : ? [ : 0 ] u16 = x : {
if ( maybe_app_dirname_wtf8 ) | app_dirname_wtf8 | {
break : x try unicode . wtf8ToWtf16LeAllocZ ( self . allocator , app_dirname_wtf8 ) ;
}
break : x null ;
} ;
defer if ( app_dirname_w != null ) self . allocator . free ( app_dirname_w .? ) ;
const app_name_w = try unicode . wtf8ToWtf16LeAllocZ ( self . allocator , app_basename_wtf8 ) ;
defer self . allocator . free ( app_name_w ) ;
2025-03-02 20:27:17 -07:00
const flags : windows . CreateProcessFlags = . {
. create_suspended = self . start_suspended ,
. create_unicode_environment = true ,
. create_no_window = self . create_no_window ,
} ;
2024-05-23 11:25:41 -07:00
run : {
const PATH : [ : 0 ] const u16 = process . getenvW ( unicode . utf8ToUtf16LeStringLiteral ( " PATH " ) ) orelse & [ _ : 0 ] u16 { } ;
const PATHEXT : [ : 0 ] const u16 = process . getenvW ( unicode . utf8ToUtf16LeStringLiteral ( " PATHEXT " ) ) orelse & [ _ : 0 ] u16 { } ;
// In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules
// and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously
// constructed arguments.
//
// We'll need to wait until we're actually trying to run the command to know for sure
// if the resolved command has the `.bat` or `.cmd` extension, so we defer actually
// serializing the command line until we determine how it should be serialized.
var cmd_line_cache = WindowsCommandLineCache . init ( self . allocator , self . argv ) ;
defer cmd_line_cache . deinit ( ) ;
2024-09-02 22:32:21 +01:00
var app_buf : std . ArrayListUnmanaged ( u16 ) = . empty ;
2024-05-23 11:25:41 -07:00
defer app_buf . deinit ( self . allocator ) ;
try app_buf . appendSlice ( self . allocator , app_name_w ) ;
2024-09-02 22:32:21 +01:00
var dir_buf : std . ArrayListUnmanaged ( u16 ) = . empty ;
2024-05-23 11:25:41 -07:00
defer dir_buf . deinit ( self . allocator ) ;
if ( cwd_path_w . len > 0 ) {
try dir_buf . appendSlice ( self . allocator , cwd_path_w ) ;
}
if ( app_dirname_w ) | app_dir | {
if ( dir_buf . items . len > 0 ) try dir_buf . append ( self . allocator , fs . path . sep ) ;
try dir_buf . appendSlice ( self . allocator , app_dir ) ;
}
if ( dir_buf . items . len > 0 ) {
// Need to normalize the path, openDirW can't handle things like double backslashes
const normalized_len = windows . normalizePath ( u16 , dir_buf . items ) catch return error . BadPathName ;
dir_buf . shrinkRetainingCapacity ( normalized_len ) ;
}
2025-03-02 20:27:17 -07:00
windowsCreateProcessPathExt ( self . allocator , & dir_buf , & app_buf , PATHEXT , & cmd_line_cache , envp_ptr , cwd_w_ptr , flags , & siStartInfo , & piProcInfo ) catch | no_path_err | {
2024-05-23 11:25:41 -07:00
const original_err = switch ( no_path_err ) {
// argv[0] contains unsupported characters that will never resolve to a valid exe.
error . InvalidArg0 = > return error . FileNotFound ,
error . FileNotFound , error . InvalidExe , error . AccessDenied = > | e | e ,
error . UnrecoverableInvalidExe = > return error . InvalidExe ,
else = > | e | return e ,
} ;
// If the app name had path separators, that disallows PATH searching,
// and there's no need to search the PATH if the app name is absolute.
// We still search the path if the cwd is absolute because of the
// "cwd set in ChildProcess is in effect when choosing the executable path
// to match posix semantics" behavior--we don't want to skip searching
// the PATH just because we were trying to set the cwd of the child process.
if ( app_dirname_w != null or app_name_is_absolute ) {
return original_err ;
}
var it = mem . tokenizeScalar ( u16 , PATH , ';' ) ;
while ( it . next ( ) ) | search_path | {
dir_buf . clearRetainingCapacity ( ) ;
try dir_buf . appendSlice ( self . allocator , search_path ) ;
// Need to normalize the path, some PATH values can contain things like double
// backslashes which openDirW can't handle
const normalized_len = windows . normalizePath ( u16 , dir_buf . items ) catch continue ;
dir_buf . shrinkRetainingCapacity ( normalized_len ) ;
2025-03-02 20:27:17 -07:00
if ( windowsCreateProcessPathExt ( self . allocator , & dir_buf , & app_buf , PATHEXT , & cmd_line_cache , envp_ptr , cwd_w_ptr , flags , & siStartInfo , & piProcInfo ) ) {
2024-05-23 11:25:41 -07:00
break : run ;
} else | err | switch ( err ) {
// argv[0] contains unsupported characters that will never resolve to a valid exe.
error . InvalidArg0 = > return error . FileNotFound ,
error . FileNotFound , error . AccessDenied , error . InvalidExe = > continue ,
error . UnrecoverableInvalidExe = > return error . InvalidExe ,
else = > | e | return e ,
}
} else {
return original_err ;
}
} ;
}
if ( g_hChildStd_IN_Wr ) | h | {
self . stdin = File { . handle = h } ;
} else {
self . stdin = null ;
}
if ( g_hChildStd_OUT_Rd ) | h | {
self . stdout = File { . handle = h } ;
} else {
self . stdout = null ;
}
if ( g_hChildStd_ERR_Rd ) | h | {
self . stderr = File { . handle = h } ;
} else {
self . stderr = null ;
}
self . id = piProcInfo . hProcess ;
self . thread_handle = piProcInfo . hThread ;
self . term = null ;
if ( self . stdin_behavior == StdIo . Pipe ) {
posix . close ( g_hChildStd_IN_Rd .? ) ;
}
if ( self . stderr_behavior == StdIo . Pipe ) {
posix . close ( g_hChildStd_ERR_Wr .? ) ;
}
if ( self . stdout_behavior == StdIo . Pipe ) {
posix . close ( g_hChildStd_OUT_Wr .? ) ;
}
}
fn setUpChildIo ( stdio : StdIo , pipe_fd : i32 , std_fileno : i32 , dev_null_fd : i32 ) ! void {
switch ( stdio ) {
. Pipe = > try posix . dup2 ( pipe_fd , std_fileno ) ,
. Close = > posix . close ( std_fileno ) ,
. Inherit = > { } ,
. Ignore = > try posix . dup2 ( dev_null_fd , std_fileno ) ,
}
}
fn destroyPipe ( pipe : [ 2 ] posix . fd_t ) void {
2024-05-23 14:10:03 -07:00
if ( pipe [ 0 ] != - 1 ) posix . close ( pipe [ 0 ] ) ;
2024-05-23 11:25:41 -07:00
if ( pipe [ 0 ] != pipe [ 1 ] ) posix . close ( pipe [ 1 ] ) ;
}
// Child of fork calls this to report an error to the fork parent.
// Then the child exits.
fn forkChildErrReport ( fd : i32 , err : ChildProcess . SpawnError ) noreturn {
writeIntFd ( fd , @as ( ErrInt , @intFromError ( err ) ) ) catch { } ;
// If we're linking libc, some naughty applications may have registered atexit handlers
// which we really do not want to run in the fork child. I caught LLVM doing this and
// it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne,
// "Why'd you have to go and make things so complicated?"
if ( builtin . link_libc ) {
// The _exit(2) function does nothing but make the exit syscall, unlike exit(3)
std . c . _exit ( 1 ) ;
}
posix . exit ( 1 ) ;
}
fn writeIntFd ( fd : i32 , value : ErrInt ) ! void {
const file : File = . { . handle = fd } ;
file . writer ( ) . writeInt ( u64 , @intCast ( value ) , . little ) catch return error . SystemResources ;
}
fn readIntFd ( fd : i32 ) ! ErrInt {
const file : File = . { . handle = fd } ;
return @intCast ( file . reader ( ) . readInt ( u64 , . little ) catch return error . SystemResources ) ;
}
const ErrInt = std . meta . Int ( . unsigned , @sizeOf ( anyerror ) * 8 ) ;
/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path.
/// Note: `app_buf` should not contain any leading path separators.
/// Note: If the dir is the cwd, dir_buf should be empty (len = 0).
fn windowsCreateProcessPathExt (
allocator : mem . Allocator ,
dir_buf : * std . ArrayListUnmanaged ( u16 ) ,
app_buf : * std . ArrayListUnmanaged ( u16 ) ,
pathext : [ : 0 ] const u16 ,
cmd_line_cache : * WindowsCommandLineCache ,
envp_ptr : ? [ * ] u16 ,
cwd_ptr : ? [ * : 0 ] u16 ,
2025-03-02 20:27:17 -07:00
flags : windows . CreateProcessFlags ,
2024-05-23 11:25:41 -07:00
lpStartupInfo : * windows . STARTUPINFOW ,
lpProcessInformation : * windows . PROCESS_INFORMATION ,
) ! void {
const app_name_len = app_buf . items . len ;
const dir_path_len = dir_buf . items . len ;
if ( app_name_len == 0 ) return error . FileNotFound ;
defer app_buf . shrinkRetainingCapacity ( app_name_len ) ;
defer dir_buf . shrinkRetainingCapacity ( dir_path_len ) ;
// The name of the game here is to avoid CreateProcessW calls at all costs,
// and only ever try calling it when we have a real candidate for execution.
// Secondarily, we want to minimize the number of syscalls used when checking
// for each PATHEXT-appended version of the app name.
//
// An overview of the technique used:
// - Open the search directory for iteration (either cwd or a path from PATH)
// - Use NtQueryDirectoryFile with a wildcard filename of `<app name>*` to
// check if anything that could possibly match either the unappended version
// of the app name or any of the versions with a PATHEXT value appended exists.
// - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early
// without needing to use PATHEXT at all.
//
// This allows us to use a <open dir, NtQueryDirectoryFile, close dir> sequence
// for any directory that doesn't contain any possible matches, instead of having
// to use a separate look up for each individual filename combination (unappended +
// each PATHEXT appended). For directories where the wildcard *does* match something,
// we iterate the matches and take note of any that are either the unappended version,
// or a version with a supported PATHEXT appended. We then try calling CreateProcessW
// with the found versions in the appropriate order.
var dir = dir : {
// needs to be null-terminated
try dir_buf . append ( allocator , 0 ) ;
defer dir_buf . shrinkRetainingCapacity ( dir_path_len ) ;
const dir_path_z = dir_buf . items [ 0 .. dir_buf . items . len - 1 : 0 ] ;
const prefixed_path = try windows . wToPrefixedFileW ( null , dir_path_z ) ;
break : dir fs . cwd ( ) . openDirW ( prefixed_path . span ( ) . ptr , . { . iterate = true } ) catch
return error . FileNotFound ;
} ;
defer dir . close ( ) ;
// Add wildcard and null-terminator
try app_buf . append ( allocator , '*' ) ;
try app_buf . append ( allocator , 0 ) ;
const app_name_wildcard = app_buf . items [ 0 .. app_buf . items . len - 1 : 0 ] ;
// This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries
// returned per NtQueryDirectoryFile call.
var file_information_buf : [ 2048 ] u8 align ( @alignOf ( windows . FILE_DIRECTORY_INFORMATION ) ) = undefined ;
const file_info_maximum_single_entry_size = @sizeOf ( windows . FILE_DIRECTORY_INFORMATION ) + ( windows . NAME_MAX * 2 ) ;
if ( file_information_buf . len < file_info_maximum_single_entry_size ) {
@compileError ( " file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry " ) ;
}
var io_status : windows . IO_STATUS_BLOCK = undefined ;
2024-08-28 02:35:53 +01:00
const num_supported_pathext = @typeInfo ( WindowsExtension ) . @" enum " . fields . len ;
2024-05-23 11:25:41 -07:00
var pathext_seen = [ _ ] bool { false } ** num_supported_pathext ;
var any_pathext_seen = false ;
var unappended_exists = false ;
// Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions
// of the app_name we should try to spawn.
// Note: This is necessary because the order of the files returned is filesystem-dependent:
// On NTFS, `blah.exe*` will always return `blah.exe` first if it exists.
// On FAT32, it's possible for something like `blah.exe.obj` to be returned first.
while ( true ) {
const app_name_len_bytes = std . math . cast ( u16 , app_name_wildcard . len * 2 ) orelse return error . NameTooLong ;
var app_name_unicode_string = windows . UNICODE_STRING {
. Length = app_name_len_bytes ,
. MaximumLength = app_name_len_bytes ,
. Buffer = @constCast ( app_name_wildcard . ptr ) ,
} ;
const rc = windows . ntdll . NtQueryDirectoryFile (
dir . fd ,
null ,
null ,
null ,
& io_status ,
& file_information_buf ,
file_information_buf . len ,
. FileDirectoryInformation ,
windows . FALSE , // single result
& app_name_unicode_string ,
windows . FALSE , // restart iteration
) ;
// If we get nothing with the wildcard, then we can just bail out
// as we know appending PATHEXT will not yield anything.
switch ( rc ) {
. SUCCESS = > { } ,
. NO_SUCH_FILE = > return error . FileNotFound ,
. NO_MORE_FILES = > break ,
. ACCESS_DENIED = > return error . AccessDenied ,
else = > return windows . unexpectedStatus ( rc ) ,
}
// According to the docs, this can only happen if there is not enough room in the
// buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry.
// Therefore, this condition should not be possible to hit with the buffer size we use.
std . debug . assert ( io_status . Information != 0 ) ;
var it = windows . FileInformationIterator ( windows . FILE_DIRECTORY_INFORMATION ) { . buf = & file_information_buf } ;
while ( it . next ( ) ) | info | {
// Skip directories
if ( info . FileAttributes & windows . FILE_ATTRIBUTE_DIRECTORY != 0 ) continue ;
const filename = @as ( [ * ] u16 , @ptrCast ( & info . FileName ) ) [ 0 .. info . FileNameLength / 2 ] ;
// Because all results start with the app_name since we're using the wildcard `app_name*`,
// if the length is equal to app_name then this is an exact match
if ( filename . len == app_name_len ) {
// Note: We can't break early here because it's possible that the unappended version
// fails to spawn, in which case we still want to try the PATHEXT appended versions.
unappended_exists = true ;
} else if ( windowsCreateProcessSupportsExtension ( filename [ app_name_len .. ] ) ) | pathext_ext | {
pathext_seen [ @intFromEnum ( pathext_ext ) ] = true ;
any_pathext_seen = true ;
}
}
}
const unappended_err = unappended : {
if ( unappended_exists ) {
if ( dir_path_len != 0 ) switch ( dir_buf . items [ dir_buf . items . len - 1 ] ) {
'/' , '\\' = > { } ,
else = > try dir_buf . append ( allocator , fs . path . sep ) ,
} ;
try dir_buf . appendSlice ( allocator , app_buf . items [ 0 .. app_name_len ] ) ;
try dir_buf . append ( allocator , 0 ) ;
const full_app_name = dir_buf . items [ 0 .. dir_buf . items . len - 1 : 0 ] ;
const is_bat_or_cmd = bat_or_cmd : {
const app_name = app_buf . items [ 0 .. app_name_len ] ;
const ext_start = std . mem . lastIndexOfScalar ( u16 , app_name , '.' ) orelse break : bat_or_cmd false ;
const ext = app_name [ ext_start .. ] ;
const ext_enum = windowsCreateProcessSupportsExtension ( ext ) orelse break : bat_or_cmd false ;
switch ( ext_enum ) {
. cmd , . bat = > break : bat_or_cmd true ,
else = > break : bat_or_cmd false ,
}
} ;
const cmd_line_w = if ( is_bat_or_cmd )
try cmd_line_cache . scriptCommandLine ( full_app_name )
else
try cmd_line_cache . commandLine ( ) ;
const app_name_w = if ( is_bat_or_cmd )
try cmd_line_cache . cmdExePath ( )
else
full_app_name ;
2025-03-02 20:27:17 -07:00
if ( windowsCreateProcess ( app_name_w . ptr , cmd_line_w . ptr , envp_ptr , cwd_ptr , flags , lpStartupInfo , lpProcessInformation ) ) | _ | {
2024-05-23 11:25:41 -07:00
return ;
} else | err | switch ( err ) {
error . FileNotFound ,
error . AccessDenied ,
= > break : unappended err ,
error . InvalidExe = > {
// On InvalidExe, if the extension of the app name is .exe then
// it's treated as an unrecoverable error. Otherwise, it'll be
// skipped as normal.
const app_name = app_buf . items [ 0 .. app_name_len ] ;
const ext_start = std . mem . lastIndexOfScalar ( u16 , app_name , '.' ) orelse break : unappended err ;
const ext = app_name [ ext_start .. ] ;
if ( windows . eqlIgnoreCaseWTF16 ( ext , unicode . utf8ToUtf16LeStringLiteral ( " .EXE " ) ) ) {
return error . UnrecoverableInvalidExe ;
}
break : unappended err ;
} ,
else = > return err ,
}
}
break : unappended error . FileNotFound ;
} ;
if ( ! any_pathext_seen ) return unappended_err ;
// Now try any PATHEXT appended versions that we've seen
var ext_it = mem . tokenizeScalar ( u16 , pathext , ';' ) ;
while ( ext_it . next ( ) ) | ext | {
const ext_enum = windowsCreateProcessSupportsExtension ( ext ) orelse continue ;
if ( ! pathext_seen [ @intFromEnum ( ext_enum ) ] ) continue ;
dir_buf . shrinkRetainingCapacity ( dir_path_len ) ;
if ( dir_path_len != 0 ) switch ( dir_buf . items [ dir_buf . items . len - 1 ] ) {
'/' , '\\' = > { } ,
else = > try dir_buf . append ( allocator , fs . path . sep ) ,
} ;
try dir_buf . appendSlice ( allocator , app_buf . items [ 0 .. app_name_len ] ) ;
try dir_buf . appendSlice ( allocator , ext ) ;
try dir_buf . append ( allocator , 0 ) ;
const full_app_name = dir_buf . items [ 0 .. dir_buf . items . len - 1 : 0 ] ;
const is_bat_or_cmd = switch ( ext_enum ) {
. cmd , . bat = > true ,
else = > false ,
} ;
const cmd_line_w = if ( is_bat_or_cmd )
try cmd_line_cache . scriptCommandLine ( full_app_name )
else
try cmd_line_cache . commandLine ( ) ;
const app_name_w = if ( is_bat_or_cmd )
try cmd_line_cache . cmdExePath ( )
else
full_app_name ;
2025-03-02 20:27:17 -07:00
if ( windowsCreateProcess ( app_name_w . ptr , cmd_line_w . ptr , envp_ptr , cwd_ptr , flags , lpStartupInfo , lpProcessInformation ) ) | _ | {
2024-05-23 11:25:41 -07:00
return ;
} else | err | switch ( err ) {
error . FileNotFound = > continue ,
error . AccessDenied = > continue ,
error . InvalidExe = > {
// On InvalidExe, if the extension of the app name is .exe then
// it's treated as an unrecoverable error. Otherwise, it'll be
// skipped as normal.
if ( windows . eqlIgnoreCaseWTF16 ( ext , unicode . utf8ToUtf16LeStringLiteral ( " .EXE " ) ) ) {
return error . UnrecoverableInvalidExe ;
}
continue ;
} ,
else = > return err ,
}
}
return unappended_err ;
}
fn windowsCreateProcess (
app_name : [ * : 0 ] u16 ,
cmd_line : [ * : 0 ] u16 ,
envp_ptr : ? [ * ] u16 ,
cwd_ptr : ? [ * : 0 ] u16 ,
2025-03-02 20:27:17 -07:00
flags : windows . CreateProcessFlags ,
2024-05-23 11:25:41 -07:00
lpStartupInfo : * windows . STARTUPINFOW ,
lpProcessInformation : * windows . PROCESS_INFORMATION ,
) ! void {
// TODO the docs for environment pointer say:
// > A pointer to the environment block for the new process. If this parameter
// > is NULL, the new process uses the environment of the calling process.
// > ...
// > An environment block can contain either Unicode or ANSI characters. If
// > the environment block pointed to by lpEnvironment contains Unicode
// > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
// > If this parameter is NULL and the environment block of the parent process
// > contains Unicode characters, you must also ensure that dwCreationFlags
// > includes CREATE_UNICODE_ENVIRONMENT.
// This seems to imply that we have to somehow know whether our process parent passed
// CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
// Since we do not know this information that would imply that we must not pass NULL
// for the parameter.
// However this would imply that programs compiled with -DUNICODE could not pass
// environment variables to programs that were not, which seems unlikely.
// More investigation is needed.
return windows . CreateProcessW (
app_name ,
cmd_line ,
null ,
null ,
windows . TRUE ,
2025-03-02 20:27:17 -07:00
flags ,
2024-05-23 11:25:41 -07:00
@as ( ? * anyopaque , @ptrCast ( envp_ptr ) ) ,
cwd_ptr ,
lpStartupInfo ,
lpProcessInformation ,
) ;
}
fn windowsMakePipeIn ( rd : * ? windows . HANDLE , wr : * ? windows . HANDLE , sattr : * const windows . SECURITY_ATTRIBUTES ) ! void {
var rd_h : windows . HANDLE = undefined ;
var wr_h : windows . HANDLE = undefined ;
try windows . CreatePipe ( & rd_h , & wr_h , sattr ) ;
errdefer windowsDestroyPipe ( rd_h , wr_h ) ;
try windows . SetHandleInformation ( wr_h , windows . HANDLE_FLAG_INHERIT , 0 ) ;
rd .* = rd_h ;
wr .* = wr_h ;
}
fn windowsDestroyPipe ( rd : ? windows . HANDLE , wr : ? windows . HANDLE ) void {
if ( rd ) | h | posix . close ( h ) ;
if ( wr ) | h | posix . close ( h ) ;
}
fn windowsMakeAsyncPipe ( rd : * ? windows . HANDLE , wr : * ? windows . HANDLE , sattr : * const windows . SECURITY_ATTRIBUTES ) ! void {
var tmp_bufw : [ 128 ] u16 = undefined ;
// Anonymous pipes are built upon Named pipes.
// https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe
// Asynchronous (overlapped) read and write operations are not supported by anonymous pipes.
// https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations
const pipe_path = blk : {
var tmp_buf : [ 128 ] u8 = undefined ;
// Forge a random path for the pipe.
const pipe_path = std . fmt . bufPrintZ (
& tmp_buf ,
" \\ \\ . \\ pipe \\ zig-childprocess-{d}-{d} " ,
. { windows . GetCurrentProcessId ( ) , pipe_name_counter . fetchAdd ( 1 , . monotonic ) } ,
) catch unreachable ;
const len = std . unicode . wtf8ToWtf16Le ( & tmp_bufw , pipe_path ) catch unreachable ;
tmp_bufw [ len ] = 0 ;
break : blk tmp_bufw [ 0 .. len : 0 ] ;
} ;
// Create the read handle that can be used with overlapped IO ops.
const read_handle = windows . kernel32 . CreateNamedPipeW (
pipe_path . ptr ,
windows . PIPE_ACCESS_INBOUND | windows . FILE_FLAG_OVERLAPPED ,
windows . PIPE_TYPE_BYTE ,
1 ,
4096 ,
4096 ,
0 ,
sattr ,
) ;
if ( read_handle == windows . INVALID_HANDLE_VALUE ) {
2024-07-15 14:49:51 -03:00
switch ( windows . GetLastError ( ) ) {
2024-05-23 11:25:41 -07:00
else = > | err | return windows . unexpectedError ( err ) ,
}
}
errdefer posix . close ( read_handle ) ;
var sattr_copy = sattr .* ;
const write_handle = windows . kernel32 . CreateFileW (
pipe_path . ptr ,
windows . GENERIC_WRITE ,
0 ,
& sattr_copy ,
windows . OPEN_EXISTING ,
windows . FILE_ATTRIBUTE_NORMAL ,
null ,
) ;
if ( write_handle == windows . INVALID_HANDLE_VALUE ) {
2024-07-15 14:49:51 -03:00
switch ( windows . GetLastError ( ) ) {
2024-05-23 11:25:41 -07:00
else = > | err | return windows . unexpectedError ( err ) ,
}
}
errdefer posix . close ( write_handle ) ;
try windows . SetHandleInformation ( read_handle , windows . HANDLE_FLAG_INHERIT , 0 ) ;
rd .* = read_handle ;
wr .* = write_handle ;
}
var pipe_name_counter = std . atomic . Value ( u32 ) . init ( 1 ) ;
2024-06-18 07:08:12 +02:00
/// File name extensions supported natively by `CreateProcess()` on Windows.
// Should be kept in sync with `windowsCreateProcessSupportsExtension`.
pub const WindowsExtension = enum {
2024-05-23 11:25:41 -07:00
bat ,
cmd ,
com ,
exe ,
} ;
/// Case-insensitive WTF-16 lookup
2024-06-18 07:08:12 +02:00
fn windowsCreateProcessSupportsExtension ( ext : [ ] const u16 ) ? WindowsExtension {
2024-05-23 11:25:41 -07:00
if ( ext . len != 4 ) return null ;
const State = enum {
start ,
dot ,
b ,
ba ,
c ,
cm ,
co ,
e ,
ex ,
} ;
var state : State = . start ;
for ( ext ) | c | switch ( state ) {
. start = > switch ( c ) {
'.' = > state = . dot ,
else = > return null ,
} ,
. dot = > switch ( c ) {
'b' , 'B' = > state = . b ,
'c' , 'C' = > state = . c ,
'e' , 'E' = > state = . e ,
else = > return null ,
} ,
. b = > switch ( c ) {
'a' , 'A' = > state = . ba ,
else = > return null ,
} ,
. c = > switch ( c ) {
'm' , 'M' = > state = . cm ,
'o' , 'O' = > state = . co ,
else = > return null ,
} ,
. e = > switch ( c ) {
'x' , 'X' = > state = . ex ,
else = > return null ,
} ,
. ba = > switch ( c ) {
't' , 'T' = > return . bat ,
else = > return null ,
} ,
. cm = > switch ( c ) {
'd' , 'D' = > return . cmd ,
else = > return null ,
} ,
. co = > switch ( c ) {
'm' , 'M' = > return . com ,
else = > return null ,
} ,
. ex = > switch ( c ) {
'e' , 'E' = > return . exe ,
else = > return null ,
} ,
} ;
return null ;
}
test windowsCreateProcessSupportsExtension {
2024-06-18 07:08:12 +02:00
try std . testing . expectEqual ( WindowsExtension . exe , windowsCreateProcessSupportsExtension ( & [ _ ] u16 { '.' , 'e' , 'X' , 'e' } ) .? ) ;
2024-05-23 11:25:41 -07:00
try std . testing . expect ( windowsCreateProcessSupportsExtension ( & [ _ ] u16 { '.' , 'e' , 'X' , 'e' , 'c' } ) == null ) ;
}
/// Serializes argv into a WTF-16 encoded command-line string for use with CreateProcessW.
///
/// Serialization is done on-demand and the result is cached in order to allow for:
/// - Only serializing the particular type of command line needed (`.bat`/`.cmd`
/// command line serialization is different from `.exe`/etc)
/// - Reusing the serialized command lines if necessary (i.e. if the execution
/// of a command fails and the PATH is going to be continued to be searched
/// for more candidates)
const WindowsCommandLineCache = struct {
cmd_line : ? [ : 0 ] u16 = null ,
script_cmd_line : ? [ : 0 ] u16 = null ,
cmd_exe_path : ? [ : 0 ] u16 = null ,
argv : [ ] const [ ] const u8 ,
allocator : mem . Allocator ,
fn init ( allocator : mem . Allocator , argv : [ ] const [ ] const u8 ) WindowsCommandLineCache {
return . {
. allocator = allocator ,
. argv = argv ,
} ;
}
fn deinit ( self : * WindowsCommandLineCache ) void {
if ( self . cmd_line ) | cmd_line | self . allocator . free ( cmd_line ) ;
if ( self . script_cmd_line ) | script_cmd_line | self . allocator . free ( script_cmd_line ) ;
if ( self . cmd_exe_path ) | cmd_exe_path | self . allocator . free ( cmd_exe_path ) ;
}
fn commandLine ( self : * WindowsCommandLineCache ) ! [ : 0 ] u16 {
if ( self . cmd_line == null ) {
self . cmd_line = try argvToCommandLineWindows ( self . allocator , self . argv ) ;
}
return self . cmd_line .? ;
}
/// Not cached, since the path to the batch script will change during PATH searching.
/// `script_path` should be as qualified as possible, e.g. if the PATH is being searched,
/// then script_path should include both the search path and the script filename
/// (this allows avoiding cmd.exe having to search the PATH again).
fn scriptCommandLine ( self : * WindowsCommandLineCache , script_path : [ ] const u16 ) ! [ : 0 ] u16 {
if ( self . script_cmd_line ) | v | self . allocator . free ( v ) ;
self . script_cmd_line = try argvToScriptCommandLineWindows (
self . allocator ,
script_path ,
self . argv [ 1 .. ] ,
) ;
return self . script_cmd_line .? ;
}
fn cmdExePath ( self : * WindowsCommandLineCache ) ! [ : 0 ] u16 {
if ( self . cmd_exe_path == null ) {
self . cmd_exe_path = try windowsCmdExePath ( self . allocator ) ;
}
return self . cmd_exe_path .? ;
}
} ;
2024-05-26 09:36:34 -07:00
/// Returns the absolute path of `cmd.exe` within the Windows system directory.
/// The caller owns the returned slice.
2024-05-23 11:25:41 -07:00
fn windowsCmdExePath ( allocator : mem . Allocator ) error { OutOfMemory , Unexpected } ! [ : 0 ] u16 {
var buf = try std . ArrayListUnmanaged ( u16 ) . initCapacity ( allocator , 128 ) ;
errdefer buf . deinit ( allocator ) ;
while ( true ) {
const unused_slice = buf . unusedCapacitySlice ( ) ;
// TODO: Get the system directory from PEB.ReadOnlyStaticServerData
const len = windows . kernel32 . GetSystemDirectoryW ( @ptrCast ( unused_slice ) , @intCast ( unused_slice . len ) ) ;
if ( len == 0 ) {
2024-07-15 14:49:51 -03:00
switch ( windows . GetLastError ( ) ) {
2024-05-23 11:25:41 -07:00
else = > | err | return windows . unexpectedError ( err ) ,
}
}
if ( len > unused_slice . len ) {
try buf . ensureUnusedCapacity ( allocator , len ) ;
} else {
buf . items . len = len ;
break ;
}
}
switch ( buf . items [ buf . items . len - 1 ] ) {
'/' , '\\' = > { } ,
else = > try buf . append ( allocator , fs . path . sep ) ,
}
try buf . appendSlice ( allocator , unicode . utf8ToUtf16LeStringLiteral ( " cmd.exe " ) ) ;
return try buf . toOwnedSliceSentinel ( allocator , 0 ) ;
}
const ArgvToCommandLineError = error { OutOfMemory , InvalidWtf8 , InvalidArg0 } ;
/// Serializes `argv` to a Windows command-line string suitable for passing to a child process and
/// parsing by the `CommandLineToArgvW` algorithm. The caller owns the returned slice.
2024-05-26 09:36:34 -07:00
///
/// To avoid arbitrary command execution, this function should not be used when spawning `.bat`/`.cmd` scripts.
/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
///
/// When executing `.bat`/`.cmd` scripts, use `argvToScriptCommandLineWindows` instead.
2024-05-23 11:25:41 -07:00
fn argvToCommandLineWindows (
allocator : mem . Allocator ,
argv : [ ] const [ ] const u8 ,
) ArgvToCommandLineError ! [ : 0 ] u16 {
var buf = std . ArrayList ( u8 ) . init ( allocator ) ;
defer buf . deinit ( ) ;
if ( argv . len != 0 ) {
const arg0 = argv [ 0 ] ;
// The first argument must be quoted if it contains spaces or ASCII control characters
// (excluding DEL). It also follows special quoting rules where backslashes have no special
// interpretation, which makes it impossible to pass certain first arguments containing
// double quotes to a child process without characters from the first argument leaking into
// subsequent ones (which could have security implications).
//
// Empty arguments technically don't need quotes, but we quote them anyway for maximum
// compatibility with different implementations of the 'CommandLineToArgvW' algorithm.
//
// Double quotes are illegal in paths on Windows, so for the sake of simplicity we reject
// all first arguments containing double quotes, even ones that we could theoretically
// serialize in unquoted form.
var needs_quotes = arg0 . len == 0 ;
for ( arg0 ) | c | {
if ( c <= ' ' ) {
needs_quotes = true ;
} else if ( c == '"' ) {
return error . InvalidArg0 ;
}
}
if ( needs_quotes ) {
try buf . append ( '"' ) ;
try buf . appendSlice ( arg0 ) ;
try buf . append ( '"' ) ;
} else {
try buf . appendSlice ( arg0 ) ;
}
for ( argv [ 1 .. ] ) | arg | {
try buf . append ( ' ' ) ;
// Subsequent arguments must be quoted if they contain spaces, tabs or double quotes,
// or if they are empty. For simplicity and for maximum compatibility with different
// implementations of the 'CommandLineToArgvW' algorithm, we also quote all ASCII
// control characters (again, excluding DEL).
needs_quotes = for ( arg ) | c | {
if ( c <= ' ' or c == '"' ) {
break true ;
}
} else arg . len == 0 ;
if ( ! needs_quotes ) {
try buf . appendSlice ( arg ) ;
continue ;
}
try buf . append ( '"' ) ;
var backslash_count : usize = 0 ;
for ( arg ) | byte | {
switch ( byte ) {
'\\' = > {
backslash_count += 1 ;
} ,
'"' = > {
try buf . appendNTimes ( '\\' , backslash_count * 2 + 1 ) ;
try buf . append ( '"' ) ;
backslash_count = 0 ;
} ,
else = > {
try buf . appendNTimes ( '\\' , backslash_count ) ;
try buf . append ( byte ) ;
backslash_count = 0 ;
} ,
}
}
try buf . appendNTimes ( '\\' , backslash_count * 2 ) ;
try buf . append ( '"' ) ;
}
}
return try unicode . wtf8ToWtf16LeAllocZ ( allocator , buf . items ) ;
}
test argvToCommandLineWindows {
const t = testArgvToCommandLineWindows ;
try t ( & . {
\\C:\Program Files\zig\zig.exe
,
\\run
,
\\.\src\main.zig
,
\\-target
,
\\x86_64-windows-gnu
,
\\-O
,
\\ReleaseSafe
,
\\--
,
\\--emoji=🗿
,
\\--eval=new Regex("Dwayne \"The Rock\" Johnson")
,
} ,
\\"C:\Program Files\zig\zig.exe" run .\src\main.zig -target x86_64-windows-gnu -O ReleaseSafe -- --emoji=🗿 "--eval=new Regex(\"Dwayne \\\"The Rock\\\" Johnson\")"
) ;
try t ( & . { } , " " ) ;
try t ( & . { " " } , " \" \" " ) ;
try t ( & . { " " } , " \" \" " ) ;
try t ( & . { " \t " } , " \" \t \" " ) ;
try t ( & . { " \x07 " } , " \" \x07 \" " ) ;
try t ( & . { " 🦎 " } , " 🦎 " ) ;
try t (
& . { " zig " , " aa aa " , " bb \t bb " , " cc \n cc " , " dd \r \n dd " , " ee \x7F ee " } ,
" zig \" aa aa \" \" bb \t bb \" \" cc \n cc \" \" dd \r \n dd \" ee \x7F ee " ,
) ;
try t (
& . { " \\ \\ foo bar \\ foo bar \\ " , " \\ \\ zig zag \\ zig zag \\ " } ,
" \" \\ \\ foo bar \\ foo bar \\ \" \" \\ \\ zig zag \\ zig zag \\ \\ \" " ,
) ;
try std . testing . expectError (
error . InvalidArg0 ,
argvToCommandLineWindows ( std . testing . allocator , & . { " \" quotes \" quotes \" " } ) ,
) ;
try std . testing . expectError (
error . InvalidArg0 ,
argvToCommandLineWindows ( std . testing . allocator , & . { " quotes \" quotes " } ) ,
) ;
try std . testing . expectError (
error . InvalidArg0 ,
argvToCommandLineWindows ( std . testing . allocator , & . { " q u o t e s \" q u o t e s " } ) ,
) ;
}
fn testArgvToCommandLineWindows ( argv : [ ] const [ ] const u8 , expected_cmd_line : [ ] const u8 ) ! void {
const cmd_line_w = try argvToCommandLineWindows ( std . testing . allocator , argv ) ;
defer std . testing . allocator . free ( cmd_line_w ) ;
const cmd_line = try unicode . wtf16LeToWtf8Alloc ( std . testing . allocator , cmd_line_w ) ;
defer std . testing . allocator . free ( cmd_line ) ;
try std . testing . expectEqualStrings ( expected_cmd_line , cmd_line ) ;
}
const ArgvToScriptCommandLineError = error {
OutOfMemory ,
InvalidWtf8 ,
/// NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
/// within arguments when executing a `.bat`/`.cmd` script.
/// - NUL/LF signifiies end of arguments, so anything afterwards
/// would be lost after execution.
/// - CR is stripped by `cmd.exe`, so any CR codepoints
/// would be lost after execution.
InvalidBatchScriptArg ,
} ;
/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific
/// escaping rules. The caller owns the returned slice.
///
/// Escapes `argv` using the suggested mitigation against arbitrary command execution from:
/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
2024-05-26 09:36:34 -07:00
///
/// The return of this function will look like
/// `cmd.exe /d /e:ON /v:OFF /c "<escaped command line>"`
/// and should be used as the `lpCommandLine` of `CreateProcessW`, while the
/// return of `windowsCmdExePath` should be used as `lpApplicationName`.
///
/// Should only be used when spawning `.bat`/`.cmd` scripts, see `argvToCommandLineWindows` otherwise.
/// The `.bat`/`.cmd` file must be known to both have the `.bat`/`.cmd` extension and exist on the filesystem.
2024-05-23 11:25:41 -07:00
fn argvToScriptCommandLineWindows (
allocator : mem . Allocator ,
/// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD.
/// The script must have been verified to exist at this path before calling this function.
script_path : [ ] const u16 ,
/// Arguments, not including the script name itself. Expected to be encoded as WTF-8.
script_args : [ ] const [ ] const u8 ,
) ArgvToScriptCommandLineError ! [ : 0 ] u16 {
var buf = try std . ArrayList ( u8 ) . initCapacity ( allocator , 64 ) ;
defer buf . deinit ( ) ;
// `/d` disables execution of AutoRun commands.
// `/e:ON` and `/v:OFF` are needed for BatBadBut mitigation:
// > If delayed expansion is enabled via the registry value DelayedExpansion,
// > it must be disabled by explicitly calling cmd.exe with the /V:OFF option.
// > Escaping for % requires the command extension to be enabled.
// > If it’ s disabled via the registry value EnableExtensions, it must be enabled with the /E:ON option.
// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
buf . appendSliceAssumeCapacity ( " cmd.exe /d /e:ON /v:OFF /c \" " ) ;
// Always quote the path to the script arg
buf . appendAssumeCapacity ( '"' ) ;
// We always want the path to the batch script to include a path separator in order to
// avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary
// command execution mitigation, we just know exactly what script we want to execute
// at this point, and potentially making cmd.exe re-find it is unnecessary.
//
// If the script path does not have a path separator, then we know its relative to CWD and
// we can just put `.\` in the front.
if ( mem . indexOfAny ( u16 , script_path , & [ _ ] u16 { mem . nativeToLittle ( u16 , '\\' ) , mem . nativeToLittle ( u16 , '/' ) } ) == null ) {
try buf . appendSlice ( " . \\ " ) ;
}
// Note that we don't do any escaping/mitigations for this argument, since the relevant
// characters (", %, etc) are illegal in file paths and this function should only be called
// with script paths that have been verified to exist.
try unicode . wtf16LeToWtf8ArrayList ( & buf , script_path ) ;
buf . appendAssumeCapacity ( '"' ) ;
for ( script_args ) | arg | {
// Literal carriage returns get stripped when run through cmd.exe
// and NUL/newlines act as 'end of command.' Because of this, it's basically
// always a mistake to include these characters in argv, so it's
// an error condition in order to ensure that the return of this
// function can always roundtrip through cmd.exe.
if ( std . mem . indexOfAny ( u8 , arg , " \x00 \r \n " ) != null ) {
return error . InvalidBatchScriptArg ;
}
// Separate args with a space.
try buf . append ( ' ' ) ;
// Need to quote if the argument is empty (otherwise the arg would just be lost)
// or if the last character is a `\`, since then something like "%~2" in a .bat
// script would cause the closing " to be escaped which we don't want.
var needs_quotes = arg . len == 0 or arg [ arg . len - 1 ] == '\\' ;
if ( ! needs_quotes ) {
for ( arg ) | c | {
switch ( c ) {
// Known good characters that don't need to be quoted
'A' .. . 'Z' , 'a' .. . 'z' , '0' .. . '9' , '#' , '$' , '*' , '+' , '-' , '.' , '/' , ':' , '?' , '@' , '\\' , '_' = > { } ,
// When in doubt, quote
else = > {
needs_quotes = true ;
break ;
} ,
}
}
}
if ( needs_quotes ) {
try buf . append ( '"' ) ;
}
var backslashes : usize = 0 ;
for ( arg ) | c | {
switch ( c ) {
'\\' = > {
backslashes += 1 ;
} ,
'"' = > {
try buf . appendNTimes ( '\\' , backslashes ) ;
try buf . append ( '"' ) ;
backslashes = 0 ;
} ,
// Replace `%` with `%%cd:~,%`.
//
// cmd.exe allows extracting a substring from an environment
// variable with the syntax: `%foo:~<start_index>,<end_index>%`.
// Therefore, `%cd:~,%` will always expand to an empty string
// since both the start and end index are blank, and it is assumed
// that `%cd%` is always available since it is a built-in variable
// that corresponds to the current directory.
//
// This means that replacing `%foo%` with `%%cd:~,%foo%%cd:~,%`
// will stop `%foo%` from being expanded and *after* expansion
// we'll still be left with `%foo%` (the literal string).
'%' = > {
// the trailing `%` is appended outside the switch
try buf . appendSlice ( " %%cd:~, " ) ;
backslashes = 0 ;
} ,
else = > {
backslashes = 0 ;
} ,
}
try buf . append ( c ) ;
}
if ( needs_quotes ) {
try buf . appendNTimes ( '\\' , backslashes ) ;
try buf . append ( '"' ) ;
}
}
try buf . append ( '"' ) ;
return try unicode . wtf8ToWtf16LeAllocZ ( allocator , buf . items ) ;
}