mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-05 15:18:49 +00:00
Merge pull request #3971 from jasonKercher/os2-process-linux
os2 process linux implementation
This commit is contained in:
@@ -23,6 +23,7 @@ General_Error :: enum u32 {
|
|||||||
Invalid_Dir,
|
Invalid_Dir,
|
||||||
Invalid_Path,
|
Invalid_Path,
|
||||||
Invalid_Callback,
|
Invalid_Callback,
|
||||||
|
Invalid_Command,
|
||||||
|
|
||||||
Pattern_Has_Separator,
|
Pattern_Has_Separator,
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ error_string :: proc(ferr: Error) -> string {
|
|||||||
case .Invalid_Dir: return "invalid directory"
|
case .Invalid_Dir: return "invalid directory"
|
||||||
case .Invalid_Path: return "invalid path"
|
case .Invalid_Path: return "invalid path"
|
||||||
case .Invalid_Callback: return "invalid callback"
|
case .Invalid_Callback: return "invalid callback"
|
||||||
|
case .Invalid_Command: return "invalid command"
|
||||||
case .Unsupported: return "unsupported"
|
case .Unsupported: return "unsupported"
|
||||||
case .Pattern_Has_Separator: return "pattern has separator"
|
case .Pattern_Has_Separator: return "pattern has separator"
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-32
@@ -19,33 +19,18 @@ File_Impl :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_stdin := File{
|
_stdin := File{
|
||||||
impl = &File_Impl{
|
|
||||||
name = "/proc/self/fd/0",
|
|
||||||
fd = 0,
|
|
||||||
allocator = file_allocator(),
|
|
||||||
},
|
|
||||||
stream = {
|
stream = {
|
||||||
procedure = _file_stream_proc,
|
procedure = _file_stream_proc,
|
||||||
},
|
},
|
||||||
fstat = _fstat,
|
fstat = _fstat,
|
||||||
}
|
}
|
||||||
_stdout := File{
|
_stdout := File{
|
||||||
impl = &File_Impl{
|
|
||||||
name = "/proc/self/fd/1",
|
|
||||||
fd = 1,
|
|
||||||
allocator = file_allocator(),
|
|
||||||
},
|
|
||||||
stream = {
|
stream = {
|
||||||
procedure = _file_stream_proc,
|
procedure = _file_stream_proc,
|
||||||
},
|
},
|
||||||
fstat = _fstat,
|
fstat = _fstat,
|
||||||
}
|
}
|
||||||
_stderr := File{
|
_stderr := File{
|
||||||
impl = &File_Impl{
|
|
||||||
name = "/proc/self/fd/2",
|
|
||||||
fd = 2,
|
|
||||||
allocator = file_allocator(),
|
|
||||||
},
|
|
||||||
stream = {
|
stream = {
|
||||||
procedure = _file_stream_proc,
|
procedure = _file_stream_proc,
|
||||||
},
|
},
|
||||||
@@ -54,10 +39,33 @@ _stderr := File{
|
|||||||
|
|
||||||
@init
|
@init
|
||||||
_standard_stream_init :: proc() {
|
_standard_stream_init :: proc() {
|
||||||
// cannot define these manually because cyclic reference
|
@static stdin_impl := File_Impl {
|
||||||
_stdin.stream.data = &_stdin
|
name = "/proc/self/fd/0",
|
||||||
_stdout.stream.data = &_stdout
|
fd = 0,
|
||||||
_stderr.stream.data = &_stderr
|
}
|
||||||
|
|
||||||
|
@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
|
stdin = &_stdin
|
||||||
stdout = &_stdout
|
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
|
// terminal would be incredibly rare. This has no effect on files while
|
||||||
// allowing us to open serial devices.
|
// allowing us to open serial devices.
|
||||||
sys_flags: linux.Open_Flags = {.NOCTTY, .CLOEXEC}
|
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_RDONLY:
|
||||||
case O_WRONLY: sys_flags += {.WRONLY}
|
case O_WRONLY: sys_flags += {.WRONLY}
|
||||||
case O_RDWR: sys_flags += {.RDWR}
|
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) {
|
_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 = ---
|
s: linux.Stat = ---
|
||||||
errno := linux.fstat(f.fd, &s)
|
errno := linux.fstat(f.fd, &s)
|
||||||
if errno != .NONE {
|
if errno != .NONE {
|
||||||
return -1, _get_platform_error(errno)
|
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 {
|
_sync :: proc(f: ^File) -> Error {
|
||||||
@@ -390,21 +404,15 @@ _fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error {
|
|||||||
_exists :: proc(name: string) -> bool {
|
_exists :: proc(name: string) -> bool {
|
||||||
TEMP_ALLOCATOR_GUARD()
|
TEMP_ALLOCATOR_GUARD()
|
||||||
name_cstr, _ := temp_cstring(name)
|
name_cstr, _ := temp_cstring(name)
|
||||||
res, errno := linux.access(name_cstr, linux.F_OK)
|
return linux.access(name_cstr, linux.F_OK) == .NONE
|
||||||
return !res && errno == .NONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Certain files in the Linux file system are not actual
|
/* For reading Linux system files that stat to size 0 */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
_read_entire_pseudo_file :: proc { _read_entire_pseudo_file_string, _read_entire_pseudo_file_cstring }
|
_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) {
|
_read_entire_pseudo_file_string :: proc(name: string, allocator: runtime.Allocator) -> (b: []u8, e: Error) {
|
||||||
name_cstr := clone_to_cstring(name, allocator) or_return
|
TEMP_ALLOCATOR_GUARD()
|
||||||
defer delete(name, allocator)
|
name_cstr := clone_to_cstring(name, temp_allocator()) or_return
|
||||||
return _read_entire_pseudo_file_cstring(name_cstr, allocator)
|
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)
|
resize(&contents, i + n)
|
||||||
|
|
||||||
return contents[:], nil
|
return contents[:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,5 +126,3 @@ random_string :: proc(buf: []byte) -> string {
|
|||||||
buf[i] = digits[u % b]
|
buf[i] = digits[u % b]
|
||||||
return string(buf[i:])
|
return string(buf[i:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//+private
|
//+private
|
||||||
package os2
|
package os2
|
||||||
|
|
||||||
|
import "core:strings"
|
||||||
import "core:strconv"
|
import "core:strconv"
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
import "core:sys/linux"
|
import "core:sys/linux"
|
||||||
@@ -75,14 +76,6 @@ _mkdir_all :: proc(path: string, perm: int) -> Error {
|
|||||||
return nil if has_created else .Exist
|
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 {
|
_remove_all :: proc(path: string) -> Error {
|
||||||
DT_DIR :: 4
|
DT_DIR :: 4
|
||||||
|
|
||||||
@@ -105,26 +98,18 @@ _remove_all :: proc(path: string) -> Error {
|
|||||||
return _get_platform_error(errno)
|
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) {
|
/* check for current or parent directory (. or ..) */
|
||||||
d = (^dirent64)(rawptr(&buf[i]))
|
if d_name_str == "." || d_name_str == ".." {
|
||||||
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
/* check for parent directory (..) */
|
#partial switch d.type {
|
||||||
#no_bounds_check if buf_len > 2 && d.d_name[0] == '.' && d.d_name[1] == '.' && d.d_name[2] == 0 {
|
case .DIR:
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch d.d_type {
|
|
||||||
case DT_DIR:
|
|
||||||
new_dfd: linux.Fd
|
new_dfd: linux.Fd
|
||||||
new_dfd, errno = linux.openat(dfd, d_name_cstr, _OPENDIR_FLAGS)
|
new_dfd, errno = linux.openat(dfd, d_name_cstr, _OPENDIR_FLAGS)
|
||||||
if errno != .NONE {
|
if errno != .NONE {
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import "core:sys/linux"
|
|||||||
|
|
||||||
_pipe :: proc() -> (r, w: ^File, err: Error) {
|
_pipe :: proc() -> (r, w: ^File, err: Error) {
|
||||||
fds: [2]linux.Fd
|
fds: [2]linux.Fd
|
||||||
errno := linux.pipe2(&fds, {})
|
errno := linux.pipe2(&fds, {.CLOEXEC})
|
||||||
if errno != .NONE {
|
if errno != .NONE {
|
||||||
return nil, nil,_get_platform_error(errno)
|
return nil, nil,_get_platform_error(errno)
|
||||||
}
|
}
|
||||||
|
|
||||||
r = _new_file(uintptr(fds[0])) or_return
|
r = _new_file(uintptr(fds[0])) or_return
|
||||||
w = _new_file(uintptr(fds[1])) or_return
|
w = _new_file(uintptr(fds[1])) or_return
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+792
-19
@@ -1,68 +1,380 @@
|
|||||||
|
//+build linux
|
||||||
//+private file
|
//+private file
|
||||||
package os2
|
package os2
|
||||||
|
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
|
import "base:intrinsics"
|
||||||
|
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
import "core:slice"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:strconv"
|
||||||
import "core:sys/linux"
|
import "core:sys/linux"
|
||||||
|
import "core:path/filepath"
|
||||||
|
|
||||||
|
PIDFD_UNASSIGNED :: ~uintptr(0)
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_exit :: proc "contextless" (code: int) -> ! {
|
_exit :: proc "contextless" (code: int) -> ! {
|
||||||
linux.exit(i32(code))
|
linux.exit_group(i32(code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_get_uid :: proc() -> int {
|
_get_uid :: proc() -> int {
|
||||||
return -1
|
return int(linux.getuid())
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_get_euid :: proc() -> int {
|
_get_euid :: proc() -> int {
|
||||||
return -1
|
return int(linux.geteuid())
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_get_gid :: proc() -> int {
|
_get_gid :: proc() -> int {
|
||||||
return -1
|
return int(linux.getgid())
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_get_egid :: proc() -> int {
|
_get_egid :: proc() -> int {
|
||||||
return -1
|
return int(linux.getegid())
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_get_pid :: proc() -> int {
|
_get_pid :: proc() -> int {
|
||||||
return -1
|
return int(linux.getpid())
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_get_ppid :: proc() -> int {
|
_get_ppid :: proc() -> int {
|
||||||
return -1
|
return int(linux.getppid())
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) {
|
_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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
|
_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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
|
_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")
|
@(private="package")
|
||||||
_current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
|
_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")
|
@(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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,25 +383,486 @@ _Sys_Process_Attributes :: struct {}
|
|||||||
|
|
||||||
@(private="package")
|
@(private="package")
|
||||||
_process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
|
_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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private="package")
|
_process_state_update_times :: proc(state: ^Process_State) -> (err: Error) {
|
||||||
_process_wait :: proc(process: Process, timeout: time.Duration) -> (process_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
|
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")
|
@(private="package")
|
||||||
_process_close :: proc(process: Process) -> Error {
|
_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")
|
@(private="package")
|
||||||
_process_kill :: proc(process: Process) -> Error {
|
_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
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// To process paths such as URLs that depend on forward slashes regardless of the OS, use the path package
|
// To process paths such as URLs that depend on forward slashes regardless of the OS, use the path package
|
||||||
package filepath
|
package filepath
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
SEPARATOR_CHARS :: `/\`
|
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 `"."`.
|
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
|
context.allocator = allocator
|
||||||
|
|
||||||
path := path
|
path := path
|
||||||
@@ -256,9 +257,9 @@ clean :: proc(path: string, allocator := context.allocator) -> string {
|
|||||||
if vol_len > 1 && original_path[1] != ':' {
|
if vol_len > 1 && original_path[1] != ':' {
|
||||||
s, ok := from_slash(original_path)
|
s, ok := from_slash(original_path)
|
||||||
if !ok {
|
if !ok {
|
||||||
s = strings.clone(s)
|
s = strings.clone(s) or_return
|
||||||
}
|
}
|
||||||
return s
|
return s, nil
|
||||||
}
|
}
|
||||||
return strings.concatenate({original_path, "."})
|
return strings.concatenate({original_path, "."})
|
||||||
}
|
}
|
||||||
@@ -275,7 +276,7 @@ clean :: proc(path: string, allocator := context.allocator) -> string {
|
|||||||
|
|
||||||
r, dot_dot := 0, 0
|
r, dot_dot := 0, 0
|
||||||
if rooted {
|
if rooted {
|
||||||
lazy_buffer_append(out, SEPARATOR)
|
lazy_buffer_append(out, SEPARATOR) or_return
|
||||||
r, dot_dot = 1, 1
|
r, dot_dot = 1, 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,33 +296,35 @@ clean :: proc(path: string, allocator := context.allocator) -> string {
|
|||||||
}
|
}
|
||||||
case !rooted:
|
case !rooted:
|
||||||
if out.w > 0 {
|
if out.w > 0 {
|
||||||
lazy_buffer_append(out, SEPARATOR)
|
lazy_buffer_append(out, SEPARATOR) or_return
|
||||||
}
|
}
|
||||||
lazy_buffer_append(out, '.')
|
lazy_buffer_append(out, '.') or_return
|
||||||
lazy_buffer_append(out, '.')
|
lazy_buffer_append(out, '.') or_return
|
||||||
dot_dot = out.w
|
dot_dot = out.w
|
||||||
}
|
}
|
||||||
case:
|
case:
|
||||||
if rooted && out.w != 1 || !rooted && out.w != 0 {
|
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 {
|
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 {
|
if out.w == 0 {
|
||||||
lazy_buffer_append(out, '.')
|
lazy_buffer_append(out, '.') or_return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := lazy_buffer_string(out)
|
s := lazy_buffer_string(out) or_return
|
||||||
cleaned, new_allocation := from_slash(s)
|
|
||||||
|
new_allocation: bool
|
||||||
|
cleaned, new_allocation = from_slash(s)
|
||||||
if new_allocation {
|
if new_allocation {
|
||||||
delete(s)
|
delete(s)
|
||||||
}
|
}
|
||||||
return cleaned
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the result of replacing each forward slash `/` character in the path with the separate OS specific character.
|
// 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.
|
// 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 `::`.
|
// 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`}.
|
// 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 == "" {
|
if path == "" {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
start: int
|
start: int
|
||||||
@@ -475,7 +478,7 @@ split_list :: proc(path: string, allocator := context.allocator) -> []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start, quote = 0, false
|
start, quote = 0, false
|
||||||
list := make([]string, count + 1, allocator)
|
list = make([]string, count + 1, allocator) or_return
|
||||||
index := 0
|
index := 0
|
||||||
for i := 0; i < len(path); i += 1 {
|
for i := 0; i < len(path); i += 1 {
|
||||||
c := path[i]
|
c := path[i]
|
||||||
@@ -494,12 +497,12 @@ split_list :: proc(path: string, allocator := context.allocator) -> []string {
|
|||||||
for s0, i in list {
|
for s0, i in list {
|
||||||
s, new := strings.replace_all(s0, `"`, ``, allocator)
|
s, new := strings.replace_all(s0, `"`, ``, allocator)
|
||||||
if !new {
|
if !new {
|
||||||
s = strings.clone(s, allocator)
|
s = strings.clone(s, allocator) or_return
|
||||||
}
|
}
|
||||||
list[i] = s
|
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]
|
return lb.s[i]
|
||||||
}
|
}
|
||||||
@(private)
|
@(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.b == nil {
|
||||||
if lb.w < len(lb.s) && lb.s[lb.w] == c {
|
if lb.w < len(lb.s) && lb.s[lb.w] == c {
|
||||||
lb.w += 1
|
lb.w += 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lb.b = make([]byte, len(lb.s))
|
lb.b = make([]byte, len(lb.s)) or_return
|
||||||
copy(lb.b, lb.s[:lb.w])
|
copy(lb.b, lb.s[:lb.w])
|
||||||
}
|
}
|
||||||
lb.b[lb.w] = c
|
lb.b[lb.w] = c
|
||||||
lb.w += 1
|
lb.w += 1
|
||||||
|
return
|
||||||
}
|
}
|
||||||
@(private)
|
@(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 {
|
if lb.b == nil {
|
||||||
return strings.clone(lb.vol_and_path[:lb.vol_len+lb.w])
|
return strings.clone(lb.vol_and_path[:lb.vol_len+lb.w])
|
||||||
}
|
}
|
||||||
|
|
||||||
x := lb.vol_and_path[:lb.vol_len]
|
x := lb.vol_and_path[:lb.vol_len]
|
||||||
y := string(lb.b[:lb.w])
|
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, x)
|
||||||
copy(z[len(x):], y)
|
copy(z[len(x):], y)
|
||||||
return string(z)
|
return string(z), nil
|
||||||
}
|
}
|
||||||
@(private)
|
@(private)
|
||||||
lazy_buffer_destroy :: proc(lb: ^Lazy_Buffer) {
|
lazy_buffer_destroy :: proc(lb: ^Lazy_Buffer) -> runtime.Allocator_Error {
|
||||||
delete(lb.b)
|
err := delete(lb.b)
|
||||||
lb^ = {}
|
lb^ = {}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,15 +38,15 @@ abs :: proc(path: string, allocator := context.allocator) -> (string, bool) {
|
|||||||
return path_str, true
|
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 {
|
for e, i in elems {
|
||||||
if e != "" {
|
if e != "" {
|
||||||
runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(ignore = context.temp_allocator == allocator)
|
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 clean(p, allocator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
|
|||||||
@@ -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
|
was_allocation = true
|
||||||
|
|
||||||
w := 0
|
w := 0
|
||||||
|
|||||||
@@ -983,6 +983,20 @@ Sig_Action_Flag :: enum u32 {
|
|||||||
RESETHAND = 31,
|
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
|
Type of socket to create
|
||||||
- For TCP you want to use SOCK_STREAM
|
- For TCP you want to use SOCK_STREAM
|
||||||
|
|||||||
@@ -290,13 +290,13 @@ writev :: proc "contextless" (fd: Fd, iov: []IO_Vec) -> (int, Errno) {
|
|||||||
Available since Linux 1.0.
|
Available since Linux 1.0.
|
||||||
For ARM64 available since Linux 2.6.16.
|
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 {
|
when ODIN_ARCH == .arm64 {
|
||||||
ret := syscall(SYS_faccessat, AT_FDCWD, cast(rawptr) name, transmute(u32) mode)
|
ret := syscall(SYS_faccessat, AT_FDCWD, cast(rawptr) name, transmute(u32) mode)
|
||||||
return errno_unwrap(ret, bool)
|
return Errno(-ret)
|
||||||
} else {
|
} else {
|
||||||
ret := syscall(SYS_access, cast(rawptr) name, transmute(u32) mode)
|
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.
|
Checks the user permissions for a file at specified dirfd.
|
||||||
Available since Linux 2.6.16.
|
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)
|
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).
|
Checks the user permissions for a file at specified dirfd (with flags).
|
||||||
Available since Linux 5.8.
|
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)
|
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
|
// TODO(flysand): process_madvise
|
||||||
|
|||||||
Executable → Regular
Reference in New Issue
Block a user