Browse Source

Merge pull request #3971 from jasonKercher/os2-process-linux

os2 process linux implementation
gingerBill 1 year ago
parent
commit
31bb3dc4f0

+ 2 - 0
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"
 		}

+ 39 - 32
core/os/os2/file_linux.odin

@@ -19,33 +19,18 @@ File_Impl :: struct {
 }
 
 _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,
 	},
@@ -54,10 +39,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
@@ -72,7 +80,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}
@@ -217,12 +225,18 @@ _write_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (i64, Error) {
 }
 
 _file_size :: proc(f: ^File_Impl) -> (n: i64, err: Error) {
+	// 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 {
@@ -390,21 +404,15 @@ _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
 }
 
-/* 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
 	return _read_entire_pseudo_file_cstring(name_cstr, allocator)
 }
 
@@ -434,7 +442,6 @@ _read_entire_pseudo_file_cstring :: proc(name: cstring, allocator: runtime.Alloc
 	}
 
 	resize(&contents, i + n)
-
 	return contents[:], nil
 }
 

+ 0 - 2
core/os/os2/internal_util.odin

@@ -126,5 +126,3 @@ random_string :: proc(buf: []byte) -> string {
 	buf[i] = digits[u % b]
 	return string(buf[i:])
 }
-
-

+ 9 - 24
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
-
-			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 {
-					continue
-				}
+			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)
 
-				/* check for parent directory (..) */
-				#no_bounds_check if buf_len > 2 && d.d_name[0] == '.' && d.d_name[1] == '.' && d.d_name[2] == 0 {
+				/* check for current or parent directory (. or ..) */
+				if d_name_str == "." || d_name_str == ".." {
 					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 {

+ 2 - 2
core/os/os2/pipe_linux.odin

@@ -5,13 +5,13 @@ 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])) or_return
 	w = _new_file(uintptr(fds[1])) or_return
+
 	return
 }
-

+ 792 - 19
core/os/os2/process_linux.odin

@@ -1,68 +1,380 @@
+//+build linux
 //+private file
 package os2
 
 import "base:runtime"
+import "base:intrinsics"
+
 import "core:time"
+import "core:slice"
+import "core:strings"
+import "core:strconv"
 import "core:sys/linux"
+import "core:path/filepath"
+
+PIDFD_UNASSIGNED  :: ~uintptr(0)
 
 @(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) {
+	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
+	case .NONE:
+	case:
+		return {}, _get_platform_error(errno)
+	}
+	defer linux.close(dir_fd)
+
+	dynamic_list := make([dynamic]int, temp_allocator()) or_return
+
+	buf := make([dynamic]u8, 128, 128, temp_allocator()) or_return
+	loop: for {
+		buflen: int
+		buflen, errno = linux.getdents(dir_fd, buf[:])
+		#partial switch errno {
+		case .EINVAL:
+			resize(&buf, len(buf) * 2)
+			continue loop
+		case .NONE:
+			if buflen == 0 { break loop }
+		case:
+			return {}, _get_platform_error(errno)
+		}
+
+		offset: int
+		for d in linux.dirent_iterate_buf(buf[:buflen], &offset) {
+			d_name_str := linux.dirent_name(d)
+
+			if pid, ok := strconv.parse_int(d_name_str); ok {
+				append(&dynamic_list, pid)
+			}
+		}
+	}
+
+	list, err = slice.clone(dynamic_list[:], allocator)
 	return
 }
 
 @(private="package")
 _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
