[os2/process]: Implement process creation procedures

This commit is contained in:
flysand7
2024-07-14 14:56:00 +11:00
parent e1eed7610c
commit 63d94301fc
2 changed files with 329 additions and 22 deletions
+146 -19
View File
@@ -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
// }
+183 -3
View File
@@ -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 {