feature. See also
. The project being documented here (as the example) is the Zig library itself.
Threaded.processSpawnWindows
fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child
File
Code
fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const any_ignore =
options.stdin == .ignore or
options.stdout == .ignore or
options.stderr == .ignore;
const nul_handle = if (any_ignore) try getNulDevice(t) else undefined;
const any_inherit =
options.stdin == .inherit or
options.stdout == .inherit or
options.stderr == .inherit;
const peb = if (any_inherit) windows.peb() else undefined;
const stdin_pipe = if (options.stdin == .pipe) try t.windowsCreatePipe(.{
.server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } },
.client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } },
.outbound = true,
}) else undefined;
errdefer if (options.stdin == .pipe) for (stdin_pipe) |handle| windows.CloseHandle(handle);
const stdout_pipe = if (options.stdout == .pipe) try t.windowsCreatePipe(.{
.server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .ASYNCHRONOUS } },
.client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } },
.inbound = true,
}) else undefined;
errdefer if (options.stdout == .pipe) for (stdout_pipe) |handle| windows.CloseHandle(handle);
const stderr_pipe = if (options.stderr == .pipe) try t.windowsCreatePipe(.{
.server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .ASYNCHRONOUS } },
.client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .SYNCHRONOUS_NONALERT } },
.inbound = true,
}) else undefined;
errdefer if (options.stderr == .pipe) for (stderr_pipe) |handle| windows.CloseHandle(handle);
const prog_pipe = if (options.progress_node.index != .none) try t.windowsCreatePipe(.{
.server = .{ .attributes = .{ .INHERIT = false }, .mode = .{ .IO = .ASYNCHRONOUS } },
.client = .{ .attributes = .{ .INHERIT = true }, .mode = .{ .IO = .ASYNCHRONOUS } },
.inbound = true,
.quota = std.Progress.max_packet_len * 2,
}) else undefined;
errdefer if (options.progress_node.index != .none) for (prog_pipe) |handle| windows.CloseHandle(handle);
var siStartInfo: windows.STARTUPINFOW = .{
.cb = @sizeOf(windows.STARTUPINFOW),
.dwFlags = windows.STARTF_USESTDHANDLES,
.hStdInput = switch (options.stdin) {
.inherit => peb.ProcessParameters.hStdInput,
.file => |file| try OpenFile(&.{}, .{
.access_mask = .{
.STANDARD = .{ .SYNCHRONIZE = true },
.GENERIC = .{ .READ = true },
},
.dir = file.handle,
.sa = &.{
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
.lpSecurityDescriptor = null,
.bInheritHandle = .TRUE,
},
.creation = .OPEN,
}),
.ignore => nul_handle,
.pipe => stdin_pipe[1],
.close => null,
},
.hStdOutput = switch (options.stdout) {
.inherit => peb.ProcessParameters.hStdOutput,
.file => |file| try OpenFile(&.{}, .{
.access_mask = .{
.STANDARD = .{ .SYNCHRONIZE = true },
.GENERIC = .{ .WRITE = true },
},
.dir = file.handle,
.sa = &.{
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
.lpSecurityDescriptor = null,
.bInheritHandle = .TRUE,
},
.creation = .OPEN,
}),
.ignore => nul_handle,
.pipe => stdout_pipe[1],
.close => null,
},
.hStdError = switch (options.stderr) {
.inherit => peb.ProcessParameters.hStdError,
.file => |file| try OpenFile(&.{}, .{
.access_mask = .{
.STANDARD = .{ .SYNCHRONIZE = true },
.GENERIC = .{ .WRITE = true },
},
.dir = file.handle,
.sa = &.{
.nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
.lpSecurityDescriptor = null,
.bInheritHandle = .TRUE,
},
.creation = .OPEN,
}),
.ignore => nul_handle,
.pipe => stderr_pipe[1],
.close => null,
},
.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;
var arena_allocator = std.heap.ArenaAllocator.init(t.allocator);
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
const cwd_w = cwd_w: {
switch (options.cwd) {
.inherit => break :cwd_w null,
.dir => |cwd_dir| {
var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1);
const dir_path = try GetFinalPathNameByHandle(
cwd_dir.handle,
.{},
dir_path_buffer[0..windows.PATH_MAX_WIDE],
);
dir_path_buffer[dir_path.len] = 0;
dir_path_buffer = try arena.realloc(dir_path_buffer, dir_path.len + 1);
break :cwd_w dir_path_buffer[0..dir_path.len :0];
},
.path => |cwd| {
break :cwd_w try std.unicode.wtf8ToWtf16LeAllocZ(arena, cwd);
},
}
};
const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null;
const env_block = env_block: {
const prog_handle = if (options.progress_node.index != .none)
prog_pipe[1]
else
windows.INVALID_HANDLE_VALUE;
if (options.environ_map) |environ_map| break :env_block try environ_map.createWindowsBlock(arena, .{
.zig_progress_handle = prog_handle,
});
break :env_block try t.environ.process_environ.createWindowsBlock(arena, .{
.zig_progress_handle = if (options.progress_node.index != .none) prog_pipe[1] else windows.INVALID_HANDLE_VALUE,
});
};
const app_name_wtf8 = options.argv[0];
const app_name_is_absolute = Dir.path.isAbsolute(app_name_wtf8);
// path to match POSIX semantics.
const cwd_path_w = x: {
if (app_name_is_absolute) {
const dir = Dir.path.dirname(app_name_wtf8).?;
break :x try std.unicode.wtf8ToWtf16LeAllocZ(arena, dir);
} else if (cwd_w) |cwd| {
break :x cwd;
} else {
break :x &[_:0]u16{};
}
};
// that into the basename and dirname and use the dirname as an addition to
// the cwd path. This is because NtQueryDirectoryFile cannot accept
// FileName params with path separators.
const app_basename_wtf8 = Dir.path.basename(app_name_wtf8);
// so only populate app_dirname if app name is a relative path with > 0 path separators.
const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) Dir.path.dirname(app_name_wtf8) else null;
const app_dirname_w: ?[:0]u16 = x: {
if (maybe_app_dirname_wtf8) |app_dirname_wtf8| {
break :x try std.unicode.wtf8ToWtf16LeAllocZ(arena, app_dirname_wtf8);
}
break :x null;
};
const app_name_w = try std.unicode.wtf8ToWtf16LeAllocZ(arena, app_basename_wtf8);
const flags: windows.CreateProcessFlags = .{
.create_suspended = options.start_suspended,
.create_unicode_environment = true,
.create_no_window = options.create_no_window,
};
run: {
const env_strings: WindowsEnvironStrings = .scan();
const PATH = env_strings.PATH orelse &[_:0]u16{};
const PATHEXT = env_strings.PATHEXT orelse &[_:0]u16{};
// 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(arena, options.argv);
var app_buf: std.ArrayList(u16) = .empty;
try app_buf.appendSlice(arena, app_name_w);
var dir_buf: std.ArrayList(u16) = .empty;
if (cwd_path_w.len > 0) {
try dir_buf.appendSlice(arena, cwd_path_w);
}
if (app_dirname_w) |app_dir| {
if (dir_buf.items.len > 0) try dir_buf.append(arena, Dir.path.sep);
try dir_buf.appendSlice(arena, app_dir);
}
windowsCreateProcessPathExt(
arena,
&dir_buf,
&app_buf,
PATHEXT,
&cmd_line_cache,
env_block,
cwd_w_ptr,
flags,
&siStartInfo,
&piProcInfo,
) catch |no_path_err| {
const original_err = switch (no_path_err) {
error.InvalidArg0 => return error.FileNotFound,
error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e,
error.UnrecoverableInvalidExe => return error.InvalidExe,
else => |e| return e,
};
// 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 provided by options is in effect when choosing the executable path
// to match posix semantics" behavior--we don't want to skip searching
// the PATH just because we were trying to set the cwd of the child process.
if (app_dirname_w != null or app_name_is_absolute) {
return original_err;
}
var it = std.mem.tokenizeScalar(u16, PATH, ';');
while (it.next()) |search_path| {
dir_buf.clearRetainingCapacity();
try dir_buf.appendSlice(arena, search_path);
if (windowsCreateProcessPathExt(
arena,
&dir_buf,
&app_buf,
PATHEXT,
&cmd_line_cache,
env_block,
cwd_w_ptr,
flags,
&siStartInfo,
&piProcInfo,
)) {
break :run;
} else |err| switch (err) {
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 (options.progress_node.index != .none) {
windows.CloseHandle(prog_pipe[1]);
options.progress_node.setIpcFile(t, .{ .handle = prog_pipe[0], .flags = .{ .nonblocking = true } });
}
return .{
.id = piProcInfo.hProcess,
.thread_handle = piProcInfo.hThread,
.stdin = stdin: switch (options.stdin) {
.file => {
windows.CloseHandle(siStartInfo.hStdInput.?);
break :stdin null;
},
.pipe => {
windows.CloseHandle(stdin_pipe[1]);
break :stdin .{ .handle = stdin_pipe[0], .flags = .{ .nonblocking = false } };
},
else => null,
},
.stdout = stdout: switch (options.stdout) {
.file => {
windows.CloseHandle(siStartInfo.hStdOutput.?);
break :stdout null;
},
.pipe => {
windows.CloseHandle(stdout_pipe[1]);
break :stdout .{ .handle = stdout_pipe[0], .flags = .{ .nonblocking = true } };
},
else => null,
},
.stderr = stderr: switch (options.stderr) {
.file => {
windows.CloseHandle(siStartInfo.hStdError.?);
break :stderr null;
},
.pipe => {
windows.CloseHandle(stderr_pipe[1]);
break :stderr .{ .handle = stderr_pipe[0], .flags = .{ .nonblocking = true } };
},
else => null,
},
.request_resource_usage_statistics = options.request_resource_usage_statistics,
};
}