std.process.Child: detached process spawning

This commit is contained in:
Vojtech Antosik 2024-07-29 00:38:37 +02:00
parent 38e0f049c5
commit f4e01b0e48
No known key found for this signature in database
GPG Key ID: 1B4CFBC7E5EE59A7
6 changed files with 103 additions and 5 deletions

View File

@ -9168,6 +9168,7 @@ pub extern "c" fn setregid(rgid: gid_t, egid: gid_t) c_int;
pub extern "c" fn setresuid(ruid: uid_t, euid: uid_t, suid: uid_t) c_int;
pub extern "c" fn setresgid(rgid: gid_t, egid: gid_t, sgid: gid_t) c_int;
pub extern "c" fn setpgid(pid: pid_t, pgid: pid_t) c_int;
pub extern "c" fn setsid() pid_t;
pub extern "c" fn malloc(usize) ?*anyopaque;
pub extern "c" fn realloc(?*anyopaque, usize) ?*anyopaque;
@ -9351,6 +9352,7 @@ pub extern "c" fn setlogmask(maskpri: c_int) c_int;
pub extern "c" fn if_nametoindex([*:0]const u8) c_int;
pub extern "c" fn getpid() pid_t;
pub extern "c" fn getsid(pid: pid_t) pid_t;
/// These are implementation defined but share identical values in at least musl and glibc:
/// - https://git.musl-libc.org/cgit/musl/tree/include/locale.h?id=ab31e9d6a0fa7c5c408856c89df2dfb12c344039#n18

View File

@ -1581,6 +1581,10 @@ pub fn setsid() pid_t {
return @bitCast(@as(u32, @truncate(syscall0(.setsid))));
}
pub fn getsid(pid: pid_t) pid_t {
return @bitCast(@as(u32, @truncate(syscall1(.getsid, @intCast(pid)))));
}
pub fn getpid() pid_t {
return @bitCast(@as(u32, @truncate(syscall0(.getpid))));
}

View File

