[os2/process] Added process_info() procedure

This commit is contained in:
flysand7
2024-07-15 07:27:43 +11:00
parent 56d55e4a86
commit 6387cd2c24
2 changed files with 594 additions and 58 deletions
+117 -55
View File
@@ -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 { Process_Info_Fields :: bit_set[Process_Info_Field]
handle: _Process_Handle, 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) { Process_Info :: struct {
return _process_open(pid) fields: Process_Info_Fields,
} // The ID of the process.
/*
Close a process handle
*/
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,
}
Process_State :: struct {
pid: int, pid: int,
exit_code: int, // The ID of the parent process.
exited: bool, ppid: int,
success: bool, // The process priority.
system_time: time.Duration, priority: int,
user_time: time.Duration, // The path to the executable, which the process runs.
sys: rawptr, 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,
} }
Signal :: #type proc() /*
Obtain information about a process.
Kill: Signal = nil This procedure obtains an information, given by `selection` parameter of
Interrupt: Signal = nil 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.
find_process :: proc(pid: int) -> (^Process, Process_Error) { **Note**: The resulting information may or may not contain the
return nil, .None 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_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) {
return _process_info(pid, selection, allocator)
} }
current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) {
process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) { return _current_process_info(selection, allocator)
return nil, .None
} }
process_release :: proc(p: ^Process) -> Process_Error { /*
return .None Free the information about the process.
This procedure frees the memory occupied by process info using the provided
allocator. The allocator needs to be the same allocator that was supplied
to the `process_info` function.
*/
free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) {
delete(pi.executable_path, allocator)
delete(pi.command_line, allocator)
delete(pi.command_args, allocator)
for s in pi.environment {
delete(s, allocator)
}
delete(pi.environment, allocator)
delete(pi.cwd, allocator)
} }
process_kill :: proc(p: ^Process) -> Process_Error { // Process_Attributes :: struct {
return .None // dir: string,
} // env: []string,
// files: []^File,
// sys: ^Process_Attributes_OS_Specific,
// }
process_signal :: proc(p: ^Process, sig: Signal) -> Process_Error { // Process_Attributes_OS_Specific :: struct{}
return .None
}
process_wait :: proc(p: ^Process) -> (Process_State, Process_Error) { // Process_Error :: enum {
return {}, .None // None,
} // }
// Process_State :: struct {
// pid: int,
// exit_code: int,
// exited: bool,
// success: bool,
// system_time: time.Duration,
// user_time: time.Duration,
// sys: rawptr,
// }
// Signal :: #type proc()
// 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
// }
// 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
// }
// process_wait :: proc(p: ^Process) -> (Process_State, Process_Error) {
// return {}, .None
// }
+484 -10
View File
@@ -2,6 +2,7 @@
package os2 package os2
import "core:sys/windows" import "core:sys/windows"
import "core:strings"
import "base:runtime" import "base:runtime"
_Process_Handle :: windows.HANDLE _Process_Handle :: windows.HANDLE
@@ -63,18 +64,491 @@ _process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) {
return pid_list[:], nil return pid_list[:], nil
} }
_process_open :: proc(pid: int) -> (Process, Error) { _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
handle := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, cast(u32) pid) info.pid = pid
if handle == windows.INVALID_HANDLE_VALUE { need_snapprocess := \
return {}, _get_platform_error() .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()
} }
return Process { defer windows.CloseHandle(snap)
handle = handle, entry := windows.PROCESSENTRY32W { dwSize = size_of(windows.PROCESSENTRY32W) }
}, nil 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)
}
}
// 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) { _current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
if !windows.CloseHandle(process.handle) { info.pid = cast(int) windows.GetCurrentProcessId()
return _get_platform_error() 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,
}