+	TEMP_ALLOCATOR_GUARD()
+
+	info.pid = pid
+
+	// Use this to make cstrings without copying.
+	path_backing: [48]u8
+	path_builder := strings.builder_from_bytes(path_backing[:])
+
+	strings.write_string(&path_builder, "/proc/")
+	strings.write_int(&path_builder, pid)
+	proc_fd, errno := linux.open(strings.to_cstring(&path_builder), _OPENDIR_FLAGS)
+	if errno != .NONE {
+		err = _get_platform_error(errno)
+		return
+	}
+	defer linux.close(proc_fd)
+
+	username_if: if .Username in selection {
+		s: linux.Stat
+		if errno = linux.fstat(proc_fd, &s); errno != .NONE {
+			err = _get_platform_error(errno)
+			break username_if
+		}
+
+		passwd_bytes: []u8
+		passwd_err: Error
+		passwd_bytes, passwd_err = _read_entire_pseudo_file_cstring("/etc/passwd", temp_allocator())
+		if passwd_err != nil {
+			err = passwd_err
+			break username_if
+		}
+
+		passwd := string(passwd_bytes)
+		for len(passwd) > 0 {
+			n := strings.index_byte(passwd, ':')
+			if n == -1 {
+				break
+			}
+			username := passwd[:n]
+			passwd = passwd[n+1:]
+
+			// skip password field
+			passwd = passwd[strings.index_byte(passwd, ':') + 1:]
+
+			n = strings.index_byte(passwd, ':')
+			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
+				info.fields += {.Username}
+				break
+			} else if !ok {
+				err = .Invalid_File
+				break username_if
+			}
+
+			eol := strings.index_byte(passwd, '\n')
+			if eol == -1 {
+				break
+			}
+			passwd = passwd[eol + 1:]
+		}
+	}
+
+	cmdline_if: if selection & {.Working_Dir, .Command_Line, .Command_Args, .Executable_Path} != {} {
+		strings.builder_reset(&path_builder)
+		strings.write_string(&path_builder, "/proc/")
+		strings.write_int(&path_builder, pid)
+		strings.write_string(&path_builder, "/cmdline")
+
+		cmdline_bytes, cmdline_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder), temp_allocator())
+		if cmdline_err != nil || len(cmdline_bytes) == 0 {
+			err = cmdline_err
+			break cmdline_if
+		}
+		cmdline := string(cmdline_bytes)
+
+		terminator := strings.index_byte(cmdline, 0)
+		assert(terminator > 0)
+
+		command_line_exec := cmdline[:terminator]
+
+		// Still need cwd if the execution on the command line is relative.
+		cwd: string
+		cwd_err: Error
+		if .Working_Dir in selection || (.Executable_Path in selection && command_line_exec[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(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 {
+				info.executable_path = filepath.join({ cwd, cmdline[:terminator] }, allocator) or_return
+				info.fields += {.Executable_Path}
+			} else {
+				break cmdline_if
+			}
+		}
+
+		if selection & {.Command_Line, .Command_Args} != {} {
+			// skip to first arg
+			//cmdline = cmdline[terminator + 1:]
+			command_line_builder: strings.Builder
+			command_args_list: [dynamic]string
+
+			if .Command_Line in selection {
+				command_line_builder = strings.builder_make(allocator) or_return
+				info.fields += {.Command_Line}
+			}
+
+			for i := 0; len(cmdline) > 0; i += 1 {
+				if terminator = strings.index_byte(cmdline, 0); terminator == -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
+					}
+				}
+
+				cmdline = cmdline[terminator + 1:]
+			}
+			info.command_line = strings.to_string(command_line_builder)
+			info.command_args = command_args_list[:]
+		}
+	}
+
+	stat_if: if selection & {.PPid, .Priority} != {} {
+		strings.builder_reset(&path_builder)
+		strings.write_string(&path_builder, "/proc/")
+		strings.write_int(&path_builder, pid)
+		strings.write_string(&path_builder, "/stat")
+
+		proc_stat_bytes, stat_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder), temp_allocator())
+		if stat_err != nil {
+			err = stat_err
+			break stat_if
+		}
+		if len(proc_stat_bytes) <= 0 {
+			break stat_if
+		}
+
+		// Skip to the first field after the executable name
+		stats: string
+		if start := strings.last_index_byte(string(proc_stat_bytes), ')'); start != -1 {
+			stats = string(proc_stat_bytes[start + 2:])
+		} else {
+			break stat_if
+		}
+
+		// NOTE: index 0 corresponds to field 3 (state) from `man 5 proc_pid_stat`
+		//       because we skipped passed the executable name above.
+		Fields :: enum {
+			State,
+			PPid,
+			PGrp,
+			Session,
+			Tty_Nr,
+			TpGid,
+			Flags,
+			MinFlt,
+			CMinFlt,
+			MajFlt,
+			CMajFlt,
+			UTime,
+			STime,
+			CUTime,
+			CSTime,
+			Priority,
+			Nice,
+			//... etc,
+		}
+		stat_fields := strings.split(stats, " ", temp_allocator()) or_return
+
+		if len(stat_fields) <= int(Fields.Nice) {
+			break stat_if
+		}
+
+		if .PPid in selection {
+			if ppid, ok := strconv.parse_int(stat_fields[Fields.PPid]); ok {
+				info.ppid = ppid
+				info.fields += {.PPid}
+			} else {
+				err = .Invalid_File
+				break stat_if
+			}
+		}
+
+		if .Priority in selection {
+			if nice, ok := strconv.parse_int(stat_fields[Fields.Nice]); ok {
+				info.priority = nice
+				info.fields += {.Priority}
+			} else {
+				err = .Invalid_File
+				break stat_if
+			}
+		}
+	}
+
+	if .Environment in selection {
+		strings.builder_reset(&path_builder)
+		strings.write_string(&path_builder, "/proc/")
+		strings.write_int(&path_builder, pid)
+		strings.write_string(&path_builder, "/environ")
+
+		if env_bytes, env_err := _read_entire_pseudo_file(strings.to_cstring(&path_builder), temp_allocator()); env_err == nil {
+			env := string(env_bytes)
+
+			env_list := make([dynamic]string, allocator) or_return
+			for len(env) > 0 {
+				terminator := strings.index_byte(env, 0)
+				if terminator == -1 || terminator == 0 {
+					break
+				}
+				e := strings.clone(env[:terminator], allocator) or_return
+				append(&env_list, e) or_return
+				env = env[terminator + 1:]
+			}
+			info.environment = env_list[:]
+			info.fields += {.Environment}
+		} else if err == nil {
+			err = env_err
+		}
+	}
+
 	return
 }
 
 @(private="package")
 _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