@ -154,6 +154,12 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN
}
}
pub fn GetProcessId(Process: HANDLE) UnexpectedError!DWORD {
const ret_code = kernel32.GetProcessId(Process);
if (ret_code == 0) return unexpectedError(GetLastError());
return ret_code;
}
pub fn GetCurrentProcess() HANDLE {
const process_pseudo_handle: usize = @bitCast(@as(isize, -1));
return @ptrFromInt(process_pseudo_handle);
@ -1813,6 +1819,12 @@ pub fn VirtualQuery(lpAddress: ?LPVOID, lpBuffer: PMEMORY_BASIC_INFORMATION, dwL
return rc;
}
pub fn GetConsoleProcessList(lpdwProcessList: []DWORD) UnexpectedError!DWORD {
const ret_code = kernel32.GetConsoleProcessList(lpdwProcessList.ptr, @min(lpdwProcessList.len, math.maxInt(DWORD)));
if (ret_code == 0) return unexpectedError(GetLastError());
return ret_code;
}
pub const SetConsoleTextAttributeError = error{Unexpected};
pub fn SetConsoleTextAttribute(hConsoleOutput: HANDLE, wAttributes: WORD) SetConsoleTextAttributeError!void {
@ -3727,6 +3739,7 @@ pub const COORD = extern struct {
Y: SHORT,
};
pub const DETACHED_PROCESS = 8;
pub const CREATE_UNICODE_ENVIRONMENT = 1024;
pub const TLS_OUT_OF_INDEXES = 4294967295;

View File

@ -340,6 +340,8 @@ pub extern "kernel32" fn GetExitCodeProcess(
// TODO: Already a wrapper for this, see `windows.GetCurrentProcess`.
pub extern "kernel32" fn GetCurrentProcess() callconv(WINAPI) HANDLE;
pub extern "kernel32" fn GetProcessId(Process: HANDLE) callconv(WINAPI) DWORD;
// TODO: memcpy peb().ProcessParameters.Environment, mem.span(0). Requires locking the PEB.
pub extern "kernel32" fn GetEnvironmentStringsW() callconv(WINAPI) ?LPWSTR;
@ -519,6 +521,11 @@ pub extern "kernel32" fn ReadConsoleOutputCharacterW(
lpNumberOfCharsRead: *DWORD,
) callconv(windows.WINAPI) BOOL;
pub extern "kernel32" fn GetConsoleProcessList(
lpdwProcessList: [*]DWORD,
dwProcessCount: DWORD,
) callconv(WINAPI) DWORD;
// Memory Mapping/Allocation
// TODO: Wrapper around RtlCreateHeap.

View File

@ -3438,6 +3438,29 @@ pub fn setpgid(pid: pid_t, pgid: pid_t) SetPgidError!void {
}
}
pub const SetSidError = error{PermissionDenied} || UnexpectedError;
pub fn setsid() SetSidError!pid_t {
const res = system.setsid();
switch (errno(@as(isize, res))) {
.SUCCESS => return res,
.PERM => return error.PermissionDenied,
else => |err| return unexpectedErrno(err),
}
}
pub const GetSidError = error{ProcessNotFound} || SetSidError;
pub fn getsid(pid: pid_t) GetSidError!pid_t {
const res = system.getsid(pid);
switch (errno(@as(isize, res))) {
.SUCCESS => return res,
.PERM => return error.PermissionDenied,
.SRCH => return error.ProcessNotFound,
else => |err| return unexpectedErrno(err),
}
}
/// Test whether a file descriptor refers to a terminal.
pub fn isatty(handle: fd_t) bool {
if (native_os == .windows) {

View File

@ -57,6 +57,9 @@ stdin_behavior: StdIo,
stdout_behavior: StdIo,
stderr_behavior: StdIo,
/// Set to spawn a detached process.
detached: bool,
/// 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,
@ -172,6 +175,7 @@ pub const SpawnError = error{
posix.ExecveError ||
posix.SetIdError ||
posix.SetPgidError ||
posix.SetSidError ||
posix.ChangeCurDirError ||
windows.CreateProcessError ||
windows.GetProcessMemoryInfoError ||
@ -215,6 +219,7 @@ pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
.term = null,
.env_map = null,
.cwd = null,
.detached = false,
.uid = if (native_os == .windows or native_os == .wasi) {} else null,
.gid = if (native_os == .windows or native_os == .wasi) {} else null,
.pgid = if (native_os == .windows or native_os == .wasi) {} else null,
@ -658,6 +663,10 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
const pid_result = try posix.fork();
if (pid_result == 0) {
// we are the child
if (self.detached) {
_ = posix.setsid() catch |err| forkChildErrReport(err_pipe[1], err);
}
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);
@ -844,6 +853,10 @@ fn spawnWindows(self: *ChildProcess) SpawnError!void {
.lpReserved2 = null,
};
var piProcInfo: windows.PROCESS_INFORMATION = undefined;
var dwCreationFlags: windows.DWORD = windows.CREATE_UNICODE_ENVIRONMENT;
if (self.detached) {
dwCreationFlags |= windows.DETACHED_PROCESS;
}
const cwd_w = if (self.cwd) |cwd| try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd) else null;
defer if (cwd_w) |cwd| self.allocator.free(cwd);
@ -928,7 +941,7 @@ fn spawnWindows(self: *ChildProcess) SpawnError!void {
dir_buf.shrinkRetainingCapacity(normalized_len);
}
windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| {
windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo, dwCreationFlags) catch |no_path_err| {
const original_err = switch (no_path_err) {
// argv[0] contains unsupported characters that will never resolve to a valid exe.
error.InvalidArg0 => return error.FileNotFound,
@ -956,7 +969,7 @@ fn spawnWindows(self: *ChildProcess) SpawnError!void {
const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue;
dir_buf.shrinkRetainingCapacity(normalized_len);
if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) {
if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo, dwCreationFlags)) {
break :run;
} else |err| switch (err) {
// argv[0] contains unsupported characters that will never resolve to a valid exe.
@ -1057,6 +1070,7 @@ fn windowsCreateProcessPathExt(
cwd_ptr: ?[*:0]u16,
lpStartupInfo: *windows.STARTUPINFOW,
lpProcessInformation: *windows.PROCESS_INFORMATION,
dwCreationFlags: windows.DWORD,
) !void {
const app_name_len = app_buf.items.len;
const dir_path_len = dir_buf.items.len;
@ -1205,7 +1219,7 @@ fn windowsCreateProcessPathExt(
else
full_app_name;
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation, dwCreationFlags)) |_| {
return;
} else |err| switch (err) {
error.FileNotFound,
@ -1260,7 +1274,7 @@ fn windowsCreateProcessPathExt(
else
full_app_name;
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation, dwCreationFlags)) |_| {
return;
} else |err| switch (err) {
error.FileNotFound => continue,
@ -1288,6 +1302,7 @@ fn windowsCreateProcess(
cwd_ptr: ?[*:0]u16,
lpStartupInfo: *windows.STARTUPINFOW,
lpProcessInformation: *windows.PROCESS_INFORMATION,
dwCreationFlags: windows.DWORD,
) !void {
// TODO the docs for environment pointer say:
// > A pointer to the environment block for the new process. If this parameter
@ -1312,7 +1327,7 @@ fn windowsCreateProcess(
null,
null,
windows.TRUE,
windows.CREATE_UNICODE_ENVIRONMENT,
dwCreationFlags,
@as(?*anyopaque, @ptrCast(envp_ptr)),
cwd_ptr,
lpStartupInfo,
@ -1857,3 +1872,37 @@ fn argvToScriptCommandLineWindows(
return try unicode.wtf8ToWtf16LeAllocZ(allocator, buf.items);
}
test "detached child" {
const cmd = if (native_os == .windows)
&.{ "ping", "-n", "1", "-w", "10000", "127.255.255.255" }
else &.{ "sleep", "10" };
var child = ChildProcess.init(cmd, std.testing.allocator);
child.stdin_behavior = .Ignore;
child.stderr_behavior = .Ignore;
child.stdout_behavior = .Pipe;
child.detached = true;
try child.spawn();
if (native_os == .windows) {
const child_pid = try windows.GetProcessId(child.id);
// Give the process some time to actually start doing something.
// If we check the process list immediately, we might be done before
// the new process attaches to the console.
var read_buffer: [1]u8 = undefined;
try std.testing.expectEqual(1, try child.stdout.?.read(&read_buffer));
var proc_buffer: [16]windows.DWORD = undefined;
const proc_count = try windows.GetConsoleProcessList(&proc_buffer);
if (proc_count > 16) return error.ProcessBufferTooSmall;
for (proc_buffer[0..proc_count]) |proc| {
if (proc == child_pid) return error.ProcessAttachedToConsole;
}
} else {
const current_sid = try posix.getsid(0);
const child_sid = try posix.getsid(child.id);
try std.testing.expect(current_sid != child_sid);
}
}