aboutsummaryrefslogtreecommitdiff
path: root/core/os/process_linux.odin
diff options
context:
space:
mode:
authorJeroen van Rijn <Kelimion@users.noreply.github.com>2026-02-09 15:50:21 +0100
committerJeroen van Rijn <Kelimion@users.noreply.github.com>2026-02-09 15:50:21 +0100
commite7dbabf6681e4e6bcae33398e939c2c9c3cdc879 (patch)
tree91f25462cc2e9f3adf9884720b7f104d4d6d59f5 /core/os/process_linux.odin
parent8ed264680b1f3f94b6aa5176824d4ccadfc30322 (diff)
core:os -> core:os/old && core:os/os2 -> core:os
Diffstat (limited to 'core/os/process_linux.odin')
-rw-r--r--core/os/process_linux.odin868
1 files changed, 868 insertions, 0 deletions
diff --git a/core/os/process_linux.odin b/core/os/process_linux.odin
new file mode 100644
index 000000000..4afd9f3fc
--- /dev/null
+++ b/core/os/process_linux.odin
@@ -0,0 +1,868 @@
+#+build linux
+#+private file
+package os2
+
+import "base:runtime"
+import "base:intrinsics"
+
+import "core:c"
+import "core:time"
+import "core:slice"
+import "core:strings"
+import "core:strconv"
+import "core:sys/unix"
+import "core:sys/linux"
+
+foreign import libc "system:c"
+
+foreign libc {
+ @(link_name="get_nprocs") _unix_get_nprocs :: proc() -> c.int ---
+}
+
+PIDFD_UNASSIGNED :: ~uintptr(0)
+
+@(private="package")
+_get_uid :: proc() -> int {
+ return int(linux.getuid())
+}
+
+@(private="package")
+_get_euid :: proc() -> int {
+ return int(linux.geteuid())
+}
+
+@(private="package")
+_get_gid :: proc() -> int {
+ return int(linux.getgid())
+}
+
+@(private="package")
+_get_egid :: proc() -> int {
+ return int(linux.getegid())
+}
+
+@(private="package")
+_get_pid :: proc() -> int {
+ return int(linux.getpid())
+}
+
+@(private="package")
+_get_ppid :: proc() -> int {
+ return int(linux.getppid())
+}
+
+@(private="package")
+_get_current_thread_id :: proc "contextless" () -> int {
+ return unix.sys_gettid()
+}
+
+@(private="package")
+_get_processor_core_count :: proc() -> int {
+ return int(_unix_get_nprocs())
+}
+
+@(private="package")
+_process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) {
+ temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator })
+
+ dir_fd, errno := linux.open("/proc/", _OPENDIR_FLAGS)
+ #partial switch errno {
+ case .NONE:
+ // okay
+ case .ENOTDIR:
+ err = .Invalid_Dir
+ return
+ case .ENOENT:
+ err = .Not_Exist
+ return
+ case:
+ err = _get_platform_error(errno)
+ return
+ }
+ defer linux.close(dir_fd)
+
+ dynamic_list := make([dynamic]int, temp_allocator) or_return
+
+ buf := make([dynamic]u8, 128, 128, temp_allocator) or_return
+ loop: for {
+ buflen: int
+ buflen, errno = linux.getdents(dir_fd, buf[:])
+ #partial switch errno {
+ case .EINVAL:
+ resize(&buf, len(buf) * 2)
+ continue loop
+ case .NONE:
+ if buflen == 0 { break loop }
+ case:
+ return {}, _get_platform_error(errno)
+ }
+
+ offset: int
+ for d in linux.dirent_iterate_buf(buf[:buflen], &offset) {
+ d_name_str := linux.dirent_name(d)
+
+ if pid, ok := strconv.parse_int(d_name_str); ok {
+ append(&dynamic_list, pid)
+ }
+ }
+ }
+
+ list, err = slice.clone(dynamic_list[:], allocator)
+ return
+}
+
+@(private="package")
+_process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
+ temp_allocator := TEMP_ALLOCATOR_GUARD({ allocator })
+
+ info.pid = pid
+
+ // Use this to make cstrings without copying.
+ path_backing: [48]u8
+ path_builder := strings.builder_from_bytes(path_backing[:])
+
+ strings.write_string(&path_builder, "/proc/")
+ strings.write_int(&path_builder, pid)
+ proc_fd, errno := linux.open(strings.to_cstring(&path_builder) or_return, _OPENDIR_FLAGS)
+ if errno != .NONE {
+ err = _get_platform_error(errno)
+ return
+ }
+ defer linux.close(proc_fd)
+
+ username_if: if .Username in selection {
+ s: linux.Stat
+ if errno = linux.fstat(proc_fd, &s); errno != .NONE {
+ err = _get_platform_error(errno)
+ break username_if
+ }
+
+ passwd_bytes: []u8
+ passwd_err: Error
+ passwd_bytes, passwd_err = _read_entire_pseudo_file_cstring("/etc/passwd", temp_allocator)
+ if passwd_err != nil {
+ err = passwd_err
+ break username_if
+ }
+
+ passwd := string(passwd_bytes)
+ for len(passwd) > 0 {
+ n := strings.index_byte(passwd, ':')
+ if n < 0 {
+ break
+ }
+ username := passwd[:n]
+ passwd = passwd[n+1:]
+
+ // skip password field
+ passwd = passwd[strings.index_byte(passwd, ':') + 1:]
+
+ n = strings.index_byte(passwd, ':')
+ if uid, ok := strconv.parse_int(passwd[:n]); ok && uid == int(s.uid) {
+ info.username = strings.clone(username, allocator) or_return
+ info.fields += {.Username}
+ break
+ } else if !ok {
+ err = .Invalid_File
+ break username_if
+ }
+
+ eol := strings.index_byte(passwd, '\n')
+ if eol < 0 {
+ break
+ }
+ passwd = passwd[eol + 1:]
+ }
+ }
+
+ cmdline_if: if selection & {.Working_Dir, .Command_Line, .Command_Args} != {} {
+ strings.builder_reset(&path_builder)
+ strings.write_string(&path_builder, "/proc/")
+ strings.write_int(&path_builder, pid)
+ strings.write_string(&path_builder, "/cmdline")
+
+ cmdline_bytes, cmdline_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator)
+ if cmdline_err != nil || len(cmdline_bytes) == 0 {
+ err = cmdline_err
+ break cmdline_if
+ }
+ cmdline := string(cmdline_bytes)
+
+ terminator := strings.index_byte(cmdline, 0)
+ assert(terminator > 0)
+
+ // command_line_exec := cmdline[:terminator]
+
+ // Still need cwd if the execution on the command line is relative.
+ cwd: string
+ cwd_err: Error
+ if .Working_Dir in selection {
+ strings.builder_reset(&path_builder)
+ strings.write_string(&path_builder, "/proc/")
+ strings.write_int(&path_builder, pid)
+ strings.write_string(&path_builder, "/cwd")
+
+ cwd, cwd_err = _read_link_cstr(strings.to_cstring(&path_builder) or_return, temp_allocator) // allowed to fail
+ if cwd_err == nil && .Working_Dir in selection {
+ info.working_dir = strings.clone(cwd, allocator) or_return
+ info.fields += {.Working_Dir}
+ } else if cwd_err != nil {
+ err = cwd_err
+ break cmdline_if
+ }
+ }
+
+ if selection & {.Command_Line, .Command_Args} != {} {
+ // skip to first arg
+ //cmdline = cmdline[terminator + 1:]
+ command_line_builder: strings.Builder
+ command_args_list: [dynamic]string
+
+ if .Command_Line in selection {
+ command_line_builder = strings.builder_make(allocator) or_return
+ info.fields += {.Command_Line}
+ }
+
+ for i := 0; len(cmdline) > 0; i += 1 {
+ if terminator = strings.index_byte(cmdline, 0); terminator < 0 {
+ break
+ }
+
+ if .Command_Line in selection {
+ if i > 0 {
+ strings.write_byte(&command_line_builder, ' ')
+ }
+ strings.write_string(&command_line_builder, cmdline[:terminator])
+ }
+ if .Command_Args in selection {
+ if i == 1 {
+ command_args_list = make([dynamic]string, allocator) or_return
+ info.fields += {.Command_Args}
+ }
+ if i > 0 {
+ arg := strings.clone(cmdline[:terminator], allocator) or_return
+ append(&command_args_list, arg) or_return
+ }
+ }
+
+ cmdline = cmdline[terminator + 1:]
+ }
+ info.command_line = strings.to_string(command_line_builder)
+ info.command_args = command_args_list[:]
+ }
+ }
+
+ stat_if: if selection & {.PPid, .Priority} != {} {
+ strings.builder_reset(&path_builder)
+ strings.write_string(&path_builder, "/proc/")
+ strings.write_int(&path_builder, pid)
+ strings.write_string(&path_builder, "/stat")
+
+ proc_stat_bytes, stat_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator)
+ if stat_err != nil {
+ err = stat_err
+ break stat_if
+ }
+ if len(proc_stat_bytes) <= 0 {
+ break stat_if
+ }
+
+ // Skip to the first field after the executable name
+ stats: string
+ if start := strings.last_index_byte(string(proc_stat_bytes), ')'); start != -1 {
+ stats = string(proc_stat_bytes[start + 2:])
+ } else {
+ break stat_if
+ }
+
+ // NOTE: index 0 corresponds to field 3 (state) from `man 5 proc_pid_stat`
+ // because we skipped passed the executable name above.
+ Fields :: enum {
+ State,
+ PPid,
+ PGrp,
+ Session,
+ Tty_Nr,
+ TpGid,
+ Flags,
+ MinFlt,
+ CMinFlt,
+ MajFlt,
+ CMajFlt,
+ UTime,
+ STime,
+ CUTime,
+ CSTime,
+ Priority,
+ Nice,
+ //... etc,
+ }
+ stat_fields := strings.split(stats, " ", temp_allocator) or_return
+
+ if len(stat_fields) <= int(Fields.Nice) {
+ break stat_if
+ }
+
+ if .PPid in selection {
+ if ppid, ok := strconv.parse_int(stat_fields[Fields.PPid]); ok {
+ info.ppid = ppid
+ info.fields += {.PPid}
+ } else {
+ err = .Invalid_File
+ break stat_if
+ }
+ }
+
+ if .Priority in selection {
+ if nice, ok := strconv.parse_int(stat_fields[Fields.Nice]); ok {
+ info.priority = nice
+ info.fields += {.Priority}
+ } else {
+ err = .Invalid_File
+ break stat_if
+ }
+ }
+ }
+
+ if .Executable_Path in selection {
+ /*
+ NOTE(Jeroen):
+
+ The old version returned the wrong executable path for things like `bash` or `sh`,
+ for whom `/proc/<pid>/cmdline` will just report "bash" or "sh",
+ resulting in misleading paths like `$PWD/sh`, even though that executable doesn't exist there.
+
+ Thanks to Yawning for suggesting `/proc/self/exe`.
+ */
+
+ strings.builder_reset(&path_builder)
+ strings.write_string(&path_builder, "/proc/")
+ strings.write_int(&path_builder, pid)
+ strings.write_string(&path_builder, "/exe")
+
+ if exe_bytes, exe_err := _read_link(strings.to_string(path_builder), temp_allocator); exe_err == nil {
+ info.executable_path = strings.clone(string(exe_bytes), allocator) or_return
+ info.fields += {.Executable_Path}
+ } else {
+ err = exe_err
+ }
+ }
+
+ if .Environment in selection {
+ strings.builder_reset(&path_builder)
+ strings.write_string(&path_builder, "/proc/")
+ strings.write_int(&path_builder, pid)
+ strings.write_string(&path_builder, "/environ")
+
+ if env_bytes, env_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator); env_err == nil {
+ env := string(env_bytes)
+
+ env_list := make([dynamic]string, allocator) or_return
+ for len(env) > 0 {
+ terminator := strings.index_byte(env, 0)
+ if terminator <= 0 {
+ break
+ }
+ e := strings.clone(env[:terminator], allocator) or_return
+ append(&env_list, e) or_return
+ env = env[terminator + 1:]
+ }
+ info.environment = env_list[:]
+ info.fields += {.Environment}
+ } else if err == nil {
+ err = env_err
+ }
+ }
+
+ return
+}
+
+@(private="package")
+_process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
+ return _process_info_by_pid(process.pid, selection, allocator)
+}
+
+@(private="package")
+_current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
+ return _process_info_by_pid(get_pid(), selection, allocator)
+}
+
+@(private="package")
+_process_open :: proc(pid: int, _: Process_Open_Flags) -> (process: Process, err: Error) {
+ process.pid = pid
+ process.handle = PIDFD_UNASSIGNED
+
+ pidfd, errno := linux.pidfd_open(linux.Pid(pid), {})
+ if errno == .ENOSYS {
+ return process, .Unsupported
+ }
+ if errno != .NONE {
+ return process, _get_platform_error(errno)
+ }
+ process.handle = uintptr(pidfd)
+ return
+}
+
+@(private="package")
+_process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
+ temp_allocator := TEMP_ALLOCATOR_GUARD({})
+
+ if len(desc.command) == 0 {
+ return process, .Invalid_Command
+ }
+
+ dir_fd := linux.AT_FDCWD
+ errno: linux.Errno
+ if desc.working_dir != "" {
+ dir_cstr := clone_to_cstring(desc.working_dir, temp_allocator) or_return
+ if dir_fd, errno = linux.open(dir_cstr, _OPENDIR_FLAGS); errno != .NONE {
+ return process, _get_platform_error(errno)
+ }
+ }
+ defer if desc.working_dir != "" {
+ linux.close(dir_fd)
+ }
+
+ // search PATH if just a plain name is provided
+ exe_path: cstring
+ executable_name := desc.command[0]
+ if strings.index_byte(executable_name, '/') < 0 {
+ path_env := get_env("PATH", temp_allocator)
+ path_dirs := split_path_list(path_env, temp_allocator) or_return
+
+ exe_builder := strings.builder_make(temp_allocator) or_return
+
+ found: bool
+ for dir in path_dirs {
+ strings.builder_reset(&exe_builder)
+ strings.write_string(&exe_builder, dir)
+ strings.write_byte(&exe_builder, '/')
+ strings.write_string(&exe_builder, executable_name)
+
+ exe_path = strings.to_cstring(&exe_builder) or_return
+ stat := linux.Stat{}
+ if linux.stat(exe_path, &stat) == .NONE && .IFREG in stat.mode && .IXUSR in stat.mode {
+ found = true
+ break
+ }
+ }
+ if !found {
+ // check in cwd to match windows behavior
+ strings.builder_reset(&exe_builder)
+ strings.write_string(&exe_builder, "./")
+ strings.write_string(&exe_builder, executable_name)
+
+ exe_path = strings.to_cstring(&exe_builder) or_return
+ if linux.access(exe_path, linux.X_OK) != .NONE {
+ return process, .Not_Exist
+ }
+ }
+ } else {
+ exe_path = clone_to_cstring(executable_name, temp_allocator) or_return
+ if linux.access(exe_path, linux.X_OK) != .NONE {
+ return process, .Not_Exist
+ }
+ }
+
+ // args and environment need to be a list of cstrings
+ // that are terminated by a nil pointer.
+ cargs := make([]cstring, len(desc.command) + 1, temp_allocator) or_return
+ for command, i in desc.command {
+ cargs[i] = clone_to_cstring(command, temp_allocator) or_return
+ }
+
+ // Use current process' environment if description didn't provide it.
+ env: [^]cstring
+ if desc.env == nil {
+ // take this process's current environment
+ env = raw_data(export_cstring_environment(temp_allocator))
+ } else {
+ cenv := make([]cstring, len(desc.env) + 1, temp_allocator) or_return
+ for env, i in desc.env {
+ cenv[i] = clone_to_cstring(env, temp_allocator) or_return
+ }
+ env = &cenv[0]
+ }
+
+ child_pipe_fds: [2]linux.Fd
+ if errno = linux.pipe2(&child_pipe_fds, {.CLOEXEC}); errno != .NONE {
+ return process, _get_platform_error(errno)
+ }
+ defer linux.close(child_pipe_fds[READ])
+
+ // TODO: This is the traditional textbook implementation with fork.
+ // A more efficient implementation with vfork:
+ //
+ // 1. retrieve signal handlers
+ // 2. block all signals
+ // 3. allocate some stack space
+ // 4. vfork (waits for child exit or execve); In child:
+ // a. set child signal handlers
+ // b. set up any necessary pipes
+ // c. execve
+ // 5. restore signal handlers
+ //
+ pid: linux.Pid
+ if pid, errno = linux.fork(); errno != .NONE {
+ linux.close(child_pipe_fds[WRITE])
+ return process, _get_platform_error(errno)
+ }
+
+ STDIN :: linux.Fd(0)
+ STDOUT :: linux.Fd(1)
+ STDERR :: linux.Fd(2)
+
+ READ :: 0
+ WRITE :: 1
+
+ if pid == 0 {
+ // in child process now
+ write_errno_to_parent_and_abort :: proc(parent_fd: linux.Fd, errno: linux.Errno) -> ! {
+ error_byte: [1]u8 = { u8(errno) }
+ linux.write(parent_fd, error_byte[:])
+ linux.exit(126)
+ }
+
+ stdin_fd: linux.Fd
+ stdout_fd: linux.Fd
+ stderr_fd: linux.Fd
+
+ if desc.stdin != nil {
+ stdin_fd = linux.Fd(fd(desc.stdin))
+ } else {
+ stdin_fd, errno = linux.open("/dev/null", {})
+ if errno != .NONE {
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+ }
+
+ write_devnull: linux.Fd = -1
+
+ if desc.stdout != nil {
+ stdout_fd = linux.Fd(fd(desc.stdout))
+ } else {
+ write_devnull, errno = linux.open("/dev/null", {.WRONLY})
+ if errno != .NONE {
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+ stdout_fd = write_devnull
+ }
+
+ if desc.stderr != nil {
+ stderr_fd = linux.Fd(fd(desc.stderr))
+ } else {
+ if write_devnull < 0 {
+ write_devnull, errno = linux.open("/dev/null", {.WRONLY})
+ if errno != .NONE {
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+ }
+ stderr_fd = write_devnull
+ }
+
+ if _, errno = linux.dup2(stdin_fd, STDIN); errno != .NONE {
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+ if _, errno = linux.dup2(stdout_fd, STDOUT); errno != .NONE {
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+ if _, errno = linux.dup2(stderr_fd, STDERR); errno != .NONE {
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+ if dir_fd != linux.AT_FDCWD {
+ if errno = linux.fchdir(dir_fd); errno != .NONE {
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+ }
+
+ errno = linux.execveat(dir_fd, exe_path, &cargs[0], env)
+ assert(errno != nil)
+ write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+ }
+
+ linux.close(child_pipe_fds[WRITE])
+
+ process.pid = int(pid)
+
+ child_byte: [1]u8
+ errno = .EINTR
+ for errno == .EINTR {
+ _, errno = linux.read(child_pipe_fds[READ], child_byte[:])
+ }
+
+ // If the read failed, something weird happened. Do not return the read
+ // error so the user knows to wait on it.
+ if errno == .NONE {
+ child_errno := linux.Errno(child_byte[0])
+ if child_errno != .NONE {
+ // We can assume it trapped here.
+ _reap_terminated(process)
+ process.pid = 0
+ return process, _get_platform_error(child_errno)
+ }
+ }
+
+ process, _ = process_open(int(pid))
+ return
+}
+
+_process_state_update_times :: proc(state: ^Process_State) -> (err: Error) {
+ temp_allocator := TEMP_ALLOCATOR_GUARD({})
+
+ stat_path_buf: [48]u8
+ path_builder := strings.builder_from_bytes(stat_path_buf[:])
+ strings.write_string(&path_builder, "/proc/")
+ strings.write_int(&path_builder, int(state.pid))
+ strings.write_string(&path_builder, "/stat")
+
+ stat_buf: []u8
+ stat_buf, err = _read_entire_pseudo_file(strings.to_cstring(&path_builder) or_return, temp_allocator)
+ if err != nil {
+ return
+ }
+
+ // ')' will be the end of the executable name (item 2)
+ idx := strings.last_index_byte(string(stat_buf), ')')
+ stats := string(stat_buf[idx + 2:])
+
+ // utime and stime are the 14 and 15th items, respectively, and we are
+ // currently on item 3. Skip 11 items here.
+ for _ in 0..<11 {
+ stats = stats[strings.index_byte(stats, ' ') + 1:]
+ }
+
+ idx = strings.index_byte(stats, ' ')
+ utime_str := stats[:idx]
+
+ stats = stats[idx + 1:]
+ stime_str := stats[:strings.index_byte(stats, ' ')]
+
+ utime, stime: int
+ ok: bool
+ if utime, ok = strconv.parse_int(utime_str, 10); !ok {
+ return .Invalid_File
+ }
+ if stime, ok = strconv.parse_int(stime_str, 10); !ok {
+ return .Invalid_File
+ }
+
+ // NOTE: Assuming HZ of 100, 1 jiffy == 10 ms
+ state.user_time = time.Duration(utime) * 10 * time.Millisecond
+ state.system_time = time.Duration(stime) * 10 * time.Millisecond
+
+ return
+}
+
+_reap_terminated :: proc(process: Process) -> (state: Process_State, err: Error) {
+ state.pid = process.pid
+ _process_state_update_times(&state)
+
+ info: linux.Sig_Info
+ errno := linux.Errno.EINTR
+ for errno == .EINTR {
+ errno = linux.waitid(.PID, linux.Id(process.pid), &info, {.WEXITED}, nil)
+ }
+ err = _get_platform_error(errno)
+
+ switch linux.Sig_Child_Code(info.code) {
+ case .NONE, .CONTINUED, .STOPPED:
+ unreachable()
+ case .EXITED:
+ state.exited = true
+ state.exit_code = int(info.status)
+ state.success = state.exit_code == 0
+ case .KILLED, .DUMPED, .TRAPPED:
+ state.exited = true
+ state.exit_code = int(info.status)
+ state.success = false
+ }
+ return
+}
+
+_timed_wait_on_handle :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) {
+ timeout := timeout
+
+ process_state.pid = process.pid
+ pidfd := linux.Fd(process.handle)
+ pollfd: [1]linux.Poll_Fd = {
+ {
+ fd = pidfd,
+ events = {.IN},
+ },
+ }
+
+ start_tick := time.tick_now()
+
+ mask: bit_set[0..<64; u64]
+ mask += { int(linux.Signal.SIGCHLD) - 1 }
+ sigchld_set := transmute(linux.Sig_Set)(mask)
+
+ info: linux.Sig_Info
+ for {
+ if timeout <= 0 {
+ _process_state_update_times(&process_state)
+ err = .Timeout
+ return
+ }
+
+ ts: linux.Time_Spec = {
+ time_sec = uint(timeout / time.Second),
+ time_nsec = uint(timeout % time.Second),
+ }
+
+ n, errno := linux.ppoll(pollfd[:], &ts, &sigchld_set)
+ if errno != .NONE {
+ if errno == .EINTR {
+ timeout -= time.tick_since(start_tick)
+ start_tick = time.tick_now()
+ continue
+ }
+ return process_state, _get_platform_error(errno)
+ }
+
+ if n == 0 { // timeout with no events
+ _process_state_update_times(&process_state)
+ err = .Timeout
+ return
+ }
+
+ if errno = linux.waitid(.PIDFD, linux.Id(process.handle), &info, {.WEXITED, .WNOHANG, .WNOWAIT}, nil); errno != .NONE {
+ return process_state, _get_platform_error(errno)
+ }
+
+ if info.signo == .SIGCHLD {
+ break
+ }
+
+ timeout -= time.tick_since(start_tick)
+ start_tick = time.tick_now()
+ }
+
+ // _reap_terminated for pidfd
+ {
+ _process_state_update_times(&process_state)
+
+ errno := linux.Errno.EINTR
+ for errno == .EINTR {
+ errno = linux.waitid(.PIDFD, linux.Id(process.handle), &info, {.WEXITED}, nil)
+ }
+ err = _get_platform_error(errno)
+
+ switch linux.Sig_Child_Code(info.code) {
+ case .NONE, .CONTINUED, .STOPPED:
+ unreachable()
+ case .EXITED:
+ process_state.exited = true
+ process_state.exit_code = int(info.status)
+ process_state.success = process_state.exit_code == 0
+ case .KILLED, .DUMPED, .TRAPPED:
+ process_state.exited = true
+ process_state.exit_code = int(info.status)
+ process_state.success = false
+ }
+ }
+ return
+}
+
+_timed_wait_on_pid :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) {
+ timeout := timeout
+ process_state.pid = process.pid
+
+ mask: bit_set[0..<64; u64]
+ mask += { int(linux.Signal.SIGCHLD) - 1 }
+ sigchld_set := transmute(linux.Sig_Set)(mask)
+
+ start_tick := time.tick_now()
+
+ org_sigset: linux.Sig_Set
+ errno := linux.rt_sigprocmask(.SIG_BLOCK, &sigchld_set, &org_sigset)
+ if errno != .NONE {
+ return process_state, _get_platform_error(errno)
+ }
+ defer linux.rt_sigprocmask(.SIG_SETMASK, &org_sigset, nil)
+
+ // In case there was a signal handler on SIGCHLD, avoid race
+ // condition by checking wait first.
+ info: linux.Sig_Info
+ errno = linux.waitid(.PID, linux.Id(process.pid), &info, {.WNOWAIT, .WEXITED, .WNOHANG}, nil)
+
+ for errno != .NONE || info.code == 0 || info.pid != linux.Pid(process.pid) {
+ if timeout <= 0 {
+ _process_state_update_times(&process_state)
+ err = .Timeout
+ return
+ }
+
+ ts: linux.Time_Spec = {
+ time_sec = uint(timeout / time.Second),
+ time_nsec = uint(timeout % time.Second),
+ }
+
+ _, errno = linux.rt_sigtimedwait(&sigchld_set, &info, &ts)
+ #partial switch errno {
+ case .EAGAIN: // timeout
+ _process_state_update_times(&process_state)
+ err = .Timeout
+ return
+ case .EINTR:
+ timeout -= time.tick_since(start_tick)
+ start_tick = time.tick_now()
+ case .EINVAL:
+ return process_state, _get_platform_error(errno)
+ }
+ }
+
+ return _reap_terminated(process)
+}
+
+@(private="package")
+_process_wait :: proc(process: Process, timeout: time.Duration) -> (Process_State, Error) {
+ if timeout > 0 {
+ if process.handle == PIDFD_UNASSIGNED {
+ return _timed_wait_on_pid(process, timeout)
+ } else {
+ return _timed_wait_on_handle(process, timeout)
+ }
+ }
+
+ process_state: Process_State = {
+ pid = process.pid,
+ }
+
+ errno: linux.Errno
+ options: linux.Wait_Options = {.WEXITED}
+ if timeout == 0 {
+ options += {.WNOHANG}
+ }
+
+ info: linux.Sig_Info
+
+ errno = .EINTR
+ for errno == .EINTR {
+ errno = linux.waitid(.PID, linux.Id(process.pid), &info, options + {.WNOWAIT}, nil)
+ }
+ if errno == .EAGAIN || (errno == .NONE && info.signo != .SIGCHLD) {
+ _process_state_update_times(&process_state)
+ return process_state, .Timeout
+ }
+ if errno != .NONE {
+ return process_state, _get_platform_error(errno)
+ }
+
+ return _reap_terminated(process)
+}
+
+@(private="package")
+_process_close :: proc(process: Process) -> Error {
+ if process.handle == 0 || process.handle == PIDFD_UNASSIGNED {
+ return nil
+ }
+ pidfd := linux.Fd(process.handle)
+ return _get_platform_error(linux.close(pidfd))
+}
+
+@(private="package")
+_process_kill :: proc(process: Process) -> Error {
+ return _get_platform_error(linux.kill(linux.Pid(process.pid), .SIGKILL))
+}
+