Files
Odin/core/os/os2/process_linux.odin
2025-10-31 10:24:21 +00:00

851 lines
22 KiB
Odin

#+build linux
#+private file
package os2
import "base:runtime"
import "base:intrinsics"
import "core:time"
import "core:slice"
import "core:strings"
import "core:strconv"
import "core:sys/linux"
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")
_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))
}