diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 862434b7b..d407ffb18 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -3,100 +3,391 @@ package os2 import "core:sync" import "core:time" import "base:runtime" +import "core:strings" -args: []string +/* + 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. +*/ +args := get_args() + +@(private="file") +get_args :: proc() -> []string { + result := make([]string, len(runtime.args__), heap_allocator()) + for rt_arg, i in runtime.args__ { + result[i] = cast(string) rt_arg + } + return result[:] +} + +/* + Exit the current process. +*/ exit :: proc "contextless" (code: int) -> ! { - runtime.trap() + _exit(code) } +/* + Obtain the UID of the current process. + + **Note(windows)**: Windows doesn't follow the posix permissions model, so + the function simply returns -1. +*/ get_uid :: proc() -> int { - return -1 + return _get_uid() } +/* + Obtain the effective UID of the current process. + + The effective UID is typically the same as the UID of the process. In case + the process was run by a user with elevated permissions, the process may + lower the privilege to perform some tasks without privilege. In these cases + the real UID of the process and the effective UID are different. + + **Note(windows)**: Windows doesn't follow the posix permissions model, so + the function simply returns -1. +*/ get_euid :: proc() -> int { - return -1 + return _get_euid() } +/* + Obtain the GID of the current process. + + **Note(windows)**: Windows doesn't follow the posix permissions model, so + the function simply returns -1. +*/ get_gid :: proc() -> int { - return -1 + return _get_gid() } +/* + Obtain the effective GID of the current process. + + The effective GID is typically the same as the GID of the process. In case + the process was run by a user with elevated permissions, the process may + lower the privilege to perform some tasks without privilege. In these cases + the real GID of the process and the effective GID are different. + + **Note(windows)**: Windows doesn't follow the posix permissions model, so + the function simply returns -1. +*/ get_egid :: proc() -> int { - return -1 + return _get_egid() } +/* + Obtain the ID of the current process. +*/ get_pid :: proc() -> int { - return -1 + return _get_pid() } +/* + Obtain the ID of the parent process. + + **Note(windows)**: Windows does not mantain strong relationships between + parent and child processes. This function returns the ID of the process + that has created the current process. In case the parent has died, the ID + returned by this function can identify a non-existent or a different + process. +*/ get_ppid :: proc() -> int { - return -1 + return _get_ppid() } +/* + Obtain ID's of all processes running in the system. +*/ +process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { + return _process_list(allocator) +} +/* + Bit set specifying which fields of the `Process_Info` struct need to be + obtained by the `process_info()` procedure. Each bit corresponds to a + field in the `Process_Info` struct. +*/ +Process_Info_Fields :: bit_set[Process_Info_Field] +Process_Info_Field :: enum { + Executable_Path, + PPid, + Priority, + Command_Line, + Command_Args, + Environment, + Username, + Working_Dir, +} + +/* + Contains information about the process as obtained by the `process_info()` + procedure. +*/ +Process_Info :: struct { + // The information about a process the struct contains. `pid` is always + // stored, no matter what. + fields: Process_Info_Fields, + // The ID of the process. + pid: int, + // The ID of the parent process. + ppid: int, + // The process priority. + priority: int, + // The path to the executable, which the process runs. + executable_path: string, + // The command line supplied to the process. + command_line: string, + // The arguments supplied to the process. + command_args: []string, + // The environment of the process. + environment: []string, + // The username of the user who started the process. + username: string, + // The current working directory of the process. + working_dir: string, +} + +/* + Obtain information about a process. + + This procedure obtains an information, specified by `selection` parameter of + a process given by `pid`. + + Use `free_process_info` to free the memory allocated by this procedure. In + case the function returns an error all temporary allocations would be freed + and as such, calling `free_process_info()` is not needed. + + **Note**: The resulting information may or may contain the fields specified + by the `selection` parameter. Always check whether the returned + `Process_Info` struct has the required fields before checking the error code + returned by this function. +*/ +process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { + return _process_info_by_pid(pid, selection, allocator) +} + +/* + Obtain information about a process. + + This procedure obtains information, specified by `selection` parameter + about a process that has been opened by the application, specified in + the `process` parameter. + + Use `free_process_info` to free the memory allocated by this procedure. In + case the function returns an error, all temporary allocations would be freed + and as such, calling `free_process_info` is not needed. + + **Note**: The resulting information may or may contain the fields specified + by the `selection` parameter. Always check whether the returned + `Process_Info` struct has the required fields before checking the error code + returned by this function. +*/ +process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { + return _process_info_by_handle(process, selection, allocator) +} + +/* + Obtain information about the current process. + + This procedure obtains the information, specified by `selection` parameter + about the currently running process. + + Use `free_process_info` to free the memory allocated by this function. In + case this function returns an error, all temporary allocations would be + freed and as such calling `free_process_info()` is not needed. + + **Note**: The resulting information may or may contain the fields specified + by the `selection` parameter. Always check whether the returned + `Process_Info` struct has the required fields before checking the error code + returned by this function. +*/ +current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { + return _current_process_info(selection, allocator) +} + +/* + Obtain information about the specified process. +*/ +process_info :: proc { + process_info_by_pid, + process_info_by_handle, + current_process_info, +} + +/* + Free the information about the process. + + This procedure frees the memory occupied by process info using the provided + allocator. The allocator needs to be the same allocator that was supplied + to the `process_info` function. +*/ +free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) { + delete(pi.executable_path, allocator) + delete(pi.command_line, allocator) + delete(pi.command_args, allocator) + for s in pi.environment { + delete(s, allocator) + } + delete(pi.environment, allocator) + delete(pi.working_dir, 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, - is_done: b32, - signal_mutex: sync.RW_Mutex, + 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, +} -Process_Attributes :: struct { - dir: string, +/* + 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. If the string has length 0, the + // working directory is assumed to be the current working directory of the + // current process. + working_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, - files: []^File, - sys: ^Process_Attributes_OS_Specific, + // 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, } -Process_Attributes_OS_Specific :: struct{} +/* + Create a new process and obtain its handle. -Process_Error :: enum { - None, + 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 in an unpredictable manner. In case multiple threads change + handle inheritance properties, make sure to serialize all those calls. +*/ +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 { - pid: int, - exit_code: int, - exited: bool, - success: bool, + // 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, - user_time: time.Duration, - sys: rawptr, + // The time the process has spend executing in userspace. + user_time: time.Duration, } -Signal :: #type proc() +/* + Wait for a process event. -Kill: Signal = nil -Interrupt: Signal = nil + 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. -find_process :: proc(pid: int) -> (^Process, Process_Error) { - return nil, .None + 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. -process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) { - return nil, .None + 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, wait for the process to finish, + then close the handle. +*/ +process_close :: proc(process: Process) -> (Error) { + return _process_close(process) } -process_release :: proc(p: ^Process) -> Process_Error { - return .None +/* + Terminate a process. + + This procedure terminates a process, specified by it's handle, `process`. + +*/ +process_kill :: proc(process: Process) -> (Error) { + return _process_kill(process) } - -process_kill :: proc(p: ^Process) -> Process_Error { - return .None -} - -process_signal :: proc(p: ^Process, sig: Signal) -> Process_Error { - 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 new file mode 100644 index 000000000..e16cedd9f --- /dev/null +++ b/core/os/os2/process_windows.odin @@ -0,0 +1,944 @@ +//+build windows +package os2 + +import "core:sys/windows" +import "core:strings" +import "core:time" + +import "base:runtime" + +_Process_Handle :: windows.HANDLE + +_exit :: proc "contextless" (code: int) -> ! { + windows.ExitProcess(u32(code)) +} + +_get_uid :: proc() -> int { + return -1 +} + +_get_euid :: proc() -> int { + return -1 +} + +_get_gid :: proc() -> int { + return -1 +} + +_get_egid :: proc() -> int { + return -1 +} + +_get_pid :: proc() -> int { + return cast(int) windows.GetCurrentProcessId() +} + +_get_ppid :: proc() -> int { + our_pid := windows.GetCurrentProcessId() + snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if snap == windows.INVALID_HANDLE_VALUE { + return -1 + } + defer windows.CloseHandle(snap) + entry := windows.PROCESSENTRY32W { dwSize = size_of(windows.PROCESSENTRY32W) } + status := windows.Process32FirstW(snap, &entry) + for status { + if entry.th32ProcessID == our_pid { + return cast(int) entry.th32ParentProcessID + } + status = windows.Process32NextW(snap, &entry) + } + return -1 +} + +_process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { + pid_list := make([dynamic]int, allocator) + snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if snap == windows.INVALID_HANDLE_VALUE { + return pid_list[:], _get_platform_error() + } + entry := windows.PROCESSENTRY32W { dwSize = size_of(windows.PROCESSENTRY32W) } + status := windows.Process32FirstW(snap, &entry) + for status { + append(&pid_list, cast(int) entry.th32ProcessID) + status = windows.Process32NextW(snap, &entry) + } + return pid_list[:], nil +} + +_process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { + info.pid = pid + defer if err != nil { + free_process_info(info, allocator) + } + need_snapprocess := \ + .PPid in selection || + .Priority in selection + need_snapmodule := \ + .Executable_Path in selection + need_peb := \ + .Command_Line in selection || + .Environment in selection || + .Working_Dir in selection + need_process_handle := need_peb || .Username in selection + // Data obtained from process snapshots + if need_snapprocess { + entry, entry_err := _process_entry_by_pid(info.pid) + if entry_err != nil { + err = General_Error.Not_Exist + return + } + if .PPid in selection { + info.fields |= {.PPid} + info.ppid = int(entry.th32ParentProcessID) + } + if .Priority in selection { + info.fields |= {.Priority} + info.priority = int(entry.pcPriClassBase) + } + } + if need_snapmodule { + exe_path, exe_path_err := _process_exe_by_pid(pid, allocator) + if exe_path_err != nil { + err = exe_path_err + return + } + info.fields |= {.Executable_Path} + info.executable_path = exe_path + } + ph := windows.INVALID_HANDLE_VALUE + if need_process_handle { + ph = windows.OpenProcess( + windows.PROCESS_QUERY_LIMITED_INFORMATION | windows.PROCESS_VM_READ, + false, + u32(pid), + ) + if ph == windows.INVALID_HANDLE_VALUE { + err = _get_platform_error() + return + } + } + defer if ph != windows.INVALID_HANDLE_VALUE { + windows.CloseHandle(ph) + } + if need_peb { + // TODO(flysand): This was not tested with WOW64 or 32-bit processes, + // might need to be revised later when issues occur. + ntdll_lib := windows.LoadLibraryW(windows.L("ntdll.dll")) + if ntdll_lib == nil { + err = _get_platform_error() + return + } + defer windows.FreeLibrary(ntdll_lib) + NtQueryInformationProcess := cast(NtQueryInformationProcess_T) windows.GetProcAddress(ntdll_lib, "NtQueryInformationProcess") + if NtQueryInformationProcess == nil { + err = _get_platform_error() + return + } + process_info_size: u32 = --- + process_info: PROCESS_BASIC_INFORMATION = --- + status := NtQueryInformationProcess(ph, .ProcessBasicInformation, &process_info, size_of(process_info), &process_info_size) + if status != 0 { + // TODO(flysand): There's probably a mismatch between NTSTATUS and + // windows userland error codes, I haven't checked. + err = Platform_Error(status) + return + } + if process_info.PebBaseAddress == nil { + // Not sure what the error is + err = General_Error.Unsupported + return + } + process_peb: PEB = --- + bytes_read: uint = --- + read_struct :: proc(h: windows.HANDLE, addr: rawptr, dest: ^$T, br: ^uint) -> windows.BOOL { + return windows.ReadProcessMemory(h, addr, dest, size_of(T), br) + } + read_slice :: proc(h: windows.HANDLE, addr: rawptr, dest: []$T, br: ^uint) -> windows.BOOL { + return windows.ReadProcessMemory(h, addr, raw_data(dest), len(dest)*size_of(T), br) + } + if !read_struct(ph, process_info.PebBaseAddress, &process_peb, &bytes_read) { + err = _get_platform_error() + return + } + process_params: RTL_USER_PROCESS_PARAMETERS = --- + if !read_struct(ph, process_peb.ProcessParameters, &process_params, &bytes_read) { + err = _get_platform_error() + return + } + if .Command_Line in selection || .Command_Args in selection { + TEMP_ALLOCATOR_GUARD() + cmdline_w := make([]u16, process_params.CommandLine.Length, temp_allocator()) + if !read_slice(ph, process_params.CommandLine.Buffer, cmdline_w, &bytes_read) { + err = _get_platform_error() + return + } + if .Command_Line in selection { + cmdline, cmdline_err := windows.utf16_to_utf8(cmdline_w, allocator) + if cmdline_err != nil { + err = cmdline_err + return + } + info.fields |= {.Command_Line} + info.command_line = cmdline + } + if .Command_Args in selection { + args, args_err := _parse_command_line(raw_data(cmdline_w), allocator) + if args_err != nil { + err = args_err + return + } + info.fields += {.Command_Args} + info.command_args = args + } + } + if .Environment in selection { + TEMP_ALLOCATOR_GUARD() + env_len := process_params.EnvironmentSize / 2 + envs_w := make([]u16, env_len, temp_allocator()) + if !read_slice(ph, process_params.Environment, envs_w, &bytes_read) { + err = _get_platform_error() + return + } + envs, envs_err := _parse_environment_block(raw_data(envs_w), allocator) + if envs_err != nil { + err = envs_err + return + } + info.fields |= {.Environment} + info.environment = envs + } + if .Working_Dir in selection { + TEMP_ALLOCATOR_GUARD() + cwd_w := make([]u16, process_params.CurrentDirectoryPath.Length, temp_allocator()) + if !read_slice(ph, process_params.CurrentDirectoryPath.Buffer, cwd_w, &bytes_read) { + err = _get_platform_error() + return + } + cwd, cwd_err := windows.utf16_to_utf8(cwd_w, allocator) + if cwd_err != nil { + err = cwd_err + return + } + info.fields |= {.Working_Dir} + info.working_dir = cwd + } + } + if .Username in selection { + username, username_err := _get_process_user(ph, allocator) + if username_err != nil { + err = username_err + return + } + info.fields |= {.Username} + info.username = username + } + err = nil + return +} + +_process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { + pid := process.pid + info.pid = pid + defer if err != nil { + free_process_info(info, allocator) + } + need_snapprocess := \ + .PPid in selection || + .Priority in selection + need_snapmodule := \ + .Executable_Path in selection + need_peb := \ + .Command_Line in selection || + .Environment in selection || + .Working_Dir in selection + // Data obtained from process snapshots + if need_snapprocess { + entry, entry_err := _process_entry_by_pid(info.pid) + if entry_err != nil { + err = General_Error.Not_Exist + return + } + if .PPid in selection { + info.fields |= {.PPid} + info.ppid = int(entry.th32ParentProcessID) + } + if .Priority in selection { + info.fields |= {.Priority} + info.priority = int(entry.pcPriClassBase) + } + } + if need_snapmodule { + exe_path, exe_path_err := _process_exe_by_pid(pid, allocator) + if exe_path_err != nil { + err = exe_path_err + return + } + info.fields |= {.Executable_Path} + info.executable_path = exe_path + } + ph := cast(windows.HANDLE) process.handle + if need_peb { + ntdll_lib := windows.LoadLibraryW(windows.L("ntdll.dll")) + if ntdll_lib == nil { + err = _get_platform_error() + return + } + defer windows.FreeLibrary(ntdll_lib) + NtQueryInformationProcess := cast(NtQueryInformationProcess_T) windows.GetProcAddress(ntdll_lib, "NtQueryInformationProcess") + if NtQueryInformationProcess == nil { + err = _get_platform_error() + return + } + process_info_size: u32 = --- + process_info: PROCESS_BASIC_INFORMATION = --- + status := NtQueryInformationProcess(ph, .ProcessBasicInformation, &process_info, size_of(process_info), &process_info_size) + if status != 0 { + // TODO(flysand): There's probably a mismatch between NTSTATUS and + // windows userland error codes, I haven't checked. + err = Platform_Error(status) + return + } + if process_info.PebBaseAddress == nil { + // Not sure what the error is + err = General_Error.Unsupported + return + } + process_peb: PEB = --- + bytes_read: uint = --- + read_struct :: proc(h: windows.HANDLE, addr: rawptr, dest: ^$T, br: ^uint) -> windows.BOOL { + return windows.ReadProcessMemory(h, addr, dest, size_of(T), br) + } + read_slice :: proc(h: windows.HANDLE, addr: rawptr, dest: []$T, br: ^uint) -> windows.BOOL { + return windows.ReadProcessMemory(h, addr, raw_data(dest), len(dest)*size_of(T), br) + } + if !read_struct(ph, process_info.PebBaseAddress, &process_peb, &bytes_read) { + err = _get_platform_error() + return + } + process_params: RTL_USER_PROCESS_PARAMETERS = --- + if !read_struct(ph, process_peb.ProcessParameters, &process_params, &bytes_read) { + err = _get_platform_error() + return + } + if .Command_Line in selection || .Command_Args in selection { + TEMP_ALLOCATOR_GUARD() + cmdline_w := make([]u16, process_params.CommandLine.Length, temp_allocator()) + if !read_slice(ph, process_params.CommandLine.Buffer, cmdline_w, &bytes_read) { + err = _get_platform_error() + return + } + if .Command_Line in selection { + cmdline, cmdline_err := windows.utf16_to_utf8(cmdline_w, allocator) + if cmdline_err != nil { + err = cmdline_err + return + } + info.fields |= {.Command_Line} + info.command_line = cmdline + } + if .Command_Args in selection { + args, args_err := _parse_command_line(raw_data(cmdline_w), allocator) + if args_err != nil { + err = args_err + return + } + info.fields += {.Command_Args} + info.command_args = args + } + } + if .Environment in selection { + TEMP_ALLOCATOR_GUARD() + env_len := process_params.EnvironmentSize / 2 + envs_w := make([]u16, env_len, temp_allocator()) + if !read_slice(ph, process_params.Environment, envs_w, &bytes_read) { + err = _get_platform_error() + return + } + envs, envs_err := _parse_environment_block(raw_data(envs_w), allocator) + if envs_err != nil { + err = envs_err + return + } + info.fields |= {.Environment} + info.environment = envs + } + if .Working_Dir in selection { + TEMP_ALLOCATOR_GUARD() + cwd_w := make([]u16, process_params.CurrentDirectoryPath.Length, temp_allocator()) + if !read_slice(ph, process_params.CurrentDirectoryPath.Buffer, cwd_w, &bytes_read) { + err = _get_platform_error() + return + } + cwd, cwd_err := windows.utf16_to_utf8(cwd_w, allocator) + if cwd_err != nil { + err = cwd_err + return + } + info.fields |= {.Working_Dir} + info.working_dir = cwd + } + } + if .Username in selection { + username, username_err := _get_process_user(ph, allocator) + if username_err != nil { + err = username_err + return + } + info.fields |= {.Username} + info.username = username + } + err = nil + return +} + +_current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { + info.pid = cast(int) windows.GetCurrentProcessId() + defer if err != nil { + free_process_info(info, allocator) + } + need_snapprocess := .PPid in selection || .Priority in selection + if need_snapprocess { + entry, entry_err := _process_entry_by_pid(info.pid) + if entry_err != nil { + err = General_Error.Not_Exist + return + } + if .PPid in selection { + info.fields += {.PPid} + info.ppid = int(entry.th32ProcessID) + } + if .Priority in selection { + info.fields += {.Priority} + info.priority = int(entry.pcPriClassBase) + } + } + if .Executable_Path in selection { + exe_filename_w: [256]u16 + path_len := windows.GetModuleFileNameW(nil, raw_data(exe_filename_w[:]), len(exe_filename_w)) + exe_filename, exe_filename_err := windows.utf16_to_utf8(exe_filename_w[:path_len], allocator) + if exe_filename_err != nil { + err = exe_filename_err + return + } + info.fields += {.Executable_Path} + info.executable_path = exe_filename + } + if .Command_Line in selection || .Command_Args in selection { + command_line_w := windows.GetCommandLineW() + if .Command_Line in selection { + command_line, command_line_err := windows.wstring_to_utf8(command_line_w, -1, allocator) + if command_line_err != nil { + err = command_line_err + return + } + info.fields += {.Command_Line} + info.command_line = command_line + } + if .Command_Args in selection { + args, args_err := _parse_command_line(command_line_w, allocator) + if args_err != nil { + err = args_err + return + } + info.fields += {.Command_Args} + info.command_args = args + } + } + if .Environment in selection { + env_block := windows.GetEnvironmentStringsW() + envs, envs_err := _parse_environment_block(env_block, allocator) + if envs_err != nil { + err = envs_err + return + } + info.fields += {.Environment} + info.environment = envs + } + if .Username in selection { + process_handle := windows.GetCurrentProcess() + username, username_err := _get_process_user(process_handle, allocator) + if username_err != nil { + err = username_err + return + } + info.fields += {.Username} + info.username = username + } + if .Working_Dir in selection { + // TODO(flysand): Implement this by reading PEB + err = .Mode_Not_Implemented + return + } + err = nil + return +} + +_process_open :: proc(pid: int, flags: Process_Open_Flags) -> (Process, Error) { + // Note(flysand): The handle will be used for querying information so we + // take the necessary permissions right away. + 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((^File_Impl)(desc.stdout.impl).fd) + } + if desc.stderr != nil { + stderr_handle = windows.HANDLE((^File_Impl)(desc.stderr.impl).fd) + } + if desc.stdin != nil { + stdin_handle = windows.HANDLE((^File_Impl)(desc.stderr.impl).fd) + } + working_dir_w := windows.wstring(nil) + if len(desc.working_dir) > 0 { + working_dir_w = windows.utf8_to_wstring(desc.working_dir, temp_allocator()) + } + 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), + working_dir_w, + &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 +} + +_process_kill :: proc(process: Process) -> (Error) { + // Note(flysand): This is different than what the task manager's "kill process" + // functionality does, as we don't try to send WM_CLOSE message first. This + // is quite a rough way to kill the process, which should be consistent with + // linux. The error code 9 is to mimic SIGKILL event. + if !windows.TerminateProcess(windows.HANDLE(process.handle), 9) { + 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) +_process_entry_by_pid :: proc(pid: int) -> (windows.PROCESSENTRY32W, Error) { + snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if snap == windows.INVALID_HANDLE_VALUE { + return {}, _get_platform_error() + } + defer windows.CloseHandle(snap) + entry := windows.PROCESSENTRY32W { dwSize = size_of(windows.PROCESSENTRY32W) } + status := windows.Process32FirstW(snap, &entry) + found := false + for status { + if u32(pid) == entry.th32ProcessID { + found = true + break + } + status = windows.Process32NextW(snap, &entry) + } + if !found { + return {}, General_Error.Not_Exist + } + return entry, nil +} + +// Note(flysand): Not sure which way it's better to get the executable path: +// via toolhelp snapshots or by reading other process' PEB memory. I have +// a slight suspicion that if both exe path and command line are desired, +// it's faster to just read both from PEB, but maybe the toolhelp snapshots +// are just better...? +@(private) +_process_exe_by_pid :: proc(pid: int, allocator: runtime.Allocator) -> (string, Error) { + snap := windows.CreateToolhelp32Snapshot( + windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, + u32(pid), + ) + if snap == windows.INVALID_HANDLE_VALUE { + return "", _get_platform_error() + } + defer windows.CloseHandle(snap) + entry := windows.MODULEENTRY32W { dwSize = size_of(windows.MODULEENTRY32W) } + status := windows.Module32FirstW(snap, &entry) + if !status { + return "", _get_platform_error() + } + exe_path, err := windows.wstring_to_utf8(raw_data(entry.szExePath[:]), -1, allocator) + if err != nil { + return "", err + } + return exe_path, nil +} + +@(private) +_get_process_user :: proc(process_handle: windows.HANDLE, allocator: runtime.Allocator) -> (full_username: string, err: Error) { + TEMP_ALLOCATOR_GUARD() + token_handle: windows.HANDLE = --- + if !windows.OpenProcessToken(process_handle, windows.TOKEN_QUERY, &token_handle) { + err = _get_platform_error() + return + } + token_user_size: u32 = --- + if !windows.GetTokenInformation(token_handle, .TokenUser, nil, 0, &token_user_size) { + // Note(flysand): Make sure the buffer too small error comes out, and not any other error + err = _get_platform_error() + if v, ok := err.(Platform_Error); !ok || int(v) != 0x7a { + return + } + } + token_user := cast(^windows.TOKEN_USER) raw_data(make([]u8, token_user_size, temp_allocator())) + if !windows.GetTokenInformation(token_handle, .TokenUser, token_user, token_user_size, &token_user_size) { + err = _get_platform_error() + return + } + sid_type: windows.SID_NAME_USE = --- + username_w: [256]u16 = --- + domain_w: [256]u16 = --- + username_chrs: u32 = 256 + domain_chrs: u32 = 256 + if !windows.LookupAccountSidW(nil, token_user.User.Sid, &username_w[0], &username_chrs, &domain_w[0], &domain_chrs, &sid_type) { + err = _get_platform_error() + return + } + username, username_err := windows.utf16_to_utf8(username_w[:username_chrs], temp_allocator()) + if username_err != nil { + err = username_err + return + } + domain, domain_err := windows.utf16_to_utf8(domain_w[:domain_chrs], temp_allocator()) + if domain_err != nil { + err = domain_err + return + } + full_name, full_name_err := strings.concatenate([]string {domain, "\\", username}, allocator) + if full_name_err != nil { + err = full_name_err + return + } + return full_name, nil +} + +@(private) +_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 { + return nil, _get_platform_error() + } + argv, argv_err := make([]string, argc, allocator) + if argv_err != nil { + return nil, argv_err + } + for arg_w, i in argv_w[:argc] { + arg, arg_err := windows.wstring_to_utf8(arg_w, -1, allocator) + if arg_err != nil { + for s in argv[:i] { + delete(s, allocator) + } + delete(argv, allocator) + return nil, arg_err + } + argv[i] = arg + } + 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 + for idx := 0; true; { + if block[idx] == 0x0000 { + zt_count += 1 + if block[idx+1] == 0x0000 { + zt_count += 1 + break + } + } + idx += 1 + } + // Note(flysand): Each string in the environment block is terminated + // by a NUL character. In addition, the environment block itself is + // terminated by a NUL character. So the number of strings in the + // environment block is the number of NUL character minus the + // block terminator. + env_count := zt_count - 1 + envs := make([]string, env_count, allocator) + env_idx := 0 + last_idx := 0 + idx := 0 + for block[idx] != 0x0000 { + for block[idx] != 0x0000 { + idx += 1 + } + env_w := block[last_idx:idx] + env, env_err := windows.utf16_to_utf8(env_w, allocator) + if env_err != nil { + return nil, env_err + } + envs[env_idx] = env + env_idx += 1 + idx += 1 + last_idx = idx + } + 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 { + ProcessBasicInformation = 0, + ProcessDebugPort = 7, + ProcessWow64Information = 26, + ProcessImageFileName = 27, + ProcessBreakOnTermination = 29, + ProcessTelemetryIdInformation = 64, + ProcessSubsystemInformation = 75, +} + +@(private="file") +NtQueryInformationProcess_T :: #type proc ( + ProcessHandle: windows.HANDLE, + ProcessInformationClass: PROCESSINFOCLASS, + ProcessInformation: rawptr, + ProcessInformationLength: u32, + ReturnLength: ^u32, +) -> u32 + +@(private="file") +PROCESS_BASIC_INFORMATION :: struct { + _: rawptr, + PebBaseAddress: ^PEB, + _: [2]rawptr, + UniqueProcessId: ^u32, + _: rawptr, +} + +@(private="file") +PEB :: struct { + _: [2]u8, + BeingDebugged: u8, + _: [1]u8, + _: [2]rawptr, + Ldr: ^PEB_LDR_DATA, + ProcessParameters: ^RTL_USER_PROCESS_PARAMETERS, + _: [104]u8, + _: [52]rawptr, + PostProcessInitRoutine: #type proc "stdcall" (), + _: [128]u8, + _: [1]rawptr, + SessionId: u32, +} + +@(private="file") +PEB_LDR_DATA :: struct { + _: [8]u8, + _: [3]rawptr, + InMemoryOrderModuleList: LIST_ENTRY, +} + +@(private="file") +RTL_USER_PROCESS_PARAMETERS :: struct { + MaximumLength: u32, + Length: u32, + Flags: u32, + DebugFlags: u32, + ConsoleHandle: rawptr, + ConsoleFlags: u32, + StdInputHandle: rawptr, + StdOutputHandle: rawptr, + StdErrorHandle: rawptr, + CurrentDirectoryPath: UNICODE_STRING, + CurrentDirectoryHandle: rawptr, + DllPath: UNICODE_STRING, + ImagePathName: UNICODE_STRING, + CommandLine: UNICODE_STRING, + Environment: rawptr, + StartingPositionLeft: u32, + StartingPositionTop: u32, + Width: u32, + Height: u32, + CharWidth: u32, + CharHeight: u32, + ConsoleTextAttributes: u32, + WindowFlags: u32, + ShowWindowFlags: u32, + WindowTitle: UNICODE_STRING, + DesktopName: UNICODE_STRING, + ShellInfo: UNICODE_STRING, + RuntimeData: UNICODE_STRING, + DLCurrentDirectory: [32]RTL_DRIVE_LETTER_CURDIR, + EnvironmentSize: u32, +} + +RTL_DRIVE_LETTER_CURDIR :: struct { + Flags: u16, + Length: u16, + TimeStamp: u32, + DosPath: UNICODE_STRING, +} + +@(private="file") +UNICODE_STRING :: struct { + Length: u16, + MaximumLength: u16, + Buffer: [^]u16, +} + +@(private="file") +LIST_ENTRY :: struct { + Flink: ^LIST_ENTRY, + Blink: ^LIST_ENTRY, +} \ No newline at end of file