-	return
+	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
+
+	pidfd, errno := linux.pidfd_open(linux.Pid(pid), {})
+	if errno == .ENOSYS {
+		return process, .Unsupported
+	}
+	if errno != .NONE {
+		return process, _get_platform_error(errno)
+	}
+	process.handle = uintptr(pidfd)
 	return
 }
 
@@ -71,25 +383,486 @@ _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
+		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_Command
+	}
+
+	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)
+		}
+	}
+	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]
+	if strings.index_byte(executable_name, '/') == -1 {
+		path_env := get_env("PATH", temp_allocator())
+		path_dirs := filepath.split_list(path_env, temp_allocator()) or_return
+
+		exe_builder := strings.builder_make(temp_allocator()) or_return
+
+		found: bool
+		for dir in path_dirs {
+			strings.builder_reset(&exe_builder)
+			strings.write_string(&exe_builder, dir)
+			strings.write_byte(&exe_builder, '/')
+			strings.write_string(&exe_builder, executable_name)
+
+			exe_path := strings.to_cstring(&exe_builder)
+			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
+			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
+			}
+			if !has_executable_permissions(exe_fd) {
+				linux.close(exe_fd)
+				return process, .Permission_Denied
+			}
+		}
+	} else {
+		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
+		}
+	}
+
+	// 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.
+	cargs := make([]cstring, len(desc.command) + 1, temp_allocator()) 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.
+	env: [^]cstring
+	if desc.env == nil {
+		// take this process's current environment
+		env = raw_data(export_cstring_environment(temp_allocator()))
+	} else {
+		cenv := make([]cstring, len(desc.env) + 1, temp_allocator()) or_return
+		for env, i in desc.env {
+			cenv[i] = temp_cstring(env) or_return
+		}
+		env = &cenv[0]
+	}
+
+	child_pipe_fds: [2]linux.Fd
+	if errno = linux.pipe2(&child_pipe_fds, {.CLOEXEC}); errno != .NONE {
+		return process, _get_platform_error(errno)
+	}
+	defer linux.close(child_pipe_fds[WRITE])
+	defer linux.close(child_pipe_fds[READ])
+
+
+	// TODO: This is the traditional textbook implementation with fork.
+	//       A more efficient implementation with vfork:
+	//
+	//       1. retrieve signal handlers
+	//       2. block all signals
+	//       3. allocate some stack space
+	//       4. vfork (waits for child exit or execve); In child:
+	//           a. set child signal handlers
+	//           b. set up any necessary pipes
+	//           c. execve
+	//       5. restore signal handlers
+	//
+	pid: linux.Pid
+	if pid, errno = linux.fork(); errno != .NONE {
+		return process, _get_platform_error(errno)
+	}
+
+	STDIN  :: linux.Fd(0)
+	STDOUT :: linux.Fd(1)
+	STDERR :: linux.Fd(2)
+
+	READ :: 0
+	WRITE :: 1
+
+	if pid == 0 {
+		// in child process now
+		write_errno_to_parent_and_abort :: proc(parent_fd: linux.Fd, errno: linux.Errno) -> ! {
+			error_byte: [1]u8 = { u8(errno) }
+			linux.write(parent_fd, error_byte[:])
+			intrinsics.trap()
+		}
+
+		stdin_fd: linux.Fd
+		stdout_fd: linux.Fd
+		stderr_fd: linux.Fd
+
+		if desc.stdin != nil {
+			stdin_fd = linux.Fd(fd(desc.stdin))
+		} else {
+			stdin_fd, errno = linux.open("/dev/null", {})
+			if errno != .NONE {
+				write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+			}
+		}
+
+		write_devnull: linux.Fd = -1
+
+		if desc.stdout != nil {
+			stdout_fd = linux.Fd(fd(desc.stdout))
+		} else {
+			write_devnull, errno = linux.open("/dev/null", {.WRONLY})
+			if errno != .NONE {
+				write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+			}
+			stdout_fd = write_devnull
+		}
+
+		if desc.stderr != nil {
+			stderr_fd = linux.Fd(fd(desc.stderr))
+		} else {
+			if write_devnull == -1 {
+				write_devnull, errno = linux.open("/dev/null", {.WRONLY})
+				if errno != .NONE {
+					write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+				}
+			}
+			stderr_fd = write_devnull
+		}
+
+		if _, errno = linux.dup2(stdin_fd, STDIN); errno != .NONE {
+			write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+		}
+		if _, errno = linux.dup2(stdout_fd, STDOUT); errno != .NONE {
+			write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+		}
+		if _, errno = linux.dup2(stderr_fd, STDERR); errno != .NONE {
+			write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+		}
+
+		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 {
+			write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
+		}
+		unreachable()
+	}
+
+	process.pid = int(pid)
+
+	n: int
+	child_byte: [1]u8
+	errno = .EINTR
+	for errno == .EINTR {
+		n, errno = linux.read(child_pipe_fds[READ], child_byte[:])
+	}
+
+	// If the read failed, something weird happened. Do not return the read
+	// error so the user knows to wait on it.
+	if errno == .NONE {
+		child_errno := linux.Errno(child_byte[0])
+		if child_errno != .NONE {
+			// We can assume it trapped here.
+			_reap_terminated(process)
+			process.pid = 0
+			return process, _get_platform_error(child_errno)
+		}
+	}
+
+	process, _ = process_open(int(pid))
 	return
 }
 
