Skip to content

std.process.Child: detached child spawning #20876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/std/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10547,6 +10547,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 getuid() uid_t;
pub extern "c" fn geteuid() uid_t;

Expand Down Expand Up @@ -10737,6 +10738,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;
pub extern "c" fn getppid() pid_t;

/// These are implementation defined but share identical values in at least musl and glibc:
Expand Down
4 changes: 4 additions & 0 deletions lib/std/os/linux.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,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))));
}
Expand Down
1 change: 1 addition & 0 deletions lib/std/os/windows.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3794,6 +3794,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;
Expand Down
23 changes: 23 additions & 0 deletions lib/std/posix.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3505,6 +3505,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),
}
}

pub fn getuid() uid_t {
return system.getuid();
}
Expand Down
24 changes: 23 additions & 1 deletion lib/std/process/Child.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -176,6 +179,7 @@ pub const SpawnError = error{
posix.ExecveError ||
posix.SetIdError ||
posix.SetPgidError ||
posix.SetSidError ||
posix.ChangeCurDirError ||
windows.CreateProcessError ||
windows.GetProcessMemoryInfoError ||
Expand Down Expand Up @@ -219,6 +223,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,
Expand All @@ -232,13 +237,25 @@ pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
};
}

/// Call this if you have no intention of calling `kill` or `wait` to properly
/// dispose of any resources related to the child process.
pub fn deinit(self: *ChildProcess) void {
if (native_os == .windows) {
posix.close(self.thread_handle);
posix.close(self.id);
}
self.cleanupStreams();
}

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`.
/// On success must call `kill` or `wait`. In the case of a detached process,
/// consider using `deinit` instead if you have no intention of synchronizing
/// with the child.
/// After spawning the `id` is available.
pub fn spawn(self: *ChildProcess) SpawnError!void {
if (!process.can_spawn) {
Expand Down Expand Up @@ -618,6 +635,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);
Expand Down Expand Up @@ -862,6 +883,7 @@ fn spawnWindows(self: *ChildProcess) SpawnError!void {
.create_suspended = self.start_suspended,
.create_unicode_environment = true,
.create_no_window = self.create_no_window,
.detached_process = self.detached,
};

run: {
Expand Down
6 changes: 6 additions & 0 deletions test/standalone/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
.child_process = .{
.path = "child_process",
},
.detached_child = .{
.path = "detached_child",
},
.child_spawn_fail = .{
.path = "child_spawn_fail",
},
.embed_generated_file = .{
.path = "embed_generated_file",
},
Expand Down
32 changes: 32 additions & 0 deletions test/standalone/child_spawn_fail/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const std = @import("std");
const builtin = @import("builtin");

pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Test it");
b.default_step = test_step;

const optimize: std.builtin.OptimizeMode = .Debug;
const target = b.graph.host;

if (builtin.os.tag == .wasi) return;

const child = b.addExecutable(.{
.name = "child",
.root_source_file = b.path("child.zig"),
.optimize = optimize,
.target = target,
});

const main = b.addExecutable(.{
.name = "main",
.root_source_file = b.path("main.zig"),
.optimize = optimize,
.target = target,
});

const run = b.addRunArtifact(main);
run.addArtifactArg(child);
run.expectExitCode(0);

test_step.dependOn(&run.step);
}
19 changes: 19 additions & 0 deletions test/standalone/child_spawn_fail/child.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const std = @import("std");

pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
defer if (gpa_state.deinit() == .leak) @panic("leaks were detected");
const gpa = gpa_state.allocator();
var args = try std.process.argsWithAllocator(gpa);
defer args.deinit();
_ = args.next() orelse unreachable; // skip executable name
const sleep_seconds = try std.fmt.parseInt(u32, args.next() orelse unreachable, 0);

const stdout = std.io.getStdOut();
_ = try stdout.write("started");

const end_time = std.time.timestamp() + sleep_seconds;
while (std.time.timestamp() < end_time) {
std.time.sleep(@max(end_time - std.time.timestamp(), 0) * 1_000_000_000);
}
}
38 changes: 38 additions & 0 deletions test/standalone/child_spawn_fail/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const std = @import("std");
const builtin = @import("builtin");

pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
defer if (gpa_state.deinit() == .leak) @panic("memory leak detected");
const gpa = gpa_state.allocator();

var args = try std.process.argsWithAllocator(gpa);
defer args.deinit();
_ = args.next() orelse unreachable; // skip executable name
const child_path = args.next() orelse unreachable;

const argv = &.{""};
var child = std.process.Child.init(argv, gpa);
child.stdin_behavior = .Ignore;
child.stderr_behavior = .Ignore;
child.stdout_behavior = .Pipe;
child.detached = true;
child.pgid = if (builtin.os.tag == .windows) void{} else try std.posix.getsid(0);
defer {
_ = child.kill() catch {};
}

if (child.spawn()) {
if (child.waitForSpawn()) {
return error.SpawnSilencedError;
} else |_| {}
} else |_| {}

child = std.process.Child.init(&.{ child_path, "30" }, gpa);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Inherit;

// this spawn should succeed and return without an error
try child.spawn();
}
32 changes: 32 additions & 0 deletions test/standalone/detached_child/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const std = @import("std");
const builtin = @import("builtin");

pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Test it");
b.default_step = test_step;

const optimize: std.builtin.OptimizeMode = .Debug;
const target = b.graph.host;

if (builtin.os.tag == .wasi) return;

const child = b.addExecutable(.{
.name = "child",
.root_source_file = b.path("child.zig"),
.optimize = optimize,
.target = target,
});

const main = b.addExecutable(.{
.name = "main",
.root_source_file = b.path("main.zig"),
.optimize = optimize,
.target = target,
});

const run = b.addRunArtifact(main);
run.addArtifactArg(child);
run.expectExitCode(0);

test_step.dependOn(&run.step);
}
19 changes: 19 additions & 0 deletions test/standalone/detached_child/child.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const std = @import("std");

pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
defer if (gpa_state.deinit() == .leak) @panic("leaks were detected");
const gpa = gpa_state.allocator();
var args = try std.process.argsWithAllocator(gpa);
defer args.deinit();
_ = args.next() orelse unreachable; // skip executable name
const sleep_seconds = try std.fmt.parseInt(u32, args.next() orelse unreachable, 0);

const stdout = std.io.getStdOut();
_ = try stdout.write("started");

const end_time = std.time.timestamp() + sleep_seconds;
while (std.time.timestamp() < end_time) {
std.time.sleep(@max(end_time - std.time.timestamp(), 0) * 1_000_000_000);
}
}
71 changes: 71 additions & 0 deletions test/standalone/detached_child/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const std = @import("std");
const builtin = @import("builtin");
const windows = std.os.windows;

extern "kernel32" fn GetProcessId(Process: windows.HANDLE) callconv(windows.WINAPI) windows.DWORD;
extern "kernel32" fn GetConsoleProcessList(
lpdwProcessList: [*]windows.DWORD,
dwProcessCount: windows.DWORD,
) callconv(windows.WINAPI) windows.DWORD;

pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
defer if (gpa_state.deinit() == .leak) @panic("memory leak detected");
const gpa = gpa_state.allocator();

var args = try std.process.argsWithAllocator(gpa);
defer args.deinit();
_ = args.next() orelse unreachable; // skip executable name
const child_path = args.next() orelse unreachable;

var child = std.process.Child.init(&.{ child_path, "30" }, gpa);
child.stdin_behavior = .Ignore;
child.stderr_behavior = .Inherit;
child.stdout_behavior = .Pipe;
child.detached = true;
try child.spawn();
try child.waitForSpawn();
defer {
_ = child.kill() catch {};
}

// Give the process some time to actually start doing something before
// checking if it properly detached.
var read_buffer: [1]u8 = undefined;
if (try child.stdout.?.read(&read_buffer) != 1) {
return error.OutputReadFailed;
}

switch (builtin.os.tag) {
.windows => {
const child_pid = GetProcessId(child.id);
if (child_pid == 0) return error.GetProcessIdFailed;

var proc_buffer: []windows.DWORD = undefined;
var proc_count: windows.DWORD = 16;
while (true) {
proc_buffer = try gpa.alloc(windows.DWORD, proc_count);
defer gpa.free(proc_buffer);

proc_count = GetConsoleProcessList(proc_buffer.ptr, @min(proc_buffer.len, std.math.maxInt(windows.DWORD)));
if (proc_count == 0) return error.ConsoleProcessListFailed;

if (proc_count <= proc_buffer.len) {
for (proc_buffer[0..proc_count]) |proc| {
if (proc == child_pid) return error.ProcessAttachedToConsole;
}
break;
}
}
},
else => {
const posix = std.posix;
const current_sid = try posix.getsid(0);
const child_sid = try posix.getsid(child.id);

if (current_sid == child_sid) {
return error.SameChildSession;
}
},
}
}