From 63d94301fc5614334ee93a088abf2cc20a31268a Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sun, 14 Jul 2024 14:56:00 +1100 Subject: [PATCH] [os2/process]: Implement process creation procedures --- core/os/os2/process.odin | 165 +++++++++++++++++++++++---- core/os/os2/process_windows.odin | 186 ++++++++++++++++++++++++++++++- 2 files changed, 329 insertions(+), 22 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 894aa1e40..ecf9354b8 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -5,6 +5,12 @@ import "core:time" import "base:runtime" import "core:strings" +/* + In procedures that explicitly state this as one of the allowed values, + specifies an infinite timeout. +*/ +TIMEOUT_INFINITE :: time.MIN_DURATION // Note(flysand): Any negative duration will be treated as infinity + /* Arguments to the current process. @@ -212,6 +218,145 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) { delete(pi.cwd, allocator) } +/* + Represents a process handle. + + When a process dies, the OS is free to re-use the pid of that process. The + `Process` struct represents a handle to the process that will refer to a + specific process, even after it has died. + + **Note(linux)**: The `handle` will be referring to pidfd. +*/ +Process :: struct { + pid: int, + handle: uintptr, +} + +Process_Open_Flags :: bit_set[Process_Open_Flag] +Process_Open_Flag :: enum { + // Request for reading from the virtual memory of another process. + Mem_Read, + // Request for writing to the virtual memory of another process. + Mem_Write, +} + +/* + Open a process handle using it's pid. + + This procedure obtains a process handle of a process specified by `pid`. + This procedure can be subject to race conditions. See the description of + `Process`. + + Use `process_close()` function to close the process handle. +*/ +process_open :: proc(pid: int, flags := Process_Open_Flags {}) -> (Process, Error) { + return _process_open(pid, flags) +} + +/* + The description of how a process should be created. +*/ +Process_Desc :: struct { + // OS-specific attributes. + sys_attr: _Sys_Process_Attributes, + // The working directory of the process. + dir: string, + // The command to run. Each element of the slice is a separate argument to + // the process. The first element of the slice would be the executable. + command: []string, + // A slice of strings, each having the format `KEY=VALUE` representing the + // full environment that the child process will receive. + // In case this slice is `nil`, the current process' environment is used. + env: []string, + // The `stderr` handle to give to the child process. It can be either a file + // or a writeable end of a pipe. Passing `nil` will shut down the process' + // stderr output. + stderr: ^File, + // The `stdout` handle to give to the child process. It can be either a file + // or a writeabe end of a pipe. Passing a `nil` will shut down the process' + // stdout output. + stdout: ^File, + // The `stdin` handle to give to the child process. It can either be a file + // or a readable end of a pipe. Passing a `nil` will shut down the process' + // input. + stdin: ^File, +} + +/* + Create a new process and obtain its handle. + + This procedure creates a new process, with a given command and environment + strings as parameters. Use `environ()` to inherit the environment of the + current process. + + The `desc` parameter specifies the description of how the process should + be created. It contains information such as the command line, the + environment of the process, the starting directory and many other options. + Most of the fields in the struct can be set to `nil` or an empty value. + + Use `process_close` to close the handle to the process. Note, that this + is not the same as terminating the process. One can terminate the process + and not close the handle, in which case the handle would be leaked. In case + the function returns an error, an invalid handle is returned. + + This procedure is not thread-safe. It may alter the inheritance properties + of file handles. +*/ +process_start :: proc(desc := Process_Desc {}) -> (Process, Error) { + return _process_start(desc) +} + +/* + The state of the process after it has finished execution. +*/ +Process_State :: struct { + // The ID of the process. + pid: int, + // Specifies whether the process has terminated or is still running. + exited: bool, + // The exit code of the process, if it has exited. + // Will also store the number of the exception or signal that has crashed the + // process. + exit_code: int, + // Specifies whether the termination of the process was successfull or not, + // i.e. whether it has crashed or not. + // **Note(windows)**: On windows `true` is always returned, as there is no + // reliable way to obtain information about whether the process has crashed. + success: bool, + // The time the process has spend executing in kernel time. + system_time: time.Duration, + // The time the process has spend executing in userspace. + user_time: time.Duration, +} + +/* + Wait for a process event. + + This procedure blocks the execution until the process has exited or the + timeout (if specified) has reached zero. If the timeout is `TIMEOUT_INFINITE`, + no timeout restriction is imposed and the procedure can block indefinately. + + If the timeout has expired, the `General_Error.Timeout` is returned as + the error. + + If an error is returned for any other reason, other than timeout, the + process state is considered undetermined. +*/ +process_wait :: proc(process: Process, timeout := TIMEOUT_INFINITE) -> (Process_State, Error) { + return _process_wait(process, timeout) +} + +/* + Close the handle to a process. + + This procedure closes the handle associated with a process. It **does not** + terminate a process, in case it was running. In case a termination is + desired, kill the process first, then close the handle. +*/ +process_close :: proc(process: Process) -> (Error) { + return _process_close(process) +} + // Process_Attributes :: struct { // dir: string, // env: []string, @@ -225,27 +370,13 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) { // None, // } -// Process_State :: struct { -// pid: int, -// exit_code: int, -// exited: bool, -// success: bool, -// system_time: time.Duration, -// user_time: time.Duration, -// sys: rawptr, -// } + // Signal :: #type proc() // Kill: Signal = nil // Interrupt: Signal = nil - -// find_process :: proc(pid: int) -> (^Process, Process_Error) { -// return nil, .None -// } - - // process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) { // return nil, .None // } @@ -262,10 +393,6 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) { // return .None // } -// process_wait :: proc(p: ^Process) -> (Process_State, Process_Error) { -// return {}, .None -// } - diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 647860dc2..6e461bade 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -3,6 +3,8 @@ package os2 import "core:sys/windows" import "core:strings" +import "core:time" + import "base:runtime" _Process_Handle :: windows.HANDLE @@ -213,7 +215,7 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti info.command_line = cmdline } if .Command_Args in selection { - args, args_err := _parse_argv(raw_data(cmdline_w), allocator) + args, args_err := _parse_command_line(raw_data(cmdline_w), allocator) if args_err != nil { err = args_err return @@ -323,7 +325,7 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime info.command_line = command_line } if .Command_Args in selection { - args, args_err := _parse_argv(command_line_w, allocator) + args, args_err := _parse_command_line(command_line_w, allocator) if args_err != nil { err = args_err return @@ -356,6 +358,121 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime return } +_process_open :: proc(pid: int, flags: Process_Open_Flags) -> (Process, Error) { + dwDesiredAccess := windows.PROCESS_QUERY_LIMITED_INFORMATION | windows.SYNCHRONIZE + if .Mem_Read in flags { + dwDesiredAccess |= windows.PROCESS_VM_READ + } + if .Mem_Write in flags { + dwDesiredAccess |= windows.PROCESS_VM_WRITE + } + handle := windows.OpenProcess( + dwDesiredAccess, + false, + u32(pid), + ) + if handle == windows.INVALID_HANDLE_VALUE { + return {}, _get_platform_error() + } + return Process { + pid = pid, + handle = cast(uintptr) handle, + }, nil +} + +_Sys_Process_Attributes :: struct {} + +_process_start :: proc(desc: Process_Desc) -> (Process, Error) { + TEMP_ALLOCATOR_GUARD() + command_line := _build_command_line(desc.command, temp_allocator()) + command_line_w := windows.utf8_to_wstring(command_line, temp_allocator()) + environment := desc.env + if desc.env == nil { + environment = environ(temp_allocator()) + } + environment_block := _build_environment_block(environment, temp_allocator()) + environment_block_w := windows.utf8_to_utf16(environment_block, temp_allocator()) + stderr_handle := windows.GetStdHandle(windows.STD_ERROR_HANDLE) + stdout_handle := windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) + stdin_handle := windows.GetStdHandle(windows.STD_INPUT_HANDLE) + if desc.stdout != nil { + stdout_handle = windows.HANDLE(desc.stdout.impl.fd) + } + if desc.stderr != nil { + stderr_handle = windows.HANDLE(desc.stderr.impl.fd) + } + process_info: windows.PROCESS_INFORMATION = --- + process_ok := windows.CreateProcessW( + nil, + command_line_w, + nil, + nil, + true, + windows.CREATE_UNICODE_ENVIRONMENT|windows.NORMAL_PRIORITY_CLASS, + raw_data(environment_block_w), + nil, + &windows.STARTUPINFOW { + cb = size_of(windows.STARTUPINFOW), + hStdError = stderr_handle, + hStdOutput = stdout_handle, + hStdInput = stdin_handle, + dwFlags = windows.STARTF_USESTDHANDLES, + }, + &process_info, + ) + if !process_ok { + return {}, _get_platform_error() + } + return Process { + pid = cast(int) process_info.dwProcessId, + handle = cast(uintptr) process_info.hProcess, + }, nil +} + +_process_wait :: proc(process: Process, timeout: time.Duration) -> (Process_State, Error) { + handle := windows.HANDLE(process.handle) + timeout_ms := u32(timeout / time.Millisecond) if timeout > 0 else windows.INFINITE + wait_result := windows.WaitForSingleObject(handle, timeout_ms) + switch wait_result { + case windows.WAIT_OBJECT_0: + exit_code: u32 = --- + if !windows.GetExitCodeProcess(handle, &exit_code) { + return {}, _get_platform_error() + } + time_created: windows.FILETIME = --- + time_exited: windows.FILETIME = --- + time_kernel: windows.FILETIME = --- + time_user: windows.FILETIME = --- + if !windows.GetProcessTimes(handle, &time_created, &time_exited, &time_kernel, &time_user) { + return {}, _get_platform_error() + } + return Process_State { + exit_code = cast(int) exit_code, + exited = true, + pid = process.pid, + success = true, + system_time = _filetime_to_duration(time_kernel), + user_time = _filetime_to_duration(time_user), + }, nil + case windows.WAIT_TIMEOUT: + return {}, General_Error.Timeout + case: + return {}, _get_platform_error() + } +} + +_process_close :: proc(process: Process) -> (Error) { + if !windows.CloseHandle(cast(windows.HANDLE) process.handle) { + return _get_platform_error() + } + return nil +} + +@(private) +_filetime_to_duration :: proc(filetime: windows.FILETIME) -> time.Duration { + ticks := u64(filetime.dwHighDateTime)<<32 | u64(filetime.dwLowDateTime) + return time.Duration(ticks * 100) +} @(private) _get_process_user :: proc(process_handle: windows.HANDLE, allocator: runtime.Allocator) -> (full_username: string, err: Error) { @@ -406,7 +523,7 @@ _get_process_user :: proc(process_handle: windows.HANDLE, allocator: runtime.All } @(private) -_parse_argv :: proc(cmd_line_w: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) { +_parse_command_line :: proc(cmd_line_w: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) { argc: i32 = --- argv_w := windows.CommandLineToArgvW(cmd_line_w, &argc) if argv_w == nil { @@ -430,6 +547,43 @@ _parse_argv :: proc(cmd_line_w: [^]u16, allocator: runtime.Allocator) -> ([]stri return argv, nil } +@(private) +_build_command_line :: proc(command: []string, allocator: runtime.Allocator) -> string { + _write_byte_n_times :: #force_inline proc(builder: ^strings.Builder, b: byte, n: int) { + for _ in 0 ..< n { + strings.write_byte(builder, b) + } + } + builder := strings.builder_make(allocator) + for arg, i in command { + if i != 0 { + strings.write_byte(&builder, ' ') + } + j := 0 + strings.write_byte(&builder, '"') + for j < len(arg) { + backslashes := 0 + for j < len(arg) && arg[j] == '\\' { + backslashes += 1 + j += 1 + } + if j == len(arg) { + _write_byte_n_times(&builder, '\\', 2*backslashes) + break + } else if arg[j] == '"' { + _write_byte_n_times(&builder, '\\', 2*backslashes+1) + strings.write_byte(&builder, '"') + } else { + _write_byte_n_times(&builder, '\\', backslashes) + strings.write_byte(&builder, arg[j]) + } + j += 1 + } + strings.write_byte(&builder, '"') + } + return strings.to_string(builder) +} + @(private) _parse_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) { zt_count := 0 @@ -470,6 +624,32 @@ _parse_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) -> return envs, nil } +@(private) +_build_environment_block :: proc(environment: []string, allocator: runtime.Allocator) -> string { + builder := strings.builder_make(allocator) + #reverse for kv, cur_idx in environment { + eq_idx := strings.index_byte(kv, '=') + assert(eq_idx != -1, "Malformed environment string. Expected '=' to separate keys and values") + key := kv[:eq_idx] + already_handled := false + for old_kv in environment[cur_idx+1:] { + old_key := old_kv[:strings.index_byte(old_kv, '=')] + if key == old_key { + already_handled = true + break + } + } + if already_handled { + continue + } + strings.write_bytes(&builder, transmute([]byte) kv) + strings.write_byte(&builder, 0) + } + // Note(flysand): In addition to the NUL-terminator for each string, the + // environment block itself is NUL-terminated. + strings.write_byte(&builder, 0) + return strings.to_string(builder) +} @(private="file") PROCESSINFOCLASS :: enum i32 {