Browse Source

[os2/process]: Refactor process_info procs, add process_info_by_handle

flysand7 1 year ago
parent
commit
4eca60946c
2 changed files with 251 additions and 63 deletions
  1. 39 10
      core/os/os2/process.odin
  2. 212 53
      core/os/os2/process_windows.odin

+ 39 - 10
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.
 

+ 212 - 53
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,27 +235,170 @@ _process_info :: proc(pid: int, selection: Process_Info_Fields, allocator: runti
 	return
 }
 
-_current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
-	info.pid = cast(int) windows.GetCurrentProcessId()
+_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_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 {
-		snap := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
-		if snap == windows.INVALID_HANDLE_VALUE {
+		entry, entry_err := _process_entry_by_pid(info.pid)
+		if entry_err != nil {
+			err = General_Error.Not_Exist
 			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
+		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
 			}
-			status = windows.Process32NextW(snap, &entry)
 		}
-		if entry.th32ProcessID != u32(info.pid) {
+		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 {
+		free_process_info(info, allocator)
+	}
+	need_snapprocess := .PPid in selection || .Priority in selection
+	if need_snapprocess {
+		entry, entry_err := _process_entry_by_pid(info.pid)
+		if entry_err != nil {
 			err = General_Error.Not_Exist
 			return
 		}
@@ -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()