Browse Source

[os2/process]: Implement process creation procedures

flysand7 1 year ago
parent
commit
63d94301fc
2 changed files with 329 additions and 22 deletions
  1. 146 19
      core/os/os2/process.odin
  2. 183 3
      core/os/os2/process_windows.odin

+ 146 - 19
core/os/os2/process.odin

@@ -5,6 +5,12 @@ import "core:time"
 import "base:runtime"
 import "base:runtime"
 import "core:strings"
 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.
 	Arguments to the current process.
 
 
@@ -212,6 +218,145 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) {
 	delete(pi.cwd, 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 {
 // Process_Attributes :: struct {
 // 	dir: string,
 // 	dir: string,
 // 	env: []string,
 // 	env: []string,
@@ -225,27 +370,13 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) {
 // 	None,
 // 	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()
 // Signal :: #type proc()
 
 
 // Kill:      Signal = nil
 // Kill:      Signal = nil
 // Interrupt: 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) {
 // process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) {
 // 	return nil, .None
 // 	return nil, .None
 // }
 // }
@@ -262,10 +393,6 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) {
 // 	return .None
 // 	return .None
 // }
 // }
 
 
-// process_wait :: proc(p: ^Process) -> (Process_State, Process_Error) {
-// 	return {}, .None
-// }
-
 
 
 
 
 
 

+ 183 - 3
core/os/os2/process_windows.odin

@@ -3,6 +3,8 @@ package os2
 
 
 import "core:sys/windows"
 import "core:sys/windows"
 import "core:strings"
 import "core:strings"
+import "core:time"
+
 import "base:runtime"
 import "base:runtime"
 
 
 _Process_Handle :: windows.HANDLE
 _Process_Handle :: windows.HANDLE
@@ -213,7 +215,7 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti
 				info.command_line = cmdline
 				info.command_line = cmdline
 			}
 			}
 			if .Command_Args in selection {
 			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 {
 				if args_err != nil {
 					err = args_err
 					err = args_err
 					return
 					return
@@ -323,7 +325,7 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime
 			info.command_line = command_line
 			info.command_line = command_line
 		}
 		}
 		if .Command_Args in selection {
 		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 {
 			if args_err != nil {
 				err = args_err
 				err = args_err
 				return
 				return
@@ -356,6 +358,121 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime
 	return
 	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)
 @(private)
 _get_process_user :: proc(process_handle: windows.HANDLE, allocator: runtime.Allocator) -> (full_username: string, err: Error) {
 _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)
 @(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 = ---
 	argc: i32 = ---
 	argv_w := windows.CommandLineToArgvW(cmd_line_w, &argc)
 	argv_w := windows.CommandLineToArgvW(cmd_line_w, &argc)
 	if argv_w == nil {
 	if argv_w == nil {
@@ -430,6 +547,43 @@ _parse_argv :: proc(cmd_line_w: [^]u16, allocator: runtime.Allocator) -> ([]stri
 	return argv, nil
 	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)
 @(private)
 _parse_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) {
 _parse_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) {
 	zt_count := 0
 	zt_count := 0
@@ -470,6 +624,32 @@ _parse_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) ->
 	return envs, nil
 	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")
 @(private="file")
 PROCESSINFOCLASS :: enum i32 {
 PROCESSINFOCLASS :: enum i32 {