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