diff options
| author | jason <jkercher43@gmail.com> | 2024-07-23 16:50:00 -0400 |
|---|---|---|
| committer | jason <jkercher43@gmail.com> | 2024-07-23 16:50:00 -0400 |
| commit | 3c7d1f35db3ea8586c5b920fbdd1978a90c76bc7 (patch) | |
| tree | 1bc6cb04ad415f5ebc00533ba8b929d0edec81fa | |
| parent | fc2e31fcd03b2dfd0258d0546e4f023f9e099b27 (diff) | |
os2 process implementation for linux
| -rw-r--r-- | core/os/os2/file_linux.odin | 71 | ||||
| -rw-r--r-- | core/os/os2/internal_util.odin | 15 | ||||
| -rw-r--r-- | core/os/os2/pipe_linux.odin | 9 | ||||
| -rw-r--r-- | core/os/os2/process_linux.odin | 518 |
4 files changed, 561 insertions, 52 deletions
diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin index cf643b31a..9c7c3ac62 100644 --- a/core/os/os2/file_linux.odin +++ b/core/os/os2/file_linux.odin @@ -6,41 +6,32 @@ import "core:time" import "base:runtime" import "core:sys/linux" +File_Impl_Kind :: enum u8 { + File, + Pipe, +} + File_Impl :: struct { file: File, name: string, fd: linux.Fd, + kind: File_Impl_Kind, allocator: runtime.Allocator, } _stdin := File{ - impl = &File_Impl{ - name = "/proc/self/fd/0", - fd = 0, - allocator = _file_allocator(), - }, stream = { procedure = _file_stream_proc, }, fstat = _fstat, } _stdout := File{ - impl = &File_Impl{ - name = "/proc/self/fd/1", - fd = 1, - allocator = _file_allocator(), - }, stream = { procedure = _file_stream_proc, }, fstat = _fstat, } _stderr := File{ - impl = &File_Impl{ - name = "/proc/self/fd/2", - fd = 2, - allocator = _file_allocator(), - }, stream = { procedure = _file_stream_proc, }, @@ -49,10 +40,33 @@ _stderr := File{ @init _standard_stream_init :: proc() { - // cannot define these manually because cyclic reference - _stdin.stream.data = &_stdin - _stdout.stream.data = &_stdout - _stderr.stream.data = &_stderr + @static stdin_impl := File_Impl { + name = "/proc/self/fd/0", + fd = 0, + } + + @static stdout_impl := File_Impl { + name = "/proc/self/fd/1", + fd = 1, + } + + @static stderr_impl := File_Impl { + name = "/proc/self/fd/2", + fd = 2, + } + + stdin_impl.allocator = _file_allocator() + stdout_impl.allocator = _file_allocator() + stderr_impl.allocator = _file_allocator() + + _stdin.impl = &stdin_impl + _stdout.impl = &stdout_impl + _stderr.impl = &stderr_impl + + // cannot define these initially because cyclic reference + _stdin.stream.data = &stdin_impl + _stdout.stream.data = &stdout_impl + _stderr.stream.data = &stderr_impl stdin = &_stdin stdout = &_stdout @@ -196,6 +210,12 @@ _write_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (i64, Error) { } _file_size :: proc(f: ^File_Impl) -> (n: i64, err: Error) { + if f.kind == .Pipe { + return 0, .No_Size + } + // TODO: Identify 0-sized "pseudo" files and return No_Size. This would + // eliminate the need for the _read_entire_pseudo_file procs. + s: linux.Stat = --- errno := linux.fstat(f.fd, &s) if errno != .NONE { @@ -373,17 +393,13 @@ _exists :: proc(name: string) -> bool { return !res && errno == .NONE } -/* Certain files in the Linux file system are not actual - * files (e.g. everything in /proc/). Therefore, the - * read_entire_file procs fail to actually read anything - * since these "files" stat to a size of 0. Here, we just - * read until there is nothing left. - */ +/* For reading Linux system files that stat to size 0 */ _read_entire_pseudo_file :: proc { _read_entire_pseudo_file_string, _read_entire_pseudo_file_cstring } _read_entire_pseudo_file_string :: proc(name: string, allocator: runtime.Allocator) -> (b: []u8, e: Error) { - name_cstr := clone_to_cstring(name, allocator) or_return - defer delete(name, allocator) + TEMP_ALLOCATOR_GUARD() + name_cstr := clone_to_cstring(name, temp_allocator()) or_return + defer delete(name, temp_allocator()) return _read_entire_pseudo_file_cstring(name_cstr, allocator) } @@ -413,7 +429,6 @@ _read_entire_pseudo_file_cstring :: proc(name: cstring, allocator: runtime.Alloc } resize(&contents, i + n) - return contents[:], nil } diff --git a/core/os/os2/internal_util.odin b/core/os/os2/internal_util.odin index f7a38f3f1..c85a6bc10 100644 --- a/core/os/os2/internal_util.odin +++ b/core/os/os2/internal_util.odin @@ -3,7 +3,8 @@ package os2 import "base:intrinsics" import "base:runtime" - +import "core:fmt" +import "core:strings" // Splits pattern by the last wildcard "*", if it exists, and returns the prefix and suffix // parts which are split by the last "*" @@ -48,6 +49,16 @@ temp_cstring :: proc(s: string) -> (cstring, runtime.Allocator_Error) { } @(require_results) +ctprintf :: proc(format: string, args: ..any, newline := false) -> cstring { + str: strings.Builder + strings.builder_init(&str, temp_allocator()) + fmt.sbprintf(&str, format, ..args, newline=newline) + strings.write_byte(&str, 0) + s := strings.to_string(str) + return cstring(raw_data(s)) +} + +@(require_results) string_from_null_terminated_bytes :: proc(b: []byte) -> (res: string) { s := string(b) i := 0 @@ -126,5 +137,3 @@ random_string :: proc(buf: []byte) -> string { buf[i] = digits[u % b] return string(buf[i:]) } - - diff --git a/core/os/os2/pipe_linux.odin b/core/os/os2/pipe_linux.odin index 8835cc30f..0442d17ac 100644 --- a/core/os/os2/pipe_linux.odin +++ b/core/os/os2/pipe_linux.odin @@ -5,13 +5,18 @@ import "core:sys/linux" _pipe :: proc() -> (r, w: ^File, err: Error) { fds: [2]linux.Fd - errno := linux.pipe2(&fds, {}) + errno := linux.pipe2(&fds, {.CLOEXEC}) if errno != .NONE { return nil, nil,_get_platform_error(errno) } r = _new_file(uintptr(fds[0])) w = _new_file(uintptr(fds[1])) + + r_impl := (^File_Impl)(r.impl) + r_impl.kind = .Pipe + w_impl := (^File_Impl)(w.impl) + w_impl.kind = .Pipe + return } - diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index d832083b6..371ae952c 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -1,68 +1,269 @@ +//+build linux //+private file package os2 import "base:runtime" + +import "core:fmt" +import "core:mem" import "core:time" +import "core:strings" +import "core:strconv" import "core:sys/linux" +import "core:path/filepath" + +PIDFD_UNASSIGNED :: ~uintptr(0) + +_has_pidfd_open: bool = true // pidfd is still fairly new (Linux 5.3) @(private="package") _exit :: proc "contextless" (code: int) -> ! { - linux.exit(i32(code)) + linux.exit_group(i32(code)) } - @(private="package") _get_uid :: proc() -> int { - return -1 + return int(linux.getuid()) } @(private="package") _get_euid :: proc() -> int { - return -1 + return int(linux.geteuid()) } @(private="package") _get_gid :: proc() -> int { - return -1 + return int(linux.getgid()) } @(private="package") _get_egid :: proc() -> int { - return -1 + return int(linux.getegid()) } @(private="package") _get_pid :: proc() -> int { - return -1 + return int(linux.getpid()) } @(private="package") _get_ppid :: proc() -> int { - return -1 + return int(linux.getppid()) } @(private="package") -_process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) { - return +_process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { + TEMP_ALLOCATOR_GUARD() + + dir_fd: linux.Fd + errno: linux.Errno + #partial switch dir_fd, errno = linux.open("/proc/", _OPENDIR_FLAGS); errno { + case .ENOTDIR: + return {}, .Invalid_Dir + case .ENOENT: + return {}, .Not_Exist + } + defer linux.close(dir_fd) + + dynamic_list := make([dynamic]int, allocator) + + buf := make([dynamic]u8, 128, 128, temp_allocator()) + 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) + } + + d: ^dirent64 + + for i := 0; i < buflen; i += int(d.d_reclen) { + d = (^dirent64)(rawptr(&buf[i])) + d_name_cstr := cstring(&d.d_name[0]) + #no_bounds_check d_name_str := string(d.d_name[:len(d_name_cstr)]) + + if pid, ok := strconv.parse_int(d_name_str); ok { + append(&dynamic_list, pid) + } + } + } + + return dynamic_list[:], nil } @(private="package") _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { + TEMP_ALLOCATOR_GUARD() + + info.fields = selection + + // Use this so we can use bprintf to make cstrings with less copying + path_backing: [48]u8 + path_slice := path_backing[:len(path_backing) - 1] + path_cstr := cstring(&path_slice[0]) + + _ = fmt.bprintf(path_slice, "/proc/%d", pid) + proc_fd, errno := linux.open(path_cstr, _OPENDIR_FLAGS) + if errno != .NONE { + err = _get_platform_error(errno) + return + } + defer linux.close(proc_fd) + + if .Username in selection { + s: linux.Stat + linux.fstat(proc_fd, &s) + + passwd_bytes := read_entire_file("/etc/passwd", temp_allocator()) or_return + passwd := string(passwd_bytes) + for len(passwd) > 0 { + n := strings.index_byte(passwd, ':') + if n == -1 { + break + } + username := passwd[:n] + passwd = passwd[:n+1] + + // skip password field + passwd = passwd[:strings.index_byte(passwd, ':') + 1] + + n = strings.index_byte(passwd, ':') + username = passwd + if uid, ok := strconv.parse_int(passwd[:n]); ok && uid == int(s.uid) { + info.username = strings.clone(username, allocator) + break + } + + eol := strings.index_byte(passwd, '\n') + if eol == -1 { + break + } + passwd = passwd[eol + 1:] + } + } + + if .Executable_Path in selection { + _ = fmt.bprintf(path_slice, "/proc/%d/exe", pid) + info.executable_path = _read_link_cstr(path_cstr, allocator) or_return + } + + if .Working_Dir in selection { + _ = fmt.bprintf(path_slice, "/proc/%d/cwd", pid) + info.working_dir = _read_link_cstr(path_cstr, allocator) or_return + } + + stat_if: if selection & {.PPid, .Priority} != {} { + _ = fmt.bprintf(path_slice, "/proc/%d/stat", pid) + proc_stat_bytes := _read_entire_pseudo_file(path_cstr, temp_allocator()) or_return + if len(proc_stat_bytes) <= 0 { + break stat_if + } + + start := strings.last_index_byte(string(proc_stat_bytes), ')') + stats := string(proc_stat_bytes[start + 2:]) + + // We are now on the 3rd field (skip) + stats = stats[strings.index_byte(stats, ' ') + 1:] + + if .PPid in selection { + ppid_str := stats[:strings.index_byte(stats, ' ')] + if ppid, ok := strconv.parse_int(ppid_str); ok { + info.ppid = ppid + } + } + + if .Priority in selection { + // On 4th field. Priority is field 18 and niceness is field 19. + for i := 4; i < 19; i += 1 { + stats = stats[strings.index_byte(stats, ' ') + 1:] + } + nice_str := stats[:strings.index_byte(stats, ' ')] + if nice, ok := strconv.parse_int(nice_str); ok { + info.priority = nice + } + } + } + + cmdline_if: if selection & {.Command_Line, .Command_Args} != {} { + _ = fmt.bprintf(path_slice, "/proc/%d/cmdline") + cmdline_bytes := _read_entire_pseudo_file(path_cstr, temp_allocator()) or_return + if len(cmdline_bytes) == 0 { + break cmdline_if + } + cmdline := string(cmdline_bytes) + + terminator := strings.index_byte(cmdline, 0) + if .Command_Line in selection { + info.command_line = strings.clone(cmdline[:terminator], allocator) + } + + if .Command_Args in selection { + // skip to first arg + cmdline = cmdline[terminator + 1:] + + arg_list := make([dynamic]string, allocator) + for len(cmdline) > 0 { + terminator = strings.index_byte(cmdline, 0) + append(&arg_list, strings.clone(cmdline[:terminator], allocator)) + cmdline = cmdline[terminator + 1:] + } + info.command_args = arg_list[:] + } + } + + if .Environment in selection { + _ = fmt.bprintf(path_slice, "/proc/%d/environ", pid) + env_bytes := _read_entire_pseudo_file(path_cstr, temp_allocator()) or_return + env := string(env_bytes) + + env_list := make([dynamic]string, allocator) + for len(env) > 0 { + terminator := strings.index_byte(env, 0) + if terminator == -1 || terminator == 0 { + break + } + append(&env_list, strings.clone(env[:terminator], allocator)) + env = env[:terminator + 1] + } + } + return } @(private="package") _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { - return + 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 + return _process_info_by_pid(get_pid(), selection, allocator) } @(private="package") -_process_open :: proc(pid: int, flags: Process_Open_Flags) -> (process: Process, err: Error) { +_process_open :: proc(pid: int, _: Process_Open_Flags) -> (process: Process, err: Error) { + process.pid = pid + process.handle = PIDFD_UNASSIGNED + if !_has_pidfd_open { + return process, .Unsupported + } + + pidfd, errno := linux.pidfd_open(linux.Pid(pid), {}) + if errno == .ENOSYS { + _has_pidfd_open = false + return process, .Unsupported + } + if errno != nil { + return process, _get_platform_error(errno) + } + + process.handle = uintptr(pidfd) return } @@ -71,25 +272,304 @@ _Sys_Process_Attributes :: struct {} @(private="package") _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { + TEMP_ALLOCATOR_GUARD() + + if len(desc.command) == 0 { + return process, .Invalid_File + } + + dir_fd := linux.AT_FDCWD + errno: linux.Errno + if desc.working_dir != "" { + dir_cstr := temp_cstring(desc.working_dir) or_return + if dir_fd, errno = linux.open(dir_cstr, _OPENDIR_FLAGS); errno != .NONE { + return process, _get_platform_error(errno) + } + } + + // search PATH if just a plain name is provided + executable_name := desc.command[0] + executable_path: cstring + if !strings.contains_rune(executable_name, '/') { + path_env := get_env("PATH", temp_allocator()) + path_dirs := filepath.split_list(path_env, temp_allocator()) + found: bool + for dir in path_dirs { + executable_path = ctprintf("%s/%s", dir, executable_name) + fail: bool + if fail, errno = linux.faccessat(dir_fd, executable_path, linux.F_OK); errno == .NONE && !fail { + found = true + break + } + } + if !found { + // check in cwd to match windows behavior + executable_path = ctprintf("./%s", name) + fail: bool + if fail, errno = linux.faccessat(dir_fd, executable_path, linux.F_OK); errno != .NONE || fail { + return process, .Not_Exist + } + } + } else { + executable_path = temp_cstring(executable_name) or_return + } + + not_exec: bool + if not_exec, errno = linux.faccessat(dir_fd, executable_path, linux.F_OK | linux.X_OK); errno != .NONE || not_exec { + return process, errno == .NONE ? .Permission_Denied : _get_platform_error(errno) + } + + // 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()) + for i := 0; i < len(desc.command); i += 1{ + cargs[i] = temp_cstring(desc.command[i]) or_return + } + + // Use current process's environment if descibutes not provided + 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()) + for i := 0; i < len(desc.env); i += 1 { + cenv[i] = temp_cstring(desc.env[i]) or_return + } + env = &cenv[0] + } + + // 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 { + return process, _get_platform_error(errno) + } + + READ :: 0 + WRITE :: 1 + + STDIN :: linux.Fd(0) + STDOUT :: linux.Fd(1) + STDERR :: linux.Fd(2) + + if pid == 0 { + // in child process now + if desc.stdin != nil { + fd := linux.Fd(fd(desc.stdin)) + if _, errno = linux.dup2(fd, STDIN); errno != .NONE { + linux.exit(1) + } + } + if desc.stdout != nil { + fd := linux.Fd(fd(desc.stdout)) + if _, errno = linux.dup2(fd, STDOUT); errno != .NONE { + linux.exit(1) + } + } + if desc.stderr != nil { + fd := linux.Fd(fd(desc.stderr)) + if _, errno = linux.dup2(fd, STDERR); errno != .NONE { + linux.exit(1) + } + } + + if errno = linux.execveat(dir_fd, executable_path, &cargs[0], env); errno != .NONE { + print_error(stderr, _get_platform_error(errno), string(executable_path)) + panic("execve failed to replace process") + } + unreachable() + } + + process.pid = int(pid) + process.handle = PIDFD_UNASSIGNED + return +} + +_process_state_update_times :: proc(p: Process, state: ^Process_State) -> (err: Error) { + TEMP_ALLOCATOR_GUARD() + + stat_path_buf: [32]u8 + _ = fmt.bprintf(stat_path_buf[:], "/proc/%d/stat", p.pid) + stat_buf: []u8 + stat_buf, err = _read_entire_pseudo_file(cstring(&stat_path_buf[0]), 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 i := 0; i < 11; i += 1 { + 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, _ := strconv.parse_int(utime_str, 10) + stime, _ := strconv.parse_int(stime_str, 10) + + // 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 } @(private="package") _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) { + process_state.pid = process.pid + + options: linux.Wait_Options + big_if: if timeout == 0 { + options += {.WNOHANG} + } else if timeout > 0 { + ts: linux.Time_Spec = { + time_sec = uint(timeout / time.Second), + time_nsec = uint(timeout % time.Second), + } + + // pidfd_open is fairly new, so don't error out on ENOSYS + pid_fd: linux.Pid_FD + errno: linux.Errno + if _has_pidfd_open { + if process.handle == PIDFD_UNASSIGNED { + pid_fd, errno = linux.pidfd_open(linux.Pid(process.pid), nil) + if errno != .NONE && errno != .ENOSYS { + return process_state, _get_platform_error(errno) + } + } else { + pid_fd = linux.Pid_FD(process.handle) + } + } + + if errno != .ENOSYS { + defer if process.handle == PIDFD_UNASSIGNED { + linux.close(linux.Fd(pid_fd)) + } + pollfd: [1]linux.Poll_Fd = { + { + fd = linux.Fd(pid_fd), + events = {.IN}, + }, + } + for { + n, e := linux.ppoll(pollfd[:], &ts, nil) + if e == .EINTR { + continue + } + if e != .NONE { + return process_state, _get_platform_error(errno) + } + if n == 0 { + _process_state_update_times(process, &process_state) + return + } + break + } + } else { + mask: bit_set[0..=63] + mask += { int(linux.Signal.SIGCHLD) - 1 } + + org_sigset: linux.Sig_Set + sigset: linux.Sig_Set + mem.copy(&sigset, &mask, size_of(mask)) + errno = linux.rt_sigprocmask(.SIG_BLOCK, &sigset, &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. + options += {.WNOHANG} + waitid_options := options + {.WNOWAIT, .WEXITED} + info: linux.Sig_Info + errno = linux.waitid(.PID, linux.Id(process.pid), &info, waitid_options, nil) + if errno == .NONE && info.code != 0 { + break big_if + } + + loop: for { + sigset = {} + mem.copy(&sigset, &mask, size_of(mask)) + + _, errno = linux.rt_sigtimedwait(&sigset, &info, &ts) + #partial switch errno { + case .EAGAIN: // timeout + _process_state_update_times(process, &process_state) + return + case .EINVAL: + return process_state, _get_platform_error(errno) + case .EINTR: + continue + case: + if info.pid == linux.Pid(process.pid) { + break loop + } + } + } + } + } + + status: u32 + errno: linux.Errno = .EINTR + for errno == .EINTR { + _, errno = linux.wait4(linux.Pid(process.pid), &status, options, nil) + if errno != .NONE { + _process_state_update_times(process, &process_state) + return process_state, _get_platform_error(errno) + } + } + + _process_state_update_times(process, &process_state) + + // terminated by exit + if linux.WIFEXITED(status) { + process_state.exited = true + process_state.exit_code = int(linux.WEXITSTATUS(status)) + process_state.success = process_state.exit_code == 0 + return + } + + // terminated by signal + if linux.WIFSIGNALED(status) { + process_state.exited = false + process_state.exit_code = int(linux.WTERMSIG(status)) + process_state.success = false + return + } return } @(private="package") _process_close :: proc(process: Process) -> Error { - return nil + pidfd := linux.Fd(process.handle) + if pidfd < 0 { + return nil + } + return _get_platform_error(linux.close(pidfd)) } @(private="package") _process_kill :: proc(process: Process) -> Error { - return nil + return _get_platform_error(linux.kill(linux.Pid(process.pid), .SIGKILL)) } -@(private="package") -_process_exe_by_pid :: proc(pid: int, allocator: runtime.Allocator) -> (exe_path: string, err: Error) { - return -}
\ No newline at end of file |