-@(private="package")
-_process_wait :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) {
+_process_state_update_times :: proc(state: ^Process_State) -> (err: Error) {
+	TEMP_ALLOCATOR_GUARD()
+
+	stat_path_buf: [48]u8
+	path_builder := strings.builder_from_bytes(stat_path_buf[:])
+	strings.write_string(&path_builder, "/proc/")
+	strings.write_int(&path_builder, int(state.pid))
+	strings.write_string(&path_builder, "/stat")
+
+	stat_buf: []u8
+	stat_buf, err = _read_entire_pseudo_file(strings.to_cstring(&path_builder), temp_allocator())
+	if err != nil {
+		return
+	}
+
+	// ')' will be the end of the executable name (item 2)
+	idx := strings.last_index_byte(string(stat_buf), ')')
+	stats := string(stat_buf[idx + 2:])
+
+	// utime and stime are the 14 and 15th items, respectively, and we are
+	// currently on item 3. Skip 11 items here.
+	for _ in 0..<11 {
+		stats = stats[strings.index_byte(stats, ' ') + 1:]
+	}
+
+	idx = strings.index_byte(stats, ' ')
+	utime_str := stats[:idx]
+
+	stats = stats[idx + 1:]
+	stime_str := stats[:strings.index_byte(stats, ' ')]
+
+	utime, stime: int
+	ok: bool
+	if utime, ok = strconv.parse_int(utime_str, 10); !ok {
+		return .Invalid_File
+	}
+	if stime, ok = strconv.parse_int(stime_str, 10); !ok {
+		return .Invalid_File
+	}
+
+	// NOTE: Assuming HZ of 100, 1 jiffy == 10 ms
+	state.user_time = time.Duration(utime) * 10 * time.Millisecond
+	state.system_time = time.Duration(stime) * 10 * time.Millisecond
+
+	return
+}
+
+_reap_terminated :: proc(process: Process) -> (state: Process_State, err: Error) {
+	state.pid = process.pid
+	_process_state_update_times(&state)
+
+	info: linux.Sig_Info
+	errno := linux.Errno.EINTR
+	for errno == .EINTR {
+		errno = linux.waitid(.PID, linux.Id(process.pid), &info, {.WEXITED}, nil)
+	}
+	err = _get_platform_error(errno)
+
+	switch linux.Sig_Child_Code(info.code) {
+	case .NONE, .CONTINUED, .STOPPED:
+		unreachable()
+	case .EXITED:
+		state.exited = true
+		state.exit_code = int(info.status)
+		state.success = state.exit_code == 0
+	case .KILLED, .DUMPED, .TRAPPED:
+		state.exited = true
+		state.exit_code = int(info.status)
+		state.success = false
+	}
+	return
+}
+
+_timed_wait_on_handle :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) {
+	timeout := timeout
+
+	process_state.pid = process.pid
+	pidfd := linux.Fd(process.handle)
+	pollfd: [1]linux.Poll_Fd = {
+		{
+			fd = pidfd,
+			events = {.IN},
+		},
+	}
+
+	start_tick := time.tick_now()
+
+	mask: bit_set[0..<64; u64]
+	mask += { int(linux.Signal.SIGCHLD) - 1 }
+	sigchld_set := transmute(linux.Sig_Set)(mask)
+
+	info: linux.Sig_Info
+	for {
+		if timeout <= 0 {
+			_process_state_update_times(&process_state)
+			err = .Timeout
+			return
+		}
+
+		ts: linux.Time_Spec = {
+			time_sec  = uint(timeout / time.Second),
+			time_nsec = uint(timeout % time.Second),
+		}
+
+		n, errno := linux.ppoll(pollfd[:], &ts, &sigchld_set)
+		if errno != .NONE {
+			if errno == .EINTR {
+				timeout -= time.tick_since(start_tick)
+				start_tick = time.tick_now()
+				continue
+			}
+			return process_state, _get_platform_error(errno)
+		}
+
+		if n == 0 {  // timeout with no events
+			_process_state_update_times(&process_state)
+			err = .Timeout
+			return
+		}
+
+		if errno = linux.waitid(.PIDFD, linux.Id(process.handle), &info, {.WEXITED, .WNOHANG, .WNOWAIT}, nil); errno != .NONE {
+			return process_state, _get_platform_error(errno)
+		}
+
+		if info.signo == .SIGCHLD {
+			break
+		}
+
+		timeout -= time.tick_since(start_tick)
+		start_tick = time.tick_now()
+	}
+
+	// _reap_terminated for pidfd
+	{
+		_process_state_update_times(&process_state)
+
+		errno := linux.Errno.EINTR
+		for errno == .EINTR {
+			errno = linux.waitid(.PIDFD, linux.Id(process.handle), &info, {.WEXITED}, nil)
+		}
+		err = _get_platform_error(errno)
+
+		switch linux.Sig_Child_Code(info.code) {
+		case .NONE, .CONTINUED, .STOPPED:
+			unreachable()
+		case .EXITED:
+			process_state.exited = true
+			process_state.exit_code = int(info.status)
+			process_state.success = process_state.exit_code == 0
+		case .KILLED, .DUMPED, .TRAPPED:
+			process_state.exited = true
+			process_state.exit_code = int(info.status)
+			process_state.success = false
+		}
+	}
 	return
 }
 
+_timed_wait_on_pid :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) {
+	timeout := timeout
+	process_state.pid = process.pid
+
+	mask: bit_set[0..<64; u64]
+	mask += { int(linux.Signal.SIGCHLD) - 1 }
+	sigchld_set := transmute(linux.Sig_Set)(mask)
+
+	start_tick := time.tick_now()
+
+	org_sigset: linux.Sig_Set
+	errno := linux.rt_sigprocmask(.SIG_BLOCK, &sigchld_set, &org_sigset)
+	if errno != .NONE {
+		return process_state, _get_platform_error(errno)
+	}
+	defer linux.rt_sigprocmask(.SIG_SETMASK, &org_sigset, nil)
+
+	// In case there was a signal handler on SIGCHLD, avoid race
+	// condition by checking wait first.
+	info: linux.Sig_Info
+	errno = linux.waitid(.PID, linux.Id(process.pid), &info, {.WNOWAIT, .WEXITED, .WNOHANG}, nil)
+
+	for errno != .NONE || info.code == 0 || info.pid != linux.Pid(process.pid) {
+		if timeout <= 0 {
+			_process_state_update_times(&process_state)
+			err = .Timeout
+			return
+		}
+
+		ts: linux.Time_Spec = {
+			time_sec  = uint(timeout / time.Second),
+			time_nsec = uint(timeout % time.Second),
+		}
+
+		_, errno = linux.rt_sigtimedwait(&sigchld_set, &info, &ts)
+		#partial switch errno {
+		case .EAGAIN:   // timeout
+			_process_state_update_times(&process_state)
+			err = .Timeout
+			return
+		case .EINTR:
+			timeout -= time.tick_since(start_tick)
+			start_tick = time.tick_now()
+		case .EINVAL:
+			return process_state, _get_platform_error(errno)
+		}
+	}
+
+	return _reap_terminated(process)
+}
+
+@(private="package")
+_process_wait :: proc(process: Process, timeout: time.Duration) -> (Process_State, Error) {
+	if timeout > 0 {
+		if process.handle == PIDFD_UNASSIGNED {
+			return _timed_wait_on_pid(process, timeout)
+		} else {
+			return _timed_wait_on_handle(process, timeout)
+		}
+	}
+
+	process_state: Process_State = {
+		pid = process.pid,
+	}
+
+	errno: linux.Errno
+	options: linux.Wait_Options = {.WEXITED}
+	if timeout == 0 {
+		options += {.WNOHANG}
+	}
+
+	info: linux.Sig_Info
+
+	errno = .EINTR
+	for errno == .EINTR {
+		errno = linux.waitid(.PID, linux.Id(process.pid), &info, options + {.WNOWAIT}, nil)
+	}
+	if errno == .EAGAIN || (errno == .NONE && info.signo != .SIGCHLD) {
+		_process_state_update_times(&process_state)
+		return process_state, .Timeout
+	}
+	if errno != .NONE {
+		return process_state, _get_platform_error(errno)
+	}
+
+	return _reap_terminated(process)
+}
+
 @(private="package")
 _process_close :: proc(process: Process) -> Error {
-	return nil
+	if process.handle == 0 || process.handle == PIDFD_UNASSIGNED {
+		return nil
+	}
+	pidfd := linux.Fd(process.handle)
+	return _get_platform_error(linux.close(pidfd))
 }
 
 @(private="package")
 _process_kill :: proc(process: Process) -> Error {
-	return 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
-}

+ 30 - 25
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
 }

+ 3 - 3
core/path/filepath/path_unix.odin

@@ -38,15 +38,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)

+ 4 - 1
core/strings/strings.odin

@@ -2063,7 +2063,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

+ 14 - 0
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

+ 7 - 7
core/sys/linux/sys.odin

@@ -290,13 +290,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)
 	}
 }
 
@@ -2627,9 +2627,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)
 }
 
 /*
@@ -2927,9 +2927,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

+ 0 - 0
core/sys/windows/kernel32.odin