From 2495f1c39aa19f872a3f42b5ccae04db7ec20822 Mon Sep 17 00:00:00 2001 From: flysand7 Date: Wed, 10 Jul 2024 04:05:32 +1100 Subject: [PATCH 01/14] [os2/process]: Fill in basic functions --- core/os/os2/process.odin | 86 +++++++++++++++++++++++++++++--- core/os/os2/process_windows.odin | 46 +++++++++++++++++ 2 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 core/os/os2/process_windows.odin diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 862434b7b..87199ca7a 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -3,35 +3,105 @@ package os2 import "core:sync" import "core:time" import "base:runtime" +import "core:strings" -args: []string +/* + Arguments to the current process. + See `get_args()` for description of the slice. +*/ +args := get_args() + +/* + Obtain the process argument array from the OS. + + Slice, containing arguments to the current process. Each element of the + slice contains a single argument. The first element of the slice would + typically is the path to the currently running executable. +*/ +get_args :: proc() -> []string { + args := make([]string, len(runtime.args__), allocator = context.allocator) + for rt_arg, i in runtime.args__ { + args[i] = cast(string) rt_arg + } + return args[:] +} + +/* + 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() } diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin new file mode 100644 index 000000000..ff99853a3 --- /dev/null +++ b/core/os/os2/process_windows.odin @@ -0,0 +1,46 @@ +//+build windows +package os2 + +import "core:sys/windows" + +_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 +} From 6fab055f435d4a43581ceadde0496249a280c21d Mon Sep 17 00:00:00 2001 From: flysand7 Date: Wed, 10 Jul 2024 04:25:44 +1100 Subject: [PATCH 02/14] [os2/process]: Add process list function --- core/os/os2/process.odin | 7 +++++++ core/os/os2/process_windows.odin | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 87199ca7a..1555d555b 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -104,6 +104,13 @@ get_ppid :: proc() -> int { 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) +} + Process :: struct { pid: int, diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index ff99853a3..6e4bd4cac 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -2,6 +2,7 @@ package os2 import "core:sys/windows" +import "base:runtime" _exit :: proc "contextless" (code: int) -> ! { windows.ExitProcess(u32(code)) @@ -44,3 +45,18 @@ _get_ppid :: proc() -> int { } 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 +} From 56d55e4a86ec51d137531a8bd3ea52ddad5c24af Mon Sep 17 00:00:00 2001 From: flysand7 Date: Wed, 10 Jul 2024 05:29:41 +1100 Subject: [PATCH 03/14] Rebase master --- core/os/os2/process.odin | 23 ++++++++++++++++++----- core/os/os2/process_windows.odin | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 1555d555b..897f7705f 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -111,12 +111,25 @@ process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { return _process_list(allocator) } - +/* + Handle to a process. +*/ Process :: struct { - pid: int, - handle: uintptr, - is_done: b32, - signal_mutex: sync.RW_Mutex, + handle: _Process_Handle, +} + +/* + Obtain a process handle. +*/ +process_open :: proc(pid: int) -> (Process, Error) { + return _process_open(pid) +} + +/* + Close a process handle +*/ +process_close :: proc(process: Process) -> (Error) { + return _process_close(process) } diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 6e4bd4cac..8de8c8347 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -4,6 +4,8 @@ package os2 import "core:sys/windows" import "base:runtime" +_Process_Handle :: windows.HANDLE + _exit :: proc "contextless" (code: int) -> ! { windows.ExitProcess(u32(code)) } @@ -60,3 +62,19 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { } return pid_list[:], nil } + +_process_open :: proc(pid: int) -> (Process, Error) { + handle := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, cast(u32) pid) + if handle == windows.INVALID_HANDLE_VALUE { + return {}, _get_platform_error() + } + return Process { + handle = handle, + }, nil +} + +_process_close :: proc(process: Process) -> (Error) { + if !windows.CloseHandle(process.handle) { + return _get_platform_error() + } +} From 6387cd2c2479fd91b1eae9491c575f093ecb37b6 Mon Sep 17 00:00:00 2001 From: flysand7 Date: Fri, 12 Jul 2024 18:03:06 +1100 Subject: [PATCH 04/14] [os2/process] Added process_info() procedure --- core/os/os2/process.odin | 158 +++++++--- core/os/os2/process_windows.odin | 494 ++++++++++++++++++++++++++++++- 2 files changed, 594 insertions(+), 58 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 897f7705f..4a0481b0f 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -112,80 +112,142 @@ process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { } /* - Handle to a process. + 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 :: struct { - handle: _Process_Handle, +Process_Info_Fields :: bit_set[Process_Info_Field] +Process_Info_Field :: enum { + Executable_Path, + PPid, + Priority, + Command_Line, + Command_Args, + Environment, + Username, + CWD, } /* - Obtain a process handle. + Contains information about the process as obtained by the `process_info()` + procedure. */ -process_open :: proc(pid: int) -> (Process, Error) { - return _process_open(pid) +Process_Info :: struct { + 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. + cwd: string, } /* - Close a process handle + Obtain information about a process. + + This procedure obtains an information, given by `selection` parameter of + a process given by `pid`. + + Use `free_process_info` to free memory allocated by this function. 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 not contain the + selected fields. Please check the `fields` field of the `Process_Info` + struct to see if the struct contains the desired fields **before** checking + the error return of this function. */ -process_close :: proc(process: Process) -> (Error) { - return _process_close(process) +process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { + return _process_info(pid, selection, allocator) } - -Process_Attributes :: struct { - dir: string, - env: []string, - files: []^File, - sys: ^Process_Attributes_OS_Specific, +current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { + return _current_process_info(selection, allocator) } -Process_Attributes_OS_Specific :: struct{} +/* + Free the information about the process. -Process_Error :: enum { - None, + 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.cwd, allocator) } -Process_State :: struct { - pid: int, - exit_code: int, - exited: bool, - success: bool, - system_time: time.Duration, - user_time: time.Duration, - sys: rawptr, -} +// Process_Attributes :: struct { +// dir: string, +// env: []string, +// files: []^File, +// sys: ^Process_Attributes_OS_Specific, +// } -Signal :: #type proc() +// Process_Attributes_OS_Specific :: struct{} -Kill: Signal = nil -Interrupt: Signal = nil +// Process_Error :: enum { +// 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 -} +// 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 -} +// process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) { +// return nil, .None +// } -process_release :: proc(p: ^Process) -> Process_Error { - return .None -} +// process_release :: proc(p: ^Process) -> Process_Error { +// return .None +// } -process_kill :: proc(p: ^Process) -> Process_Error { - return .None -} +// process_kill :: proc(p: ^Process) -> Process_Error { +// return .None +// } -process_signal :: proc(p: ^Process, sig: Signal) -> 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 -} +// 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 8de8c8347..d6d9f0866 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -2,6 +2,7 @@ package os2 import "core:sys/windows" +import "core:strings" import "base:runtime" _Process_Handle :: windows.HANDLE @@ -63,18 +64,491 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { return pid_list[:], nil } -_process_open :: proc(pid: int) -> (Process, Error) { - handle := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, cast(u32) pid) - if handle == windows.INVALID_HANDLE_VALUE { - return {}, _get_platform_error() +_process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { + info.pid = pid + need_snapprocess := \ + .PPid in selection || + .Priority in selection + need_snapmodule := \ + .Executable_Path in selection + need_peb := \ + .Command_Line in selection || + .Environment in selection || + .CWD in selection + need_process_handle := need_peb || .Username in selection + // Data obtained from process snapshots + if need_snapprocess { + snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if snap == windows.INVALID_HANDLE_VALUE { + return info, _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 { + 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) + } } - return Process { - handle = handle, - }, 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...? + if need_snapmodule { + snap := windows.CreateToolhelp32Snapshot( + windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, + u32(pid), + ) + if snap == windows.INVALID_HANDLE_VALUE { + err = _get_platform_error() + return + } + defer windows.CloseHandle(snap) + entry := windows.MODULEENTRY32W { dwSize = size_of(windows.MODULEENTRY32W) } + status := windows.Module32FirstW(snap, &entry) + if !status { + err = _get_platform_error() + return + } + exe_path: string + exe_path, err = windows.wstring_to_utf8(raw_data(entry.szExePath[:]), -1, allocator) + if err != nil { + return + } + info.fields |= {.Executable_Path} + info.executable_path = exe_path + } + defer if .Executable_Path in info.fields && err != nil { + delete(info.executable_path, allocator) + } + 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) + } + defer if .CWD in info.fields && err != nil { + delete(info.cwd, allocator) + } + defer if .Environment in info.fields && err != nil { + for s in info.environment { + delete(s, allocator) + } + delete(info.environment, allocator) + } + defer if .Command_Line in info.fields && err != nil { + delete(info.command_line, allocator) + } + 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 { + 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 + } + 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 .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 .CWD 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 |= {.CWD} + info.cwd = 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_close :: proc(process: Process) -> (Error) { - if !windows.CloseHandle(process.handle) { - return _get_platform_error() +_current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) { + info.pid = cast(int) windows.GetCurrentProcessId() + need_snapprocess := .PPid in selection || .Priority in selection + if need_snapprocess { + snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if snap == windows.INVALID_HANDLE_VALUE { + return + } + defer windows.CloseHandle(snap) + entry := windows.PROCESSENTRY32W { dwSize = size_of(windows.PROCESSENTRY32W) } + status := windows.Process32FirstW(snap, &entry) + for status { + if entry.th32ProcessID == u32(info.pid) { + break + } + status = windows.Process32NextW(snap, &entry) + } + if entry.th32ProcessID != u32(info.pid) { + 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 + } + defer if .Executable_Path in selection && err != nil { + delete(info.executable_path, allocator) + } + if .Command_Line in selection { + command_line_w := windows.GetCommandLineW() + 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 + } + defer if .Command_Line in selection && err != nil { + delete(info.command_line, allocator) + } + 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 + } + defer if .Environment in selection && err != nil { + for s in info.environment { + delete(s, allocator) + } + delete(info.environment) + } + 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 + } + defer if .Username in selection && err != nil { + delete(info.username) + } + err = nil + return } + + +@(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_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) { + zt_count := 0 + for idx := 0; true; idx += 1 { + if block[idx] == 0x0000 { + zt_count += 1 + } + if block[idx] == 0x0000 { + zt_count += 1 + break + } + } + // 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="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 From f3d4a734d89698bfa72e34dfaa2763a3d8f12e3b Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sat, 13 Jul 2024 08:55:58 +1100 Subject: [PATCH 05/14] [os2/process]: Fix environment block null-terminator counting --- core/os/os2/process_windows.odin | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index d6d9f0866..0dde9efe0 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -410,14 +410,15 @@ _get_process_user :: proc(process_handle: windows.HANDLE, allocator: runtime.All @(private) _parse_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) { zt_count := 0 - for idx := 0; true; idx += 1 { + for idx := 0; true; { if block[idx] == 0x0000 { zt_count += 1 + if block[idx+1] == 0x0000 { + zt_count += 1 + break + } } - if block[idx] == 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 From c1f5d8f006830227a7de0eaa5f6e0b088da8bbfc Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sat, 13 Jul 2024 09:02:26 +1100 Subject: [PATCH 06/14] [os2/process]: Improve documentation for *process_info() family of function --- core/os/os2/process.odin | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 4a0481b0f..894aa1e40 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -133,6 +133,8 @@ Process_Info_Field :: enum { 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, @@ -157,22 +159,37 @@ Process_Info :: struct { /* Obtain information about a process. - This procedure obtains an information, given by `selection` parameter of + This procedure obtains an information, specified by `selection` parameter of a process given by `pid`. - Use `free_process_info` to free memory allocated by this function. In case - the function returns an error all temporary allocations would be freed and - as such, calling `free_process_info()` is not needed. + Use `free_process_info` to free the memory allocated by this function. 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 not contain the selected fields. Please check the `fields` field of the `Process_Info` struct to see if the struct contains the desired fields **before** checking - the error return of this function. + the error code returned by this function. */ process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { return _process_info(pid, 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 `Process_Info` may or may not contain the selected + fields. Check the `fields` field of the `Process_Info` struct to see, if the + struct contains the selected 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) } From 5d6e0bc793aa8e80515e1b22aa31c4ef906940fb Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sat, 13 Jul 2024 09:39:35 +1100 Subject: [PATCH 07/14] [os2/process]: Implement retrieving command args in process info --- core/os/os2/process_windows.odin | 83 ++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 14 deletions(-) diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 0dde9efe0..7fe891b38 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -165,6 +165,12 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti defer if .Command_Line in info.fields && err != nil { delete(info.command_line, allocator) } + defer if .Command_Args in selection && err != nil { + for arg in info.command_args { + delete(arg, allocator) + } + delete(info.command_args, allocator) + } if need_peb { ntdll_lib := windows.LoadLibraryW(windows.L("ntdll.dll")) if ntdll_lib == nil { @@ -208,20 +214,31 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti err = _get_platform_error() return } - if .Command_Line in selection { + 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 } - cmdline, cmdline_err := windows.utf16_to_utf8(cmdline_w, allocator) - if cmdline_err != nil { - err = cmdline_err - 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_argv(raw_data(cmdline_w), allocator) + if args_err != nil { + err = args_err + return + } + info.fields += {.Command_Args} + info.command_args = args } - info.fields |= {.Command_Line} - info.command_line = cmdline } if .Environment in selection { TEMP_ALLOCATOR_GUARD() @@ -312,19 +329,36 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime defer if .Executable_Path in selection && err != nil { delete(info.executable_path, allocator) } - if .Command_Line in selection { + if .Command_Line in selection || .Command_Args in selection { command_line_w := windows.GetCommandLineW() - command_line, command_line_err := windows.wstring_to_utf8(command_line_w, -1, allocator) - if command_line_err != nil { - err = command_line_err - return + 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_argv(command_line_w, allocator) + if args_err != nil { + err = args_err + return + } + info.fields += {.Command_Args} + info.command_args = args } - info.fields += {.Command_Line} - info.command_line = command_line } defer if .Command_Line in selection && err != nil { delete(info.command_line, allocator) } + defer if .Command_Args in selection && err != nil { + for arg in info.command_args { + delete(arg, allocator) + } + delete(info.command_args, allocator) + } if .Environment in selection { env_block := windows.GetEnvironmentStringsW() envs, envs_err := _parse_environment_block(env_block, allocator) @@ -407,6 +441,27 @@ _get_process_user :: proc(process_handle: windows.HANDLE, allocator: runtime.All return full_name, nil } +@(private) +_parse_argv :: 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 { + return nil, arg_err + } + argv[i] = arg + } + return argv, nil +} + @(private) _parse_environment_block :: proc(block: [^]u16, allocator: runtime.Allocator) -> ([]string, Error) { zt_count := 0 From e1eed7610c64c884751f189e3d165d7ca1473b01 Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sat, 13 Jul 2024 10:02:49 +1100 Subject: [PATCH 08/14] [os2/process]: Fix leaking memory on errors --- core/os/os2/process_windows.odin | 52 ++++++-------------------------- 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 7fe891b38..647860dc2 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -66,6 +66,9 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { _process_info :: 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 @@ -135,9 +138,6 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti info.fields |= {.Executable_Path} info.executable_path = exe_path } - defer if .Executable_Path in info.fields && err != nil { - delete(info.executable_path, allocator) - } ph := windows.INVALID_HANDLE_VALUE if need_process_handle { ph = windows.OpenProcess( @@ -153,24 +153,6 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti defer if ph != windows.INVALID_HANDLE_VALUE { windows.CloseHandle(ph) } - defer if .CWD in info.fields && err != nil { - delete(info.cwd, allocator) - } - defer if .Environment in info.fields && err != nil { - for s in info.environment { - delete(s, allocator) - } - delete(info.environment, allocator) - } - defer if .Command_Line in info.fields && err != nil { - delete(info.command_line, allocator) - } - defer if .Command_Args in selection && err != nil { - for arg in info.command_args { - delete(arg, allocator) - } - delete(info.command_args, allocator) - } if need_peb { ntdll_lib := windows.LoadLibraryW(windows.L("ntdll.dll")) if ntdll_lib == nil { @@ -287,6 +269,9 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti _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 { snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) @@ -326,9 +311,6 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime info.fields += {.Executable_Path} info.executable_path = exe_filename } - defer if .Executable_Path in selection && err != nil { - delete(info.executable_path, allocator) - } if .Command_Line in selection || .Command_Args in selection { command_line_w := windows.GetCommandLineW() if .Command_Line in selection { @@ -350,15 +332,6 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime info.command_args = args } } - defer if .Command_Line in selection && err != nil { - delete(info.command_line, allocator) - } - defer if .Command_Args in selection && err != nil { - for arg in info.command_args { - delete(arg, allocator) - } - delete(info.command_args, allocator) - } if .Environment in selection { env_block := windows.GetEnvironmentStringsW() envs, envs_err := _parse_environment_block(env_block, allocator) @@ -369,12 +342,6 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime info.fields += {.Environment} info.environment = envs } - defer if .Environment in selection && err != nil { - for s in info.environment { - delete(s, allocator) - } - delete(info.environment) - } if .Username in selection { process_handle := windows.GetCurrentProcess() username, username_err := _get_process_user(process_handle, allocator) @@ -385,9 +352,6 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime info.fields += {.Username} info.username = username } - defer if .Username in selection && err != nil { - delete(info.username) - } err = nil return } @@ -455,6 +419,10 @@ _parse_argv :: proc(cmd_line_w: [^]u16, allocator: runtime.Allocator) -> ([]stri for arg_w, i in argv_w[:argc] { arg, arg_err := windows.wstring_to_utf8(arg_w, -1, allocator) if arg_err != nil { + for arg in argv[:i] { + delete(arg, allocator) + } + delete(argv, allocator) return nil, arg_err } argv[i] = arg From 63d94301fc5614334ee93a088abf2cc20a31268a Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sun, 14 Jul 2024 14:56:00 +1100 Subject: [PATCH 09/14] [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 { From 8f4755532ef1386ba031fdda774d9c7137f5569a Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sun, 14 Jul 2024 15:28:37 +1100 Subject: [PATCH 10/14] [os2/process]: Adjust docs on process_close function --- core/os/os2/process.odin | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index ecf9354b8..fb6766747 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -351,7 +351,8 @@ process_wait :: proc(process: Process, timeout := TIMEOUT_INFINITE) -> (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. + 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) From 4eca60946c294e3551e70641051e85f0d23b17bd Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sun, 14 Jul 2024 15:47:40 +1100 Subject: [PATCH 11/14] [os2/process]: Refactor process_info procs, add process_info_by_handle --- core/os/os2/process.odin | 49 ++++-- core/os/os2/process_windows.odin | 267 ++++++++++++++++++++++++------- 2 files changed, 252 insertions(+), 64 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index fb6766747..212032259 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -168,17 +168,37 @@ Process_Info :: struct { 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 function. In + 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 not contain the - selected fields. Please check the `fields` field of the `Process_Info` - struct to see if the struct contains the desired fields **before** checking - the error code returned by this function. + **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 :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { - return _process_info(pid, selection, allocator) +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) } /* @@ -191,15 +211,24 @@ process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runtim 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 `Process_Info` may or may not contain the selected - fields. Check the `fields` field of the `Process_Info` struct to see, if the - struct contains the selected fields **before** checking the error code + **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. diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 6e461bade..4e7c7e62d 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -66,7 +66,7 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { return pid_list[:], nil } -_process_info :: 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) { info.pid = pid defer if err != nil { free_process_info(info, allocator) @@ -83,22 +83,8 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti need_process_handle := need_peb || .Username in selection // Data obtained from process snapshots if need_snapprocess { - snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) - if snap == windows.INVALID_HANDLE_VALUE { - return info, _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 { + entry, entry_err := _process_entry_by_pid(info.pid) + if entry_err != nil { err = General_Error.Not_Exist return } @@ -111,30 +97,10 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti info.priority = int(entry.pcPriClassBase) } } - // 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...? if need_snapmodule { - snap := windows.CreateToolhelp32Snapshot( - windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, - u32(pid), - ) - if snap == windows.INVALID_HANDLE_VALUE { - err = _get_platform_error() - return - } - defer windows.CloseHandle(snap) - entry := windows.MODULEENTRY32W { dwSize = size_of(windows.MODULEENTRY32W) } - status := windows.Module32FirstW(snap, &entry) - if !status { - err = _get_platform_error() - return - } - exe_path: string - exe_path, err = windows.wstring_to_utf8(raw_data(entry.szExePath[:]), -1, allocator) - if err != nil { + 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} @@ -269,6 +235,161 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti 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 || + .CWD 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 .CWD 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 |= {.CWD} + info.cwd = 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 { @@ -276,20 +397,8 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime } need_snapprocess := .PPid in selection || .Priority in selection if need_snapprocess { - snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) - if snap == windows.INVALID_HANDLE_VALUE { - return - } - defer windows.CloseHandle(snap) - entry := windows.PROCESSENTRY32W { dwSize = size_of(windows.PROCESSENTRY32W) } - status := windows.Process32FirstW(snap, &entry) - for status { - if entry.th32ProcessID == u32(info.pid) { - break - } - status = windows.Process32NextW(snap, &entry) - } - if entry.th32ProcessID != u32(info.pid) { + entry, entry_err := _process_entry_by_pid(info.pid) + if entry_err != nil { err = General_Error.Not_Exist return } @@ -474,6 +583,56 @@ _filetime_to_duration :: proc(filetime: windows.FILETIME) -> time.Duration { 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() From b7ccfed9af60392e44d281770d55789f1d59df13 Mon Sep 17 00:00:00 2001 From: flysand7 Date: Sun, 14 Jul 2024 15:59:18 +1100 Subject: [PATCH 12/14] [os2/process]: Implement process_kill --- core/os/os2/process.odin | 50 +++++++------------------------- core/os/os2/process_windows.odin | 11 +++++-- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 212032259..905aa2182 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -26,11 +26,11 @@ args := get_args() typically is the path to the currently running executable. */ get_args :: proc() -> []string { - args := make([]string, len(runtime.args__), allocator = context.allocator) + result := make([]string, len(runtime.args__), allocator = context.allocator) for rt_arg, i in runtime.args__ { - args[i] = cast(string) rt_arg + result[i] = cast(string) rt_arg } - return args[:] + return result[:] } /* @@ -387,42 +387,12 @@ process_close :: proc(process: Process) -> (Error) { return _process_close(process) } -// Process_Attributes :: struct { -// dir: string, -// env: []string, -// files: []^File, -// sys: ^Process_Attributes_OS_Specific, -// } - -// Process_Attributes_OS_Specific :: struct{} - -// Process_Error :: enum { -// None, -// } - - - -// Signal :: #type proc() - -// Kill: Signal = nil -// Interrupt: Signal = nil - -// process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) { -// return nil, .None -// } - -// process_release :: proc(p: ^Process) -> Process_Error { -// return .None -// } - -// process_kill :: proc(p: ^Process) -> Process_Error { -// return .None -// } - -// process_signal :: proc(p: ^Process, sig: Signal) -> 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) +} diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 4e7c7e62d..251900020 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -577,6 +577,13 @@ _process_close :: proc(process: Process) -> (Error) { return nil } +_process_kill :: proc(process: Process) -> (Error) { + 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) @@ -695,8 +702,8 @@ _parse_command_line :: proc(cmd_line_w: [^]u16, allocator: runtime.Allocator) -> for arg_w, i in argv_w[:argc] { arg, arg_err := windows.wstring_to_utf8(arg_w, -1, allocator) if arg_err != nil { - for arg in argv[:i] { - delete(arg, allocator) + for s in argv[:i] { + delete(s, allocator) } delete(argv, allocator) return nil, arg_err From 255f00d9711af0c55c87b97d5d36486bfecd0f76 Mon Sep 17 00:00:00 2001 From: flysand7 Date: Mon, 15 Jul 2024 20:24:05 +1100 Subject: [PATCH 13/14] [os2/process]: Implement missing functionality, update docs --- core/os/os2/process.odin | 15 +++++++----- core/os/os2/process_windows.odin | 42 +++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 905aa2182..3dcb6473f 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -131,7 +131,7 @@ Process_Info_Field :: enum { Command_Args, Environment, Username, - CWD, + Working_Dir, } /* @@ -159,7 +159,7 @@ Process_Info :: struct { // The username of the user who started the process. username: string, // The current working directory of the process. - cwd: string, + working_dir: string, } /* @@ -244,7 +244,7 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) { delete(s, allocator) } delete(pi.environment, allocator) - delete(pi.cwd, allocator) + delete(pi.working_dir, allocator) } /* @@ -288,8 +288,10 @@ process_open :: proc(pid: int, flags := Process_Open_Flags {}) -> (Process, Erro Process_Desc :: struct { // OS-specific attributes. sys_attr: _Sys_Process_Attributes, - // The working directory of the process. - dir: string, + // 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, @@ -329,7 +331,8 @@ Process_Desc :: struct { 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. + 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) diff --git a/core/os/os2/process_windows.odin b/core/os/os2/process_windows.odin index 251900020..e16cedd9f 100644 --- a/core/os/os2/process_windows.odin +++ b/core/os/os2/process_windows.odin @@ -79,7 +79,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator need_peb := \ .Command_Line in selection || .Environment in selection || - .CWD in selection + .Working_Dir in selection need_process_handle := need_peb || .Username in selection // Data obtained from process snapshots if need_snapprocess { @@ -122,6 +122,8 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator 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() @@ -206,7 +208,7 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator info.fields |= {.Environment} info.environment = envs } - if .CWD in selection { + 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) { @@ -218,8 +220,8 @@ _process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator err = cwd_err return } - info.fields |= {.CWD} - info.cwd = cwd + info.fields |= {.Working_Dir} + info.working_dir = cwd } } if .Username in selection { @@ -249,7 +251,7 @@ _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields need_peb := \ .Command_Line in selection || .Environment in selection || - .CWD in selection + .Working_Dir in selection // Data obtained from process snapshots if need_snapprocess { entry, entry_err := _process_entry_by_pid(info.pid) @@ -361,7 +363,7 @@ _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields info.fields |= {.Environment} info.environment = envs } - if .CWD in selection { + 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) { @@ -373,8 +375,8 @@ _process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields err = cwd_err return } - info.fields |= {.CWD} - info.cwd = cwd + info.fields |= {.Working_Dir} + info.working_dir = cwd } } if .Username in selection { @@ -463,11 +465,18 @@ _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime 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 @@ -505,10 +514,17 @@ _process_start :: proc(desc: Process_Desc) -> (Process, Error) { 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) + stdout_handle = windows.HANDLE((^File_Impl)(desc.stdout.impl).fd) } if desc.stderr != nil { - stderr_handle = windows.HANDLE(desc.stderr.impl.fd) + 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( @@ -519,7 +535,7 @@ _process_start :: proc(desc: Process_Desc) -> (Process, Error) { true, windows.CREATE_UNICODE_ENVIRONMENT|windows.NORMAL_PRIORITY_CLASS, raw_data(environment_block_w), - nil, + working_dir_w, &windows.STARTUPINFOW { cb = size_of(windows.STARTUPINFOW), hStdError = stderr_handle, @@ -578,6 +594,10 @@ _process_close :: proc(process: Process) -> (Error) { } _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() } From 8df61b7209169a25ad7776cd5c9e0ec51cb47dc0 Mon Sep 17 00:00:00 2001 From: flysand7 Date: Mon, 15 Jul 2024 23:28:03 +1100 Subject: [PATCH 14/14] [os2/process]: Make get_args() private and use heap_allocator --- core/os/os2/process.odin | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/core/os/os2/process.odin b/core/os/os2/process.odin index 3dcb6473f..d407ffb18 100644 --- a/core/os/os2/process.odin +++ b/core/os/os2/process.odin @@ -13,20 +13,12 @@ TIMEOUT_INFINITE :: time.MIN_DURATION // Note(flysand): Any negative duration wi /* Arguments to the current process. - - See `get_args()` for description of the slice. */ args := get_args() -/* - Obtain the process argument array from the OS. - - Slice, containing arguments to the current process. Each element of the - slice contains a single argument. The first element of the slice would - typically is the path to the currently running executable. -*/ +@(private="file") get_args :: proc() -> []string { - result := make([]string, len(runtime.args__), allocator = context.allocator) + result := make([]string, len(runtime.args__), heap_allocator()) for rt_arg, i in runtime.args__ { result[i] = cast(string) rt_arg }