Browse Source

[os2/process] Added process_info() procedure

flysand7 1 year ago
parent
commit
6387cd2c24
2 changed files with 594 additions and 58 deletions
  1. 110 48
      core/os/os2/process.odin
  2. 484 10
      core/os/os2/process_windows.odin

+ 110 - 48
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,
+// }
 
 
-find_process :: proc(pid: int) -> (^Process, Process_Error) {
-	return nil, .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
-}
+// find_process :: proc(pid: int) -> (^Process, Process_Error) {
+// 	return nil, .None
+// }
 
 
-process_kill :: proc(p: ^Process) -> Process_Error {
-	return .None
-}
 
 
-process_signal :: proc(p: ^Process, sig: Signal) -> Process_Error {
-	return .None
-}
+// process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) {
+// 	return nil, .None
+// }
 
 
-process_wait :: proc(p: ^Process) -> (Process_State, Process_Error) {
-	return {}, .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
core/os/os2/process_windows.odin

@@ -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) {
-	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)
+		}
+	}
+	// 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)
 	}
 	}
-	return Process {
-		handle = handle,
-	}, nil
+	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,
+}