From 3c7d1f35db3ea8586c5b920fbdd1978a90c76bc7 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 23 Jul 2024 16:50:00 -0400 Subject: [PATCH 01/23] os2 process implementation for linux --- core/os/os2/file_linux.odin | 71 +++-- core/os/os2/internal_util.odin | 15 +- core/os/os2/pipe_linux.odin | 9 +- 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 "*" @@ -47,6 +48,16 @@ temp_cstring :: proc(s: string) -> (cstring, runtime.Allocator_Error) { return clone_to_cstring(s, temp_allocator()) } +@(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) @@ -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 From 82deaa59ad7de52c381c8abc62cd895b76c9d96c Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 23 Jul 2024 17:50:30 -0400 Subject: [PATCH 02/23] os2 linux: fix order of operations bug in _open; fix process_info routine --- core/os/os2/file_linux.odin | 2 +- core/os/os2/process_linux.odin | 47 +++++++++++++++++----------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin index 9c7c3ac62..bf1b44c19 100644 --- a/core/os/os2/file_linux.odin +++ b/core/os/os2/file_linux.odin @@ -85,7 +85,7 @@ _open :: proc(name: string, flags: File_Flags, perm: int) -> (f: ^File, err: Err // terminal would be incredibly rare. This has no effect on files while // allowing us to open serial devices. sys_flags: linux.Open_Flags = {.NOCTTY, .CLOEXEC} - switch flags & O_RDONLY|O_WRONLY|O_RDWR { + switch flags & (O_RDONLY|O_WRONLY|O_RDWR) { case O_RDONLY: case O_WRONLY: sys_flags += {.WRONLY} case O_RDWR: sys_flags += {.RDWR} diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 371ae952c..50024b2b3 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -120,7 +120,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator s: linux.Stat linux.fstat(proc_fd, &s) - passwd_bytes := read_entire_file("/etc/passwd", temp_allocator()) or_return + passwd_bytes := _read_entire_pseudo_file_cstring("/etc/passwd", temp_allocator()) or_return passwd := string(passwd_bytes) for len(passwd) > 0 { n := strings.index_byte(passwd, ':') @@ -128,13 +128,12 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator break } username := passwd[:n] - passwd = passwd[:n+1] + passwd = passwd[n+1:] // skip password field - passwd = passwd[:strings.index_byte(passwd, ':') + 1] + 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 @@ -148,14 +147,9 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } } - 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 + info.working_dir, _ = _read_link_cstr(path_cstr, allocator) // allowed to fail } stat_if: if selection & {.PPid, .Priority} != {} { @@ -172,7 +166,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator stats = stats[strings.index_byte(stats, ' ') + 1:] if .PPid in selection { - ppid_str := stats[:strings.index_byte(stats, ' ')] + ppid_str := stats[strings.index_byte(stats, ' '):] if ppid, ok := strconv.parse_int(ppid_str); ok { info.ppid = ppid } @@ -183,15 +177,15 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator for i := 4; i < 19; i += 1 { stats = stats[strings.index_byte(stats, ' ') + 1:] } - nice_str := stats[:strings.index_byte(stats, ' ')] + 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_if: if selection & {.Command_Line, .Command_Args, .Executable_Path} != {} { + _ = fmt.bprintf(path_slice, "/proc/%d/cmdline", pid) cmdline_bytes := _read_entire_pseudo_file(path_cstr, temp_allocator()) or_return if len(cmdline_bytes) == 0 { break cmdline_if @@ -199,6 +193,9 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator cmdline := string(cmdline_bytes) terminator := strings.index_byte(cmdline, 0) + if .Executable_Path in selection { + info.executable_path = strings.clone(cmdline[:terminator], allocator) + } if .Command_Line in selection { info.command_line = strings.clone(cmdline[:terminator], allocator) } @@ -219,17 +216,19 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator 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) + if env_bytes, env_err := _read_entire_pseudo_file(path_cstr, temp_allocator()); env_err == nil { + 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 + 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:] } - append(&env_list, strings.clone(env[:terminator], allocator)) - env = env[:terminator + 1] + info.environment = env_list[:] } } @@ -326,7 +325,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { cargs[i] = temp_cstring(desc.command[i]) or_return } - // Use current process's environment if descibutes not provided + // Use current process's environment if description not provided env: [^]cstring if desc.env == nil { // take this process's current environment From 0455e4b60f3262fed6badba835a12e567e34a23f Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 23 Jul 2024 19:48:18 -0400 Subject: [PATCH 03/23] remove unused constants; fix comment --- core/os/os2/process_linux.odin | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 50024b2b3..d925e0e86 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -325,7 +325,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { cargs[i] = temp_cstring(desc.command[i]) or_return } - // Use current process's environment if description not provided + // Use current process' environment if description didn't provide it. env: [^]cstring if desc.env == nil { // take this process's current environment @@ -355,9 +355,6 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { return process, _get_platform_error(errno) } - READ :: 0 - WRITE :: 1 - STDIN :: linux.Fd(0) STDOUT :: linux.Fd(1) STDERR :: linux.Fd(2) From 16bdc6d240c1f09a394951b2504c152137bb10da Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 24 Jul 2024 08:43:22 -0400 Subject: [PATCH 04/23] use more iterators; global "has pidfd open" state is now thread-safe --- core/os/os2/file_linux.odin | 1 - core/os/os2/process_linux.odin | 41 ++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin index bf1b44c19..7d4c9b509 100644 --- a/core/os/os2/file_linux.odin +++ b/core/os/os2/file_linux.odin @@ -399,7 +399,6 @@ _read_entire_pseudo_file :: proc { _read_entire_pseudo_file_string, _read_entire _read_entire_pseudo_file_string :: proc(name: string, allocator: runtime.Allocator) -> (b: []u8, e: Error) { 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) } diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index d925e0e86..f54528d69 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -6,7 +6,9 @@ import "base:runtime" import "core:fmt" import "core:mem" +import "core:sync" import "core:time" +import "core:slice" import "core:strings" import "core:strconv" import "core:sys/linux" @@ -14,7 +16,16 @@ import "core:path/filepath" PIDFD_UNASSIGNED :: ~uintptr(0) -_has_pidfd_open: bool = true // pidfd is still fairly new (Linux 5.3) +_default_has_pidfd_open: bool = true + +// pidfd is still fairly new (Linux 5.3) +_has_pidfd_open :: proc () -> bool { + @thread_local has_pidfd_open: bool = true + if has_pidfd_open { + has_pidfd_open = sync.atomic_load(&_default_has_pidfd_open) + } + return has_pidfd_open +} @(private="package") _exit :: proc "contextless" (code: int) -> ! { @@ -65,7 +76,7 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { } defer linux.close(dir_fd) - dynamic_list := make([dynamic]int, allocator) + dynamic_list := make([dynamic]int, temp_allocator()) buf := make([dynamic]u8, 128, 128, temp_allocator()) loop: for { @@ -94,7 +105,7 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { } } - return dynamic_list[:], nil + return slice.clone(dynamic_list[:], allocator), nil } @(private="package") @@ -174,7 +185,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator if .Priority in selection { // On 4th field. Priority is field 18 and niceness is field 19. - for i := 4; i < 19; i += 1 { + for _ in 4..<19 { stats = stats[strings.index_byte(stats, ' ') + 1:] } nice_str := stats[strings.index_byte(stats, ' '):] @@ -249,13 +260,13 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime _process_open :: proc(pid: int, _: Process_Open_Flags) -> (process: Process, err: Error) { process.pid = pid process.handle = PIDFD_UNASSIGNED - if !_has_pidfd_open { + if _has_pidfd_open() { return process, .Unsupported } pidfd, errno := linux.pidfd_open(linux.Pid(pid), {}) if errno == .ENOSYS { - _has_pidfd_open = false + sync.atomic_store(&_default_has_pidfd_open, false) return process, .Unsupported } if errno != nil { @@ -289,7 +300,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { // search PATH if just a plain name is provided executable_name := desc.command[0] executable_path: cstring - if !strings.contains_rune(executable_name, '/') { + if strings.index_byte(executable_name, '/') == -1 { path_env := get_env("PATH", temp_allocator()) path_dirs := filepath.split_list(path_env, temp_allocator()) found: bool @@ -321,8 +332,8 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { // 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 + for command, i in desc.command { + cargs[i] = temp_cstring(command) or_return } // Use current process' environment if description didn't provide it. @@ -332,8 +343,8 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { 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 + for env, i in desc.env { + cenv[i] = temp_cstring(env) or_return } env = &cenv[0] } @@ -409,7 +420,7 @@ _process_state_update_times :: proc(p: Process, state: ^Process_State) -> (err: // 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 { + for _ in 0..<11 { stats = stats[strings.index_byte(stats, ' ') + 1:] } @@ -445,7 +456,7 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_stat // 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 _has_pidfd_open() { if process.handle == PIDFD_UNASSIGNED { pid_fd, errno = linux.pidfd_open(linux.Pid(process.pid), nil) if errno != .NONE && errno != .ENOSYS { @@ -481,7 +492,9 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_stat break } } else { - mask: bit_set[0..=63] + sync.atomic_store(&_default_has_pidfd_open, false) + + mask: bit_set[0..<64; u64] mask += { int(linux.Signal.SIGCHLD) - 1 } org_sigset: linux.Sig_Set From ecdd3887b28de42e9a023258a0c5ebbe26d3e7ab Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 24 Jul 2024 10:09:50 -0400 Subject: [PATCH 05/23] fix process_info assumptions --- core/os/os2/process_linux.odin | 103 ++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index f54528d69..f8da3ea08 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -114,12 +114,15 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator info.fields = selection - // Use this so we can use bprintf to make cstrings with less copying + // Use this so we can use bprintf to make cstrings with less copying. + // The path_backing is manually zero terminated as we go. 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) + path_slice := path_backing[:len(path_backing) - 1] + path_cstr := cstring(&path_slice[0]) + path_len := len(fmt.bprintf(path_slice, "/proc/%d", pid)) + // path_len unused here as path_backing[path_len] = 0 is assumed. + proc_fd, errno := linux.open(path_cstr, _OPENDIR_FLAGS) if errno != .NONE { err = _get_platform_error(errno) @@ -158,13 +161,63 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } } - if .Working_Dir in selection { - _ = fmt.bprintf(path_slice, "/proc/%d/cwd", pid) - info.working_dir, _ = _read_link_cstr(path_cstr, allocator) // allowed to fail + cmdline_if: if selection & {.Working_Dir, .Command_Line, .Command_Args, .Executable_Path} != {} { + path_len = len(fmt.bprintf(path_slice, "/proc/%d/cmdline", pid)) + path_backing[path_len] = 0 + + 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) + + 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 || (.Executable_Path in selection && command_line_exec[0] != '/') { + path_len = len(fmt.bprintf(path_slice, "/proc/%d/cwd", pid)) + path_backing[path_len] = 0 + + cwd, cwd_err = _read_link_cstr(path_cstr, temp_allocator()) // allowed to fail + if cwd_err == nil && .Working_Dir in selection { + info.working_dir = strings.clone(cwd, allocator) + } + } + + if .Executable_Path in selection { + if cmdline[0] == '/' { + info.executable_path = strings.clone(cmdline[:terminator], allocator) + } else if cwd_err == nil { + join_paths: [2]string = { cwd, cmdline[:terminator] } + info.executable_path = filepath.join(join_paths[:], allocator) + } + } + 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[:] + } } stat_if: if selection & {.PPid, .Priority} != {} { - _ = fmt.bprintf(path_slice, "/proc/%d/stat", pid) + path_len = len(fmt.bprintf(path_slice, "/proc/%d/stat", pid)) + path_backing[path_len] = 0 + proc_stat_bytes := _read_entire_pseudo_file(path_cstr, temp_allocator()) or_return if len(proc_stat_bytes) <= 0 { break stat_if @@ -195,38 +248,10 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } } - cmdline_if: if selection & {.Command_Line, .Command_Args, .Executable_Path} != {} { - _ = fmt.bprintf(path_slice, "/proc/%d/cmdline", pid) - 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 .Executable_Path in selection { - info.executable_path = strings.clone(cmdline[:terminator], allocator) - } - 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) + path_len = len(fmt.bprintf(path_slice, "/proc/%d/environ", pid)) + path_backing[path_len] = 0 + if env_bytes, env_err := _read_entire_pseudo_file(path_cstr, temp_allocator()); env_err == nil { env := string(env_bytes) From 95a8a4e7f027e5711fb5a8f63c8a5a9a94c06d0e Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 24 Jul 2024 10:12:16 -0400 Subject: [PATCH 06/23] typo - inverted logic --- core/os/os2/process_linux.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index f8da3ea08..89d54d272 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -285,7 +285,7 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime _process_open :: proc(pid: int, _: Process_Open_Flags) -> (process: Process, err: Error) { process.pid = pid process.handle = PIDFD_UNASSIGNED - if _has_pidfd_open() { + if !_has_pidfd_open() { return process, .Unsupported } From a5fa93e06d7ebcb9ec54e9f587043cbd2ac8606b Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 24 Jul 2024 10:23:23 -0400 Subject: [PATCH 07/23] remove ctprintf; use fmt.caprintf; fix pipe_linux that I broke. --- core/os/os2/internal_util.odin | 13 +------------ core/os/os2/pipe_linux.odin | 4 ++-- core/os/os2/process_linux.odin | 4 ++-- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/core/os/os2/internal_util.odin b/core/os/os2/internal_util.odin index c85a6bc10..e26cf7439 100644 --- a/core/os/os2/internal_util.odin +++ b/core/os/os2/internal_util.odin @@ -3,8 +3,7 @@ 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,16 +47,6 @@ temp_cstring :: proc(s: string) -> (cstring, runtime.Allocator_Error) { return clone_to_cstring(s, temp_allocator()) } -@(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) diff --git a/core/os/os2/pipe_linux.odin b/core/os/os2/pipe_linux.odin index 0442d17ac..20c9e37f0 100644 --- a/core/os/os2/pipe_linux.odin +++ b/core/os/os2/pipe_linux.odin @@ -10,8 +10,8 @@ _pipe :: proc() -> (r, w: ^File, err: Error) { return nil, nil,_get_platform_error(errno) } - r = _new_file(uintptr(fds[0])) - w = _new_file(uintptr(fds[1])) + r = _new_file(uintptr(fds[0])) or_return + w = _new_file(uintptr(fds[1])) or_return r_impl := (^File_Impl)(r.impl) r_impl.kind = .Pipe diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 89d54d272..7458d06d6 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -330,7 +330,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { path_dirs := filepath.split_list(path_env, temp_allocator()) found: bool for dir in path_dirs { - executable_path = ctprintf("%s/%s", dir, executable_name) + executable_path = fmt.caprintf("%s/%s", dir, executable_name, temp_allocator()) fail: bool if fail, errno = linux.faccessat(dir_fd, executable_path, linux.F_OK); errno == .NONE && !fail { found = true @@ -339,7 +339,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } if !found { // check in cwd to match windows behavior - executable_path = ctprintf("./%s", name) + executable_path = fmt.caprintf("./%s", name, temp_allocator()) fail: bool if fail, errno = linux.faccessat(dir_fd, executable_path, linux.F_OK); errno != .NONE || fail { return process, .Not_Exist From a03dffcd1aab0f13c538620ac3006953bbd2372e Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 30 Jul 2024 08:45:53 -0400 Subject: [PATCH 08/23] improve error handling; do not report errors from failed execve --- core/os/os2/process_linux.odin | 56 +++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 7458d06d6..0879f9b16 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -63,7 +63,7 @@ _get_ppid :: proc() -> int { } @(private="package") -_process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { +_process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) { TEMP_ALLOCATOR_GUARD() dir_fd: linux.Fd @@ -101,17 +101,21 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { if pid, ok := strconv.parse_int(d_name_str); ok { append(&dynamic_list, pid) + } else { + return nil, .Invalid_File } } } - return slice.clone(dynamic_list[:], allocator), nil + 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_GUARD() + info.pid = pid info.fields = selection // Use this so we can use bprintf to make cstrings with less copying. @@ -148,10 +152,15 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator 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) + uid: int + ok: bool + if uid, ok = strconv.parse_int(passwd[:n]); ok && uid == int(s.uid) { + info.username = strings.clone(username, allocator) or_return break } + if !ok { + return info, .Invalid_File + } eol := strings.index_byte(passwd, '\n') if eol == -1 { @@ -184,30 +193,31 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator cwd, cwd_err = _read_link_cstr(path_cstr, temp_allocator()) // allowed to fail if cwd_err == nil && .Working_Dir in selection { - info.working_dir = strings.clone(cwd, allocator) + info.working_dir = strings.clone(cwd, allocator) or_return } } if .Executable_Path in selection { if cmdline[0] == '/' { - info.executable_path = strings.clone(cmdline[:terminator], allocator) + info.executable_path = strings.clone(cmdline[:terminator], allocator) or_return } else if cwd_err == nil { join_paths: [2]string = { cwd, cmdline[:terminator] } info.executable_path = filepath.join(join_paths[:], allocator) } } if .Command_Line in selection { - info.command_line = strings.clone(cmdline[:terminator], allocator) + info.command_line = strings.clone(cmdline[:terminator], allocator) or_return } if .Command_Args in selection { // skip to first arg cmdline = cmdline[terminator + 1:] - arg_list := make([dynamic]string, allocator) + arg_list := make([dynamic]string, allocator) or_return for len(cmdline) > 0 { terminator = strings.index_byte(cmdline, 0) - append(&arg_list, strings.clone(cmdline[:terminator], allocator)) + arg := strings.clone(cmdline[:terminator], allocator) or_return + append(&arg_list, arg) or_return cmdline = cmdline[terminator + 1:] } info.command_args = arg_list[:] @@ -230,9 +240,11 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator stats = stats[strings.index_byte(stats, ' ') + 1:] if .PPid in selection { - ppid_str := stats[strings.index_byte(stats, ' '):] + ppid_str := stats[:strings.index_byte(stats, ' ')] if ppid, ok := strconv.parse_int(ppid_str); ok { info.ppid = ppid + } else { + return info, .Invalid_File } } @@ -241,9 +253,11 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator for _ in 4..<19 { stats = stats[strings.index_byte(stats, ' ') + 1:] } - nice_str := stats[strings.index_byte(stats, ' '):] + nice_str := stats[:strings.index_byte(stats, ' ')] if nice, ok := strconv.parse_int(nice_str); ok { info.priority = nice + } else { + return info, .Invalid_File } } } @@ -255,13 +269,14 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator if env_bytes, env_err := _read_entire_pseudo_file(path_cstr, temp_allocator()); env_err == nil { env := string(env_bytes) - env_list := make([dynamic]string, allocator) + env_list := make([dynamic]string, allocator) or_return for len(env) > 0 { terminator := strings.index_byte(env, 0) if terminator == -1 || terminator == 0 { break } - append(&env_list, strings.clone(env[:terminator], allocator)) + e := strings.clone(env[:terminator], allocator) or_return + append(&env_list, e) or_return env = env[terminator + 1:] } info.environment = env_list[:] @@ -417,12 +432,13 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } 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") + linux.exit(1) } unreachable() } + // TODO: We need to come up with a way to detect the execve failure from here. + process.pid = int(pid) process.handle = PIDFD_UNASSIGNED return @@ -455,8 +471,14 @@ _process_state_update_times :: proc(p: Process, state: ^Process_State) -> (err: 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) + 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 From 278a63caaa82c8d6a5385e41ef5817b620f438fd Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 30 Jul 2024 09:28:40 -0400 Subject: [PATCH 09/23] remove pidfd availability caching --- core/os/os2/process_linux.odin | 50 ++++++---------------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 0879f9b16..74b66f272 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -6,7 +6,6 @@ import "base:runtime" import "core:fmt" import "core:mem" -import "core:sync" import "core:time" import "core:slice" import "core:strings" @@ -16,17 +15,6 @@ import "core:path/filepath" PIDFD_UNASSIGNED :: ~uintptr(0) -_default_has_pidfd_open: bool = true - -// pidfd is still fairly new (Linux 5.3) -_has_pidfd_open :: proc () -> bool { - @thread_local has_pidfd_open: bool = true - if has_pidfd_open { - has_pidfd_open = sync.atomic_load(&_default_has_pidfd_open) - } - return has_pidfd_open -} - @(private="package") _exit :: proc "contextless" (code: int) -> ! { linux.exit_group(i32(code)) @@ -300,19 +288,14 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime _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 { - sync.atomic_store(&_default_has_pidfd_open, false) return process, .Unsupported } if errno != nil { return process, _get_platform_error(errno) } - process.handle = uintptr(pidfd) return } @@ -439,8 +422,10 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { // TODO: We need to come up with a way to detect the execve failure from here. - process.pid = int(pid) - process.handle = PIDFD_UNASSIGNED + process, err = process_open(int(pid)) + if err == .Unsupported { + return process, nil + } return } @@ -491,6 +476,7 @@ _process_state_update_times :: proc(p: Process, state: ^Process_State) -> (err: _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) { process_state.pid = process.pid + errno: linux.Errno options: linux.Wait_Options big_if: if timeout == 0 { options += {.WNOHANG} @@ -500,30 +486,14 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_stat 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)) - } + if process.handle != PIDFD_UNASSIGNED { pollfd: [1]linux.Poll_Fd = { { - fd = linux.Fd(pid_fd), + fd = linux.Fd(process.handle), events = {.IN}, }, } + for { n, e := linux.ppoll(pollfd[:], &ts, nil) if e == .EINTR { @@ -539,8 +509,6 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_stat break } } else { - sync.atomic_store(&_default_has_pidfd_open, false) - mask: bit_set[0..<64; u64] mask += { int(linux.Signal.SIGCHLD) - 1 } @@ -586,7 +554,7 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_stat } status: u32 - errno: linux.Errno = .EINTR + errno = .EINTR for errno == .EINTR { _, errno = linux.wait4(linux.Pid(process.pid), &status, options, nil) if errno != .NONE { From 792640df1f475527c0ef72f8439dfba9e1e3fb15 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 30 Jul 2024 09:41:49 -0400 Subject: [PATCH 10/23] remove File_Impl_Kind from file_linux --- core/os/os2/file_linux.odin | 16 +++++----------- core/os/os2/pipe_linux.odin | 5 ----- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin index 94477d87e..9f1559fdd 100644 --- a/core/os/os2/file_linux.odin +++ b/core/os/os2/file_linux.odin @@ -6,16 +6,10 @@ 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, } @@ -213,18 +207,18 @@ _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 { return -1, _get_platform_error(errno) } - return i64(s.size), nil + + if s.mode & linux.S_IFMT == linux.S_IFREG { + return i64(s.size), nil + } + return 0, .No_Size } _sync :: proc(f: ^File) -> Error { diff --git a/core/os/os2/pipe_linux.odin b/core/os/os2/pipe_linux.odin index 20c9e37f0..42315cf4e 100644 --- a/core/os/os2/pipe_linux.odin +++ b/core/os/os2/pipe_linux.odin @@ -13,10 +13,5 @@ _pipe :: proc() -> (r, w: ^File, err: Error) { r = _new_file(uintptr(fds[0])) or_return w = _new_file(uintptr(fds[1])) or_return - r_impl := (^File_Impl)(r.impl) - r_impl.kind = .Pipe - w_impl := (^File_Impl)(w.impl) - w_impl.kind = .Pipe - return } From c7eb2ae6bb9b0df2b28c9b65e3edbd9abfb6179f Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 30 Jul 2024 10:19:09 -0400 Subject: [PATCH 11/23] use sys/linux dirent instead of manual iteration --- core/os/os2/path_linux.odin | 33 +++++++++------------------------ core/os/os2/process_linux.odin | 11 +++-------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/core/os/os2/path_linux.odin b/core/os/os2/path_linux.odin index be60f9b86..ba2f7235c 100644 --- a/core/os/os2/path_linux.odin +++ b/core/os/os2/path_linux.odin @@ -1,6 +1,7 @@ //+private package os2 +import "core:strings" import "core:strconv" import "base:runtime" import "core:sys/linux" @@ -75,14 +76,6 @@ _mkdir_all :: proc(path: string, perm: int) -> Error { return nil if has_created else .Exist } -dirent64 :: struct { - d_ino: u64, - d_off: u64, - d_reclen: u16, - d_type: u8, - d_name: [1]u8, -} - _remove_all :: proc(path: string) -> Error { DT_DIR :: 4 @@ -105,26 +98,18 @@ _remove_all :: proc(path: string) -> Error { return _get_platform_error(errno) } - d: ^dirent64 + offset: int + for d in linux.dirent_iterate_buf(buf[:buflen], &offset) { + d_name_str := linux.dirent_name(d) + d_name_cstr := strings.unsafe_string_to_cstring(d_name_str) - for i := 0; i < buflen; i += int(d.d_reclen) { - d = (^dirent64)(rawptr(&buf[i])) - d_name_cstr := cstring(&d.d_name[0]) - - buf_len := uintptr(d.d_reclen) - offset_of(d.d_name) - - /* check for current directory (.) */ - #no_bounds_check if buf_len > 1 && d.d_name[0] == '.' && d.d_name[1] == 0 { + /* check for current or parent directory (. or ..) */ + if d_name_str == "." || d_name_str == ".." { continue } - /* check for parent directory (..) */ - #no_bounds_check if buf_len > 2 && d.d_name[0] == '.' && d.d_name[1] == '.' && d.d_name[2] == 0 { - continue - } - - switch d.d_type { - case DT_DIR: + #partial switch d.type { + case .DIR: new_dfd: linux.Fd new_dfd, errno = linux.openat(dfd, d_name_cstr, _OPENDIR_FLAGS) if errno != .NONE { diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 74b66f272..eed600c48 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -80,17 +80,12 @@ _process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) 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)]) + 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) - } else { - return nil, .Invalid_File } } } From 38b96a7981ca465ed5e7f3227e182822a19f54dc Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 3 Aug 2024 17:50:47 -0400 Subject: [PATCH 12/23] change child error behavior to trap instead of exit --- core/os/os2/process_linux.odin | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index eed600c48..0efb19042 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -3,6 +3,7 @@ package os2 import "base:runtime" +import "base:intrinsics" import "core:fmt" import "core:mem" @@ -393,26 +394,26 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if desc.stdin != nil { fd := linux.Fd(fd(desc.stdin)) if _, errno = linux.dup2(fd, STDIN); errno != .NONE { - linux.exit(1) + intrinsics.trap() } } if desc.stdout != nil { fd := linux.Fd(fd(desc.stdout)) if _, errno = linux.dup2(fd, STDOUT); errno != .NONE { - linux.exit(1) + intrinsics.trap() } } if desc.stderr != nil { fd := linux.Fd(fd(desc.stderr)) if _, errno = linux.dup2(fd, STDERR); errno != .NONE { - linux.exit(1) + intrinsics.trap() } } if errno = linux.execveat(dir_fd, executable_path, &cargs[0], env); errno != .NONE { - linux.exit(1) + intrinsics.trap() } - unreachable() + intrinsics.trap() // unreachable } // TODO: We need to come up with a way to detect the execve failure from here. From 2b89829b5259a8e7120514f9d9592a82271dfaee Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 3 Aug 2024 18:08:00 -0400 Subject: [PATCH 13/23] minor edits in process_linux.odin --- core/os/os2/process_linux.odin | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 0efb19042..553be5fda 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -141,8 +141,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator if uid, ok = strconv.parse_int(passwd[:n]); ok && uid == int(s.uid) { info.username = strings.clone(username, allocator) or_return break - } - if !ok { + } else if !ok { return info, .Invalid_File } @@ -413,7 +412,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if errno = linux.execveat(dir_fd, executable_path, &cargs[0], env); errno != .NONE { intrinsics.trap() } - intrinsics.trap() // unreachable + unreachable() } // TODO: We need to come up with a way to detect the execve failure from here. From 2a7db08c20811139c9410b65499d27eca47e793c Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 4 Aug 2024 00:59:40 -0400 Subject: [PATCH 14/23] Remove returned bool from access and faccessat in sys/linux. Switch to using AT_EMPTY_PATH to execve with file descriptors. --- core/os/os2/file_linux.odin | 3 +- core/os/os2/process_linux.odin | 52 ++++++++++++++++++++++++---------- core/sys/linux/sys.odin | 14 ++++----- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin index 9f1559fdd..8887c8274 100644 --- a/core/os/os2/file_linux.odin +++ b/core/os/os2/file_linux.odin @@ -386,8 +386,7 @@ _fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error { _exists :: proc(name: string) -> bool { TEMP_ALLOCATOR_GUARD() name_cstr, _ := temp_cstring(name) - res, errno := linux.access(name_cstr, linux.F_OK) - return !res && errno == .NONE + return linux.access(name_cstr, linux.F_OK) == .NONE } /* For reading Linux system files that stat to size 0 */ diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 553be5fda..f645131b2 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -300,6 +300,12 @@ _Sys_Process_Attributes :: struct {} @(private="package") _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { + _has_executable_permissions :: proc(fd: linux.Fd) -> bool { + backing: [48]u8 + _ = fmt.bprintf(backing[:], "/proc/self/fd/%d", fd) + return linux.access(cstring(&backing[0]), linux.X_OK) == .NONE + } + TEMP_ALLOCATOR_GUARD() if len(desc.command) == 0 { @@ -314,38 +320,54 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { 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_fd: linux.Fd executable_name := desc.command[0] - executable_path: cstring if strings.index_byte(executable_name, '/') == -1 { 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 = fmt.caprintf("%s/%s", dir, executable_name, temp_allocator()) - fail: bool - if fail, errno = linux.faccessat(dir_fd, executable_path, linux.F_OK); errno == .NONE && !fail { - found = true - break + exe_path := fmt.caprintf("%s/%s", dir, executable_name, allocator=temp_allocator()) + if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { + continue } + if !_has_executable_permissions(exe_fd) { + linux.close(exe_fd) + continue + } + found = true + break } if !found { // check in cwd to match windows behavior - executable_path = fmt.caprintf("./%s", name, temp_allocator()) - fail: bool - if fail, errno = linux.faccessat(dir_fd, executable_path, linux.F_OK); errno != .NONE || fail { + exe_path := fmt.caprintf("./%s", executable_name, allocator=temp_allocator()) + if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { return process, .Not_Exist } + if !_has_executable_permissions(exe_fd) { + linux.close(exe_fd) + return process, .Permission_Denied + } } } else { - executable_path = temp_cstring(executable_name) or_return + exe_path := temp_cstring(executable_name) or_return + if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { + return process, _get_platform_error(errno) + } + if !_has_executable_permissions(exe_fd) { + linux.close(exe_fd) + return process, .Permission_Denied + } } - 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) - } + // At this point, we have an executable. + defer linux.close(exe_fd) // args and environment need to be a list of cstrings // that are terminated by a nil pointer. @@ -409,7 +431,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } } - if errno = linux.execveat(dir_fd, executable_path, &cargs[0], env); errno != .NONE { + if errno = linux.execveat(exe_fd, "", &cargs[0], env, {.AT_EMPTY_PATH}); errno != .NONE { intrinsics.trap() } unreachable() diff --git a/core/sys/linux/sys.odin b/core/sys/linux/sys.odin index ec7357c48..d756f0ece 100644 --- a/core/sys/linux/sys.odin +++ b/core/sys/linux/sys.odin @@ -279,13 +279,13 @@ writev :: proc "contextless" (fd: Fd, iov: []IO_Vec) -> (int, Errno) { Available since Linux 1.0. For ARM64 available since Linux 2.6.16. */ -access :: proc "contextless" (name: cstring, mode: Mode = F_OK) -> (bool, Errno) { +access :: proc "contextless" (name: cstring, mode: Mode = F_OK) -> (Errno) { when ODIN_ARCH == .arm64 { ret := syscall(SYS_faccessat, AT_FDCWD, cast(rawptr) name, transmute(u32) mode) - return errno_unwrap(ret, bool) + return Errno(-ret) } else { ret := syscall(SYS_access, cast(rawptr) name, transmute(u32) mode) - return errno_unwrap(ret, bool) + return Errno(-ret) } } @@ -2616,9 +2616,9 @@ fchmodat :: proc "contextless" (dirfd: Fd, name: cstring, mode: Mode, flags: FD_ Checks the user permissions for a file at specified dirfd. Available since Linux 2.6.16. */ -faccessat :: proc "contextless" (dirfd: Fd, name: cstring, mode: Mode = F_OK) -> (bool, Errno) { +faccessat :: proc "contextless" (dirfd: Fd, name: cstring, mode: Mode = F_OK) -> (Errno) { ret := syscall(SYS_faccessat, dirfd, cast(rawptr) name, transmute(u32) mode) - return errno_unwrap(ret, bool) + return Errno(-ret) } /* @@ -2916,9 +2916,9 @@ pidfd_getfd :: proc "contextless" (pidfd: Pid_FD, fd: Fd, flags: i32 = 0) -> (Fd Checks the user permissions for a file at specified dirfd (with flags). Available since Linux 5.8. */ -faccessat2 :: proc "contextless" (dirfd: Fd, name: cstring, mode: Mode = F_OK, flags: FD_Flags = FD_Flags{}) -> (bool, Errno) { +faccessat2 :: proc "contextless" (dirfd: Fd, name: cstring, mode: Mode = F_OK, flags: FD_Flags = FD_Flags{}) -> (Errno) { ret := syscall(SYS_faccessat2, dirfd, cast(rawptr) name, transmute(u32) mode, transmute(i32) flags) - return errno_unwrap(ret, bool) + return Errno(-ret) } // TODO(flysand): process_madvise From c691c7dc68c517068e024df34ac166fd19d2ea0b Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 4 Aug 2024 01:47:10 -0400 Subject: [PATCH 15/23] point stdin, stdout, stderr to /dev/null if unused in os2.process_start --- core/os/os2/process_linux.odin | 48 ++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index f645131b2..b3e9f1852 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -412,23 +412,51 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if pid == 0 { // in child process now + stdin_fd: linux.Fd + stdout_fd: linux.Fd + stderr_fd: linux.Fd + if desc.stdin != nil { - fd := linux.Fd(fd(desc.stdin)) - if _, errno = linux.dup2(fd, STDIN); errno != .NONE { - intrinsics.trap() + stdin_fd = linux.Fd(fd(desc.stdin)) + } else { + stdin_fd, errno = linux.open("/dev/null", {}) + if errno != nil { + intrinsics.trap() // TODO: our own special pipe } } + + write_devnull: linux.Fd = -1 + if desc.stdout != nil { - fd := linux.Fd(fd(desc.stdout)) - if _, errno = linux.dup2(fd, STDOUT); errno != .NONE { - intrinsics.trap() + stdout_fd = linux.Fd(fd(desc.stdout)) + } else { + write_devnull, errno = linux.open("/dev/null", {.WRONLY}) + if errno != nil { + intrinsics.trap() // TODO } + stdout_fd = write_devnull } + if desc.stderr != nil { - fd := linux.Fd(fd(desc.stderr)) - if _, errno = linux.dup2(fd, STDERR); errno != .NONE { - intrinsics.trap() + stderr_fd = linux.Fd(fd(desc.stderr)) + } else { + if write_devnull == -1 { + write_devnull, errno = linux.open("/dev/null", {.WRONLY}) + if errno != nil { + intrinsics.trap() // TODO + } } + stderr_fd = write_devnull + } + + if _, errno = linux.dup2(stdin_fd, STDIN); errno != .NONE { + intrinsics.trap() + } + if _, errno = linux.dup2(stdout_fd, STDOUT); errno != .NONE { + intrinsics.trap() + } + if _, errno = linux.dup2(stderr_fd, STDERR); errno != .NONE { + intrinsics.trap() } if errno = linux.execveat(exe_fd, "", &cargs[0], env, {.AT_EMPTY_PATH}); errno != .NONE { @@ -437,8 +465,6 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { unreachable() } - // TODO: We need to come up with a way to detect the execve failure from here. - process, err = process_open(int(pid)) if err == .Unsupported { return process, nil From c3ba8fbd094654fd6bd4695027dcd843d3b9306a Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 8 Aug 2024 08:56:29 -0400 Subject: [PATCH 16/23] add child pipe for reporting post-fork errors --- core/os/os2/process_linux.odin | 64 +++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index b3e9f1852..346497289 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -288,7 +288,7 @@ _process_open :: proc(pid: int, _: Process_Open_Flags) -> (process: Process, err if errno == .ENOSYS { return process, .Unsupported } - if errno != nil { + if errno != .NONE { return process, _get_platform_error(errno) } process.handle = uintptr(pidfd) @@ -300,7 +300,7 @@ _Sys_Process_Attributes :: struct {} @(private="package") _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { - _has_executable_permissions :: proc(fd: linux.Fd) -> bool { + has_executable_permissions :: proc(fd: linux.Fd) -> bool { backing: [48]u8 _ = fmt.bprintf(backing[:], "/proc/self/fd/%d", fd) return linux.access(cstring(&backing[0]), linux.X_OK) == .NONE @@ -337,7 +337,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { continue } - if !_has_executable_permissions(exe_fd) { + if !has_executable_permissions(exe_fd) { linux.close(exe_fd) continue } @@ -350,7 +350,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { return process, .Not_Exist } - if !_has_executable_permissions(exe_fd) { + if !has_executable_permissions(exe_fd) { linux.close(exe_fd) return process, .Permission_Denied } @@ -360,7 +360,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { return process, _get_platform_error(errno) } - if !_has_executable_permissions(exe_fd) { + if !has_executable_permissions(exe_fd) { linux.close(exe_fd) return process, .Permission_Denied } @@ -410,8 +410,22 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { STDOUT :: linux.Fd(1) STDERR :: linux.Fd(2) + READ :: 0 + WRITE :: 1 + + child_pipe_fds: [2]linux.Fd + if errno = linux.pipe2(&child_pipe_fds, {.CLOEXEC}); errno != .NONE { + return process, _get_platform_error(errno) + } + 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(i32(errno) * -1) } + linux.write(parent_fd, error_byte[:]) + intrinsics.trap() + } + stdin_fd: linux.Fd stdout_fd: linux.Fd stderr_fd: linux.Fd @@ -420,8 +434,8 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { stdin_fd = linux.Fd(fd(desc.stdin)) } else { stdin_fd, errno = linux.open("/dev/null", {}) - if errno != nil { - intrinsics.trap() // TODO: our own special pipe + if errno != .NONE { + write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno) } } @@ -431,8 +445,8 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { stdout_fd = linux.Fd(fd(desc.stdout)) } else { write_devnull, errno = linux.open("/dev/null", {.WRONLY}) - if errno != nil { - intrinsics.trap() // TODO + if errno != .NONE { + write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno) } stdout_fd = write_devnull } @@ -442,33 +456,51 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } else { if write_devnull == -1 { write_devnull, errno = linux.open("/dev/null", {.WRONLY}) - if errno != nil { - intrinsics.trap() // TODO + 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 { - intrinsics.trap() + write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno) } if _, errno = linux.dup2(stdout_fd, STDOUT); errno != .NONE { - intrinsics.trap() + write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno) } if _, errno = linux.dup2(stderr_fd, STDERR); errno != .NONE { - intrinsics.trap() + write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno) } + success_byte: [1]u8 + linux.write(child_pipe_fds[WRITE], success_byte[:]) + if errno = linux.execveat(exe_fd, "", &cargs[0], env, {.AT_EMPTY_PATH}); errno != .NONE { - intrinsics.trap() + write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno) } unreachable() } + linux.close(child_pipe_fds[WRITE]) + defer linux.close(child_pipe_fds[WRITE]) + + n: int + child_byte: [1]u8 + n, errno = linux.read(child_pipe_fds[READ], child_byte[:]) + if errno != .NONE { + return process, _get_platform_error(errno) + } + child_errno := linux.Errno(child_byte[0]) + if child_errno != .NONE { + return process, _get_platform_error(child_errno) + } + process, err = process_open(int(pid)) if err == .Unsupported { return process, nil } + return } @@ -529,7 +561,7 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (process_stat time_nsec = uint(timeout % time.Second), } - if process.handle != PIDFD_UNASSIGNED { + if false {//process.handle != PIDFD_UNASSIGNED { pollfd: [1]linux.Poll_Fd = { { fd = linux.Fd(process.handle), From c4d43bbab081ae1480be8f07f88572d8315bd94a Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 9 Aug 2024 09:16:44 -0400 Subject: [PATCH 17/23] os2 linux process_wait rework; add Sig_Child_Code to sys/linux bits --- core/os/os2/process_linux.odin | 251 +++++++++++++++++++++------------ core/sys/linux/bits.odin | 14 ++ 2 files changed, 171 insertions(+), 94 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 346497289..0fa96a5a5 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -6,7 +6,6 @@ import "base:runtime" import "base:intrinsics" import "core:fmt" -import "core:mem" import "core:time" import "core:slice" import "core:strings" @@ -504,11 +503,11 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { return } -_process_state_update_times :: proc(p: Process, state: ^Process_State) -> (err: Error) { +_process_state_update_times :: proc(state: ^Process_State) -> (err: Error) { TEMP_ALLOCATOR_GUARD() stat_path_buf: [32]u8 - _ = fmt.bprintf(stat_path_buf[:], "/proc/%d/stat", p.pid) + _ = fmt.bprintf(stat_path_buf[:], "/proc/%d/stat", state.pid) stat_buf: []u8 stat_buf, err = _read_entire_pseudo_file(cstring(&stat_path_buf[0]), temp_allocator()) if err != nil { @@ -547,115 +546,179 @@ _process_state_update_times :: proc(p: Process, state: ^Process_State) -> (err: return } -@(private="package") -_process_wait :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) { - process_state.pid = process.pid +_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: + 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) + return + } - errno: linux.Errno - 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), } - if false {//process.handle != PIDFD_UNASSIGNED { - pollfd: [1]linux.Poll_Fd = { - { - fd = linux.Fd(process.handle), - 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..<64; u64] - 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 - } - } + n, errno := linux.ppoll(pollfd[:], &ts, &sigchld_set) + if errno != .NONE { + if errno == .EINTR { + new_tick := time.tick_now() + timeout -= time.tick_diff(start_tick, new_tick) + start_tick = new_tick + continue } + return process_state, _get_platform_error(errno) } + + if n == 0 { // timeout with no events + _process_state_update_times(&process_state) + return + } + + // This throws EBADF with pidfd + if errno = linux.waitid(.PID, linux.Id(process.pid), &info, {.WEXITED, .WNOHANG, .WNOWAIT}, nil); errno != .NONE { + return process_state, _get_platform_error(errno) + } + + if info.signo == .SIGCHLD { + break + } + + new_tick := time.tick_now() + timeout -= time.tick_diff(start_tick, new_tick) + start_tick = new_tick } - status: u32 - 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 _reap_terminated(process) +} + +_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) + 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) + return + case .EINTR: + new_tick := time.tick_now() + timeout -= time.tick_diff(start_tick, new_tick) + start_tick = new_tick + case .EINVAL: return process_state, _get_platform_error(errno) } } - _process_state_update_times(process, &process_state) + return _reap_terminated(process) +} - // 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 +@(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) + } } - // terminated by signal - if linux.WIFSIGNALED(status) { - process_state.exited = false - process_state.exit_code = int(linux.WTERMSIG(status)) - process_state.success = false - return + process_state: Process_State = { + pid = process.pid, } - return + + 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, nil + } + if errno != .NONE { + return process_state, _get_platform_error(errno) + } + + return _reap_terminated(process) } @(private="package") diff --git a/core/sys/linux/bits.odin b/core/sys/linux/bits.odin index e10edf558..b8ec3c133 100644 --- a/core/sys/linux/bits.odin +++ b/core/sys/linux/bits.odin @@ -983,6 +983,20 @@ Sig_Action_Flag :: enum u32 { RESETHAND = 31, } +/* + Translation of code in Sig_Info for when signo is SIGCHLD +*/ +Sig_Child_Code :: enum { + NONE, + EXITED, + KILLED, + DUMPED, + TRAPPED, + STOPPED, + CONTINUED, +} + + /* Type of socket to create - For TCP you want to use SOCK_STREAM From baacc512e310004652539339b56dd2e26c44877b Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 9 Aug 2024 15:21:11 -0400 Subject: [PATCH 18/23] fix child pipe in process_start --- core/os/os2/process_linux.odin | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 0fa96a5a5..6ce4ece5d 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -388,6 +388,11 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { 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) + } + // TODO: This is the traditional textbook implementation with fork. // A more efficient implementation with vfork: // @@ -412,15 +417,10 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { READ :: 0 WRITE :: 1 - child_pipe_fds: [2]linux.Fd - if errno = linux.pipe2(&child_pipe_fds, {.CLOEXEC}); errno != .NONE { - return process, _get_platform_error(errno) - } - 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(i32(errno) * -1) } + error_byte: [1]u8 = { u8(errno) } linux.write(parent_fd, error_byte[:]) intrinsics.trap() } From f7a73b9555dd686852b5444047f082c456c8f877 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 10 Aug 2024 08:55:40 -0400 Subject: [PATCH 19/23] fix error handling; close read end of the child pipe instead of the write end twice --- core/os/os2/process_linux.odin | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 6ce4ece5d..41b46b235 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -61,6 +61,9 @@ _process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) return {}, .Invalid_Dir case .ENOENT: return {}, .Not_Exist + case .NONE: + case: + return {}, _get_platform_error(errno) } defer linux.close(dir_fd) @@ -482,16 +485,31 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } linux.close(child_pipe_fds[WRITE]) - defer linux.close(child_pipe_fds[WRITE]) + defer linux.close(child_pipe_fds[READ]) + + process.pid = int(pid) n: int child_byte: [1]u8 - n, errno = linux.read(child_pipe_fds[READ], child_byte[:]) - if errno != .NONE { - return process, _get_platform_error(errno) + errno = .EINTR + for errno == .EINTR { + n, errno = linux.read(child_pipe_fds[READ], child_byte[:]) } + if errno != .NONE { + child_state, _ := process_wait(process, 0) + if child_state.exited { + process.pid = 0 // If the child exited, we reaped it. + return process, _get_platform_error(errno) + } + // else.. something weird happened, but there IS a running process. + // Do not return the read error so the user knows to wait on it. + } + 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) } @@ -559,6 +577,7 @@ _reap_terminated :: proc(process: Process) -> (state: Process_State, err: Error) switch linux.Sig_Child_Code(info.code) { case .NONE, .CONTINUED, .STOPPED: + unreachable() case .EXITED: state.exited = true state.exit_code = int(info.status) From e54d6e5a1123281ef23d4f0f59d829c7421b5190 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 10 Aug 2024 09:03:13 -0400 Subject: [PATCH 20/23] Ignore process_open errors in process_start. This enforces a contract with the user that any process returned without error must eventually be waited on. --- core/os/os2/process_linux.odin | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 41b46b235..e64678f74 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -513,11 +513,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { return process, _get_platform_error(child_errno) } - process, err = process_open(int(pid)) - if err == .Unsupported { - return process, nil - } - + process, _ = process_open(int(pid)) return } From a52f7c129f8091a4df0d1e6effe068813bc347f4 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 10 Aug 2024 09:13:07 -0400 Subject: [PATCH 21/23] stop trying to handle child pipe read errors in process_start --- core/os/os2/process_linux.odin | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index e64678f74..72acf71ff 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -495,22 +495,17 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { for errno == .EINTR { n, errno = linux.read(child_pipe_fds[READ], child_byte[:]) } - if errno != .NONE { - child_state, _ := process_wait(process, 0) - if child_state.exited { - process.pid = 0 // If the child exited, we reaped it. - return process, _get_platform_error(errno) - } - // else.. something weird happened, but there IS a running process. - // Do not return the read error so the user knows to wait on it. - } - 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) + // 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)) From 0f052dbde797721fec3f937a9626571762fa033d Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 14 Aug 2024 00:45:25 -0400 Subject: [PATCH 22/23] os2/process_linux: improve error handling, use pidfd where possible, remove usage of fmt --- core/os/os2/errors.odin | 2 + core/os/os2/process_linux.odin | 279 +++++++++++++++++++++++---------- 2 files changed, 202 insertions(+), 79 deletions(-) diff --git a/core/os/os2/errors.odin b/core/os/os2/errors.odin index bc51bb1e8..f90baa699 100644 --- a/core/os/os2/errors.odin +++ b/core/os/os2/errors.odin @@ -23,6 +23,7 @@ General_Error :: enum u32 { Invalid_Dir, Invalid_Path, Invalid_Callback, + Invalid_Command, Pattern_Has_Separator, @@ -69,6 +70,7 @@ error_string :: proc(ferr: Error) -> string { case .Invalid_Dir: return "invalid directory" case .Invalid_Path: return "invalid path" case .Invalid_Callback: return "invalid callback" + case .Invalid_Command: return "invalid command" case .Unsupported: return "unsupported" case .Pattern_Has_Separator: return "pattern has separator" } diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 72acf71ff..1ca6da9f3 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -5,7 +5,6 @@ package os2 import "base:runtime" import "base:intrinsics" -import "core:fmt" import "core:time" import "core:slice" import "core:strings" @@ -102,29 +101,35 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator TEMP_ALLOCATOR_GUARD() info.pid = pid - info.fields = selection - // Use this so we can use bprintf to make cstrings with less copying. - // The path_backing is manually zero terminated as we go. + // Use this to make cstrings without copying. path_backing: [48]u8 + path_builder := strings.builder_from_bytes(path_backing[:]) - path_slice := path_backing[:len(path_backing) - 1] - path_cstr := cstring(&path_slice[0]) - path_len := len(fmt.bprintf(path_slice, "/proc/%d", pid)) - // path_len unused here as path_backing[path_len] = 0 is assumed. - - proc_fd, errno := linux.open(path_cstr, _OPENDIR_FLAGS) + strings.write_string(&path_builder, "/proc/") + strings.write_int(&path_builder, pid) + proc_fd, errno := linux.open(strings.to_cstring(&path_builder), _OPENDIR_FLAGS) if errno != .NONE { err = _get_platform_error(errno) return } defer linux.close(proc_fd) - if .Username in selection { + username_if: if .Username in selection { s: linux.Stat - linux.fstat(proc_fd, &s) + 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_bytes := _read_entire_pseudo_file_cstring("/etc/passwd", temp_allocator()) or_return passwd := string(passwd_bytes) for len(passwd) > 0 { n := strings.index_byte(passwd, ':') @@ -142,9 +147,11 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator ok: bool 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 { - return info, .Invalid_File + err = .Invalid_File + break username_if } eol := strings.index_byte(passwd, '\n') @@ -156,16 +163,20 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator } cmdline_if: if selection & {.Working_Dir, .Command_Line, .Command_Args, .Executable_Path} != {} { - path_len = len(fmt.bprintf(path_slice, "/proc/%d/cmdline", pid)) - path_backing[path_len] = 0 + 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 := _read_entire_pseudo_file(path_cstr, temp_allocator()) or_return - if len(cmdline_bytes) == 0 { + cmdline_bytes, cmdline_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder), 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] @@ -173,85 +184,152 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator cwd: string cwd_err: Error if .Working_Dir in selection || (.Executable_Path in selection && command_line_exec[0] != '/') { - path_len = len(fmt.bprintf(path_slice, "/proc/%d/cwd", pid)) - path_backing[path_len] = 0 + 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(path_cstr, temp_allocator()) // allowed to fail + cwd, cwd_err = _read_link_cstr(strings.to_cstring(&path_builder), 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 .Executable_Path in selection { if cmdline[0] == '/' { info.executable_path = strings.clone(cmdline[:terminator], allocator) or_return + info.fields += {.Executable_Path} } else if cwd_err == nil { - join_paths: [2]string = { cwd, cmdline[:terminator] } - info.executable_path = filepath.join(join_paths[:], allocator) + info.executable_path = filepath.join({ cwd, cmdline[:terminator] }, allocator) + info.fields += {.Executable_Path} + } else { + break cmdline_if } } - if .Command_Line in selection { - info.command_line = strings.clone(cmdline[:terminator], allocator) or_return - } - if .Command_Args in selection { + if selection & {.Command_Line, .Command_Args} != {} { // skip to first arg - cmdline = cmdline[terminator + 1:] + //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 == -1 { + 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 + } + } - arg_list := make([dynamic]string, allocator) or_return - for len(cmdline) > 0 { - terminator = strings.index_byte(cmdline, 0) - arg := strings.clone(cmdline[:terminator], allocator) or_return - append(&arg_list, arg) or_return cmdline = cmdline[terminator + 1:] } - info.command_args = arg_list[:] + info.command_line = strings.to_string(command_line_builder) + info.command_args = command_args_list[:] } } stat_if: if selection & {.PPid, .Priority} != {} { - path_len = len(fmt.bprintf(path_slice, "/proc/%d/stat", pid)) - path_backing[path_len] = 0 + 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 := _read_entire_pseudo_file(path_cstr, temp_allocator()) or_return + proc_stat_bytes, stat_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder), temp_allocator()) + if stat_err != nil { + err = stat_err + break stat_if + } 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:]) + // 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 + } - // We are now on the 3rd field (skip) - stats = stats[strings.index_byte(stats, ' ') + 1:] + // 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 { - ppid_str := stats[:strings.index_byte(stats, ' ')] - if ppid, ok := strconv.parse_int(ppid_str); ok { + if ppid, ok := strconv.parse_int(stat_fields[Fields.PPid]); ok { info.ppid = ppid + info.fields += {.PPid} } else { - return info, .Invalid_File + err = .Invalid_File + break stat_if } } if .Priority in selection { - // On 4th field. Priority is field 18 and niceness is field 19. - for _ in 4..<19 { - stats = stats[strings.index_byte(stats, ' ') + 1:] - } - nice_str := stats[:strings.index_byte(stats, ' ')] - if nice, ok := strconv.parse_int(nice_str); ok { + if nice, ok := strconv.parse_int(stat_fields[Fields.Nice]); ok { info.priority = nice + info.fields += {.Priority} } else { - return info, .Invalid_File + err = .Invalid_File + break stat_if } } } if .Environment in selection { - path_len = len(fmt.bprintf(path_slice, "/proc/%d/environ", pid)) - path_backing[path_len] = 0 + 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(path_cstr, temp_allocator()); env_err == nil { + if env_bytes, env_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder), temp_allocator()); env_err == nil { env := string(env_bytes) env_list := make([dynamic]string, allocator) or_return @@ -265,6 +343,9 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator env = env[terminator + 1:] } info.environment = env_list[:] + info.fields += {.Environment} + } else if err == nil { + err = env_err } } @@ -304,14 +385,16 @@ _Sys_Process_Attributes :: struct {} _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { has_executable_permissions :: proc(fd: linux.Fd) -> bool { backing: [48]u8 - _ = fmt.bprintf(backing[:], "/proc/self/fd/%d", fd) - return linux.access(cstring(&backing[0]), linux.X_OK) == .NONE + b := strings.builder_from_bytes(backing[:]) + strings.write_string(&b, "/proc/self/fd/") + strings.write_int(&b, int(fd)) + return linux.access(strings.to_cstring(&b), linux.X_OK) == .NONE } TEMP_ALLOCATOR_GUARD() if len(desc.command) == 0 { - return process, .Invalid_File + return process, .Invalid_Command } dir_fd := linux.AT_FDCWD @@ -333,9 +416,16 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { path_env := get_env("PATH", temp_allocator()) path_dirs := filepath.split_list(path_env, temp_allocator()) + exe_builder := strings.builder_make(temp_allocator()) + found: bool for dir in path_dirs { - exe_path := fmt.caprintf("%s/%s", dir, executable_name, allocator=temp_allocator()) + 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) if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { continue } @@ -348,7 +438,11 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { } if !found { // check in cwd to match windows behavior - exe_path := fmt.caprintf("./%s", executable_name, allocator=temp_allocator()) + strings.builder_reset(&exe_builder) + strings.write_string(&exe_builder, "./") + strings.write_string(&exe_builder, executable_name) + + exe_path := strings.to_cstring(&exe_builder) if exe_fd, errno = linux.openat(dir_fd, exe_path, {.PATH, .CLOEXEC}); errno != .NONE { return process, .Not_Exist } @@ -395,6 +489,9 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { if errno = linux.pipe2(&child_pipe_fds, {.CLOEXEC}); errno != .NONE { return process, _get_platform_error(errno) } + defer linux.close(child_pipe_fds[WRITE]) + defer linux.close(child_pipe_fds[READ]) + // TODO: This is the traditional textbook implementation with fork. // A more efficient implementation with vfork: @@ -484,9 +581,6 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { unreachable() } - linux.close(child_pipe_fds[WRITE]) - defer linux.close(child_pipe_fds[READ]) - process.pid = int(pid) n: int @@ -516,9 +610,13 @@ _process_state_update_times :: proc(state: ^Process_State) -> (err: Error) { TEMP_ALLOCATOR_GUARD() stat_path_buf: [32]u8 - _ = fmt.bprintf(stat_path_buf[:], "/proc/%d/stat", state.pid) + 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(cstring(&stat_path_buf[0]), temp_allocator()) + stat_buf, err = _read_entire_pseudo_file(strings.to_cstring(&path_builder), temp_allocator()) if err != nil { return } @@ -603,6 +701,7 @@ _timed_wait_on_handle :: proc(process: Process, timeout: time.Duration) -> (proc for { if timeout <= 0 { _process_state_update_times(&process_state) + err = .Timeout return } @@ -614,9 +713,8 @@ _timed_wait_on_handle :: proc(process: Process, timeout: time.Duration) -> (proc n, errno := linux.ppoll(pollfd[:], &ts, &sigchld_set) if errno != .NONE { if errno == .EINTR { - new_tick := time.tick_now() - timeout -= time.tick_diff(start_tick, new_tick) - start_tick = new_tick + timeout -= time.tick_since(start_tick) + start_tick = time.tick_now() continue } return process_state, _get_platform_error(errno) @@ -624,11 +722,11 @@ _timed_wait_on_handle :: proc(process: Process, timeout: time.Duration) -> (proc if n == 0 { // timeout with no events _process_state_update_times(&process_state) + err = .Timeout return } - // This throws EBADF with pidfd - if errno = linux.waitid(.PID, linux.Id(process.pid), &info, {.WEXITED, .WNOHANG, .WNOWAIT}, nil); errno != .NONE { + if errno = linux.waitid(.PIDFD, linux.Id(process.handle), &info, {.WEXITED, .WNOHANG, .WNOWAIT}, nil); errno != .NONE { return process_state, _get_platform_error(errno) } @@ -636,12 +734,34 @@ _timed_wait_on_handle :: proc(process: Process, timeout: time.Duration) -> (proc break } - new_tick := time.tick_now() - timeout -= time.tick_diff(start_tick, new_tick) - start_tick = new_tick + timeout -= time.tick_since(start_tick) + start_tick = time.tick_now() } - return _reap_terminated(process) + // _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) { @@ -669,6 +789,7 @@ _timed_wait_on_pid :: proc(process: Process, timeout: time.Duration) -> (process for errno != .NONE || info.code == 0 || info.pid != linux.Pid(process.pid) { if timeout <= 0 { _process_state_update_times(&process_state) + err = .Timeout return } @@ -681,11 +802,11 @@ _timed_wait_on_pid :: proc(process: Process, timeout: time.Duration) -> (process #partial switch errno { case .EAGAIN: // timeout _process_state_update_times(&process_state) + err = .Timeout return case .EINTR: - new_tick := time.tick_now() - timeout -= time.tick_diff(start_tick, new_tick) - start_tick = new_tick + timeout -= time.tick_since(start_tick) + start_tick = time.tick_now() case .EINVAL: return process_state, _get_platform_error(errno) } @@ -722,7 +843,7 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (Process_Stat } if errno == .EAGAIN || (errno == .NONE && info.signo != .SIGCHLD) { _process_state_update_times(&process_state) - return process_state, nil + return process_state, .Timeout } if errno != .NONE { return process_state, _get_platform_error(errno) @@ -733,10 +854,10 @@ _process_wait :: proc(process: Process, timeout: time.Duration) -> (Process_Stat @(private="package") _process_close :: proc(process: Process) -> Error { - pidfd := linux.Fd(process.handle) - if pidfd < 0 { + if process.handle == 0 || process.handle == PIDFD_UNASSIGNED { return nil } + pidfd := linux.Fd(process.handle) return _get_platform_error(linux.close(pidfd)) } From 07a9c69714fa8700ae888e9a52988afc760ba9ff Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 16 Aug 2024 01:48:27 -0400 Subject: [PATCH 23/23] update core:filepath's clean, join and split_list to return optional Allocator_Errors --- core/os/os2/process_linux.odin | 16 ++++----- core/path/filepath/path.odin | 55 +++++++++++++++++-------------- core/path/filepath/path_unix.odin | 6 ++-- core/strings/strings.odin | 5 ++- core/sys/windows/kernel32.odin | 0 5 files changed, 45 insertions(+), 37 deletions(-) mode change 100755 => 100644 core/sys/windows/kernel32.odin diff --git a/core/os/os2/process_linux.odin b/core/os/os2/process_linux.odin index 1ca6da9f3..8c1effddb 100644 --- a/core/os/os2/process_linux.odin +++ b/core/os/os2/process_linux.odin @@ -66,9 +66,9 @@ _process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) } defer linux.close(dir_fd) - dynamic_list := make([dynamic]int, temp_allocator()) + dynamic_list := make([dynamic]int, temp_allocator()) or_return - buf := make([dynamic]u8, 128, 128, temp_allocator()) + buf := make([dynamic]u8, 128, 128, temp_allocator()) or_return loop: for { buflen: int buflen, errno = linux.getdents(dir_fd, buf[:]) @@ -204,7 +204,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator info.executable_path = strings.clone(cmdline[:terminator], allocator) or_return info.fields += {.Executable_Path} } else if cwd_err == nil { - info.executable_path = filepath.join({ cwd, cmdline[:terminator] }, allocator) + info.executable_path = filepath.join({ cwd, cmdline[:terminator] }, allocator) or_return info.fields += {.Executable_Path} } else { break cmdline_if @@ -414,9 +414,9 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { executable_name := desc.command[0] if strings.index_byte(executable_name, '/') == -1 { path_env := get_env("PATH", temp_allocator()) - path_dirs := filepath.split_list(path_env, temp_allocator()) + path_dirs := filepath.split_list(path_env, temp_allocator()) or_return - exe_builder := strings.builder_make(temp_allocator()) + exe_builder := strings.builder_make(temp_allocator()) or_return found: bool for dir in path_dirs { @@ -467,7 +467,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { // 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()) + cargs := make([]cstring, len(desc.command) + 1, temp_allocator()) or_return for command, i in desc.command { cargs[i] = temp_cstring(command) or_return } @@ -478,7 +478,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { // take this process's current environment env = raw_data(export_cstring_environment(temp_allocator())) } else { - cenv := make([]cstring, len(desc.env) + 1, temp_allocator()) + cenv := make([]cstring, len(desc.env) + 1, temp_allocator()) or_return for env, i in desc.env { cenv[i] = temp_cstring(env) or_return } @@ -609,7 +609,7 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) { _process_state_update_times :: proc(state: ^Process_State) -> (err: Error) { TEMP_ALLOCATOR_GUARD() - stat_path_buf: [32]u8 + 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)) diff --git a/core/path/filepath/path.odin b/core/path/filepath/path.odin index c3dfa2bb1..e23183b02 100644 --- a/core/path/filepath/path.odin +++ b/core/path/filepath/path.odin @@ -2,6 +2,7 @@ // To process paths such as URLs that depend on forward slashes regardless of the OS, use the path package package filepath +import "base:runtime" import "core:strings" SEPARATOR_CHARS :: `/\` @@ -244,7 +245,7 @@ long_ext :: proc(path: string) -> string { If the result of the path is an empty string, the returned path with be `"."`. */ -clean :: proc(path: string, allocator := context.allocator) -> string { +clean :: proc(path: string, allocator := context.allocator) -> (cleaned: string, err: runtime.Allocator_Error) #optional_allocator_error { context.allocator = allocator path := path @@ -256,9 +257,9 @@ clean :: proc(path: string, allocator := context.allocator) -> string { if vol_len > 1 && original_path[1] != ':' { s, ok := from_slash(original_path) if !ok { - s = strings.clone(s) + s = strings.clone(s) or_return } - return s + return s, nil } return strings.concatenate({original_path, "."}) } @@ -275,7 +276,7 @@ clean :: proc(path: string, allocator := context.allocator) -> string { r, dot_dot := 0, 0 if rooted { - lazy_buffer_append(out, SEPARATOR) + lazy_buffer_append(out, SEPARATOR) or_return r, dot_dot = 1, 1 } @@ -295,33 +296,35 @@ clean :: proc(path: string, allocator := context.allocator) -> string { } case !rooted: if out.w > 0 { - lazy_buffer_append(out, SEPARATOR) + lazy_buffer_append(out, SEPARATOR) or_return } - lazy_buffer_append(out, '.') - lazy_buffer_append(out, '.') + lazy_buffer_append(out, '.') or_return + lazy_buffer_append(out, '.') or_return dot_dot = out.w } case: if rooted && out.w != 1 || !rooted && out.w != 0 { - lazy_buffer_append(out, SEPARATOR) + lazy_buffer_append(out, SEPARATOR) or_return } for ; r < n && !is_separator(path[r]); r += 1 { - lazy_buffer_append(out, path[r]) + lazy_buffer_append(out, path[r]) or_return } } } if out.w == 0 { - lazy_buffer_append(out, '.') + lazy_buffer_append(out, '.') or_return } - s := lazy_buffer_string(out) - cleaned, new_allocation := from_slash(s) + s := lazy_buffer_string(out) or_return + + new_allocation: bool + cleaned, new_allocation = from_slash(s) if new_allocation { delete(s) } - return cleaned + return } // Returns the result of replacing each forward slash `/` character in the path with the separate OS specific character. @@ -453,9 +456,9 @@ dir :: proc(path: string, allocator := context.allocator) -> string { // An empty string returns nil. A non-empty string with no separators returns a 1-element array. // Any empty components will be included, e.g. `a::b` will return a 3-element array, as will `::`. // Separators within pairs of double-quotes will be ignored and stripped, e.g. `"a:b"c:d` will return []{`a:bc`, `d`}. -split_list :: proc(path: string, allocator := context.allocator) -> []string { +split_list :: proc(path: string, allocator := context.allocator) -> (list: []string, err: runtime.Allocator_Error) #optional_allocator_error { if path == "" { - return nil + return nil, nil } start: int @@ -475,7 +478,7 @@ split_list :: proc(path: string, allocator := context.allocator) -> []string { } start, quote = 0, false - list := make([]string, count + 1, allocator) + list = make([]string, count + 1, allocator) or_return index := 0 for i := 0; i < len(path); i += 1 { c := path[i] @@ -494,12 +497,12 @@ split_list :: proc(path: string, allocator := context.allocator) -> []string { for s0, i in list { s, new := strings.replace_all(s0, `"`, ``, allocator) if !new { - s = strings.clone(s, allocator) + s = strings.clone(s, allocator) or_return } list[i] = s } - return list + return list, nil } @@ -526,33 +529,35 @@ lazy_buffer_index :: proc(lb: ^Lazy_Buffer, i: int) -> byte { return lb.s[i] } @(private) -lazy_buffer_append :: proc(lb: ^Lazy_Buffer, c: byte) { +lazy_buffer_append :: proc(lb: ^Lazy_Buffer, c: byte) -> (err: runtime.Allocator_Error) { if lb.b == nil { if lb.w < len(lb.s) && lb.s[lb.w] == c { lb.w += 1 return } - lb.b = make([]byte, len(lb.s)) + lb.b = make([]byte, len(lb.s)) or_return copy(lb.b, lb.s[:lb.w]) } lb.b[lb.w] = c lb.w += 1 + return } @(private) -lazy_buffer_string :: proc(lb: ^Lazy_Buffer) -> string { +lazy_buffer_string :: proc(lb: ^Lazy_Buffer) -> (s: string, err: runtime.Allocator_Error) { if lb.b == nil { return strings.clone(lb.vol_and_path[:lb.vol_len+lb.w]) } x := lb.vol_and_path[:lb.vol_len] y := string(lb.b[:lb.w]) - z := make([]byte, len(x)+len(y)) + z := make([]byte, len(x)+len(y)) or_return copy(z, x) copy(z[len(x):], y) - return string(z) + return string(z), nil } @(private) -lazy_buffer_destroy :: proc(lb: ^Lazy_Buffer) { - delete(lb.b) +lazy_buffer_destroy :: proc(lb: ^Lazy_Buffer) -> runtime.Allocator_Error { + err := delete(lb.b) lb^ = {} + return err } diff --git a/core/path/filepath/path_unix.odin b/core/path/filepath/path_unix.odin index b44a6a344..be2437203 100644 --- a/core/path/filepath/path_unix.odin +++ b/core/path/filepath/path_unix.odin @@ -39,15 +39,15 @@ abs :: proc(path: string, allocator := context.allocator) -> (string, bool) { return path_str, true } -join :: proc(elems: []string, allocator := context.allocator) -> string { +join :: proc(elems: []string, allocator := context.allocator) -> (joined: string, err: runtime.Allocator_Error) #optional_allocator_error { for e, i in elems { if e != "" { runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(ignore = context.temp_allocator == allocator) - p := strings.join(elems[i:], SEPARATOR_STRING, context.temp_allocator) + p := strings.join(elems[i:], SEPARATOR_STRING, context.temp_allocator) or_return return clean(p, allocator) } } - return "" + return "", nil } @(private) diff --git a/core/strings/strings.odin b/core/strings/strings.odin index e9b50bab0..2c3738c12 100644 --- a/core/strings/strings.odin +++ b/core/strings/strings.odin @@ -2071,7 +2071,10 @@ replace :: proc(s, old, new: string, n: int, allocator := context.allocator, loc } - t := make([]byte, len(s) + byte_count*(len(new) - len(old)), allocator, loc) + t, err := make([]byte, len(s) + byte_count*(len(new) - len(old)), allocator, loc) + if err != nil { + return + } was_allocation = true w := 0 diff --git a/core/sys/windows/kernel32.odin b/core/sys/windows/kernel32.odin old mode 100755 new mode 100644