Merge pull request #3310 from flysand7/core-process

Addition of `core:os2/process` api.
This commit is contained in:
gingerBill
2024-07-16 11:24:24 +01:00
committed by GitHub
2 changed files with 1285 additions and 50 deletions

View File

@@ -3,100 +3,391 @@ package os2
import "core:sync"
import "core:time"
import "base:runtime"
import "core:strings"
args: []string
/*
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.
*/
args := get_args()
@(private="file")
get_args :: proc() -> []string {
result := make([]string, len(runtime.args__), heap_allocator())
for rt_arg, i in runtime.args__ {
result[i] = cast(string) rt_arg
}
return result[:]
}
/*
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()
}
/*
Obtain ID's of all processes running in the system.
*/
process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) {
return _process_list(allocator)
}
/*
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_Info_Fields :: bit_set[Process_Info_Field]
Process_Info_Field :: enum {
Executable_Path,
PPid,
Priority,
Command_Line,
Command_Args,
Environment,
Username,
Working_Dir,
}
/*
Contains information about the process as obtained by the `process_info()`
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,
// 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.
working_dir: string,
}
/*
Obtain information about a process.
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 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_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)
}
/*
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 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.
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.working_dir, 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,
is_done: b32,
signal_mutex: sync.RW_Mutex,
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,
}
Process_Attributes :: struct {
dir: string,
/*
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. 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,
// 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,
files: []^File,
sys: ^Process_Attributes_OS_Specific,
// 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,
}
Process_Attributes_OS_Specific :: struct{}
/*
Create a new process and obtain its handle.
Process_Error :: enum {
None,
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 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)
}
/*
The state of the process after it has finished execution.
*/
Process_State :: struct {
pid: int,
exit_code: int,
exited: bool,
success: bool,
// 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,
user_time: time.Duration,
sys: rawptr,
// The time the process has spend executing in userspace.
user_time: time.Duration,
}
Signal :: #type proc()
/*
Wait for a process event.
Kill: Signal = nil
Interrupt: Signal = nil
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.
find_process :: proc(pid: int) -> (^Process, Process_Error) {
return nil, .None
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.
process_start :: proc(name: string, argv: []string, attr: ^Process_Attributes) -> (^Process, Process_Error) {
return nil, .None
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, wait for the process to finish,
then close the handle.
*/
process_close :: proc(process: Process) -> (Error) {
return _process_close(process)
}
process_release :: proc(p: ^Process) -> 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)
}
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
}

View File

@@ -0,0 +1,944 @@
//+build windows
package os2
import "core:sys/windows"
import "core:strings"
import "core:time"
import "base:runtime"
_Process_Handle :: windows.HANDLE
_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
}
_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
}
_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)
}
need_snapprocess := \
.PPid in selection ||
.Priority in selection
need_snapmodule := \
.Executable_Path in selection
need_peb := \
.Command_Line in selection ||
.Environment in selection ||
.Working_Dir in selection
need_process_handle := need_peb || .Username 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 := 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)
}
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()
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 .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) {
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 |= {.Working_Dir}
info.working_dir = 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_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 ||
.Working_Dir 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 .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) {
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 |= {.Working_Dir}
info.working_dir = 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
}
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
}
if .Command_Line in selection || .Command_Args in selection {
command_line_w := windows.GetCommandLineW()
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_command_line(command_line_w, allocator)
if args_err != nil {
err = args_err
return
}
info.fields += {.Command_Args}
info.command_args = args
}
}
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
}
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
}
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
}
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((^File_Impl)(desc.stdout.impl).fd)
}
if desc.stderr != nil {
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(
nil,
command_line_w,
nil,
nil,
true,
windows.CREATE_UNICODE_ENVIRONMENT|windows.NORMAL_PRIORITY_CLASS,
raw_data(environment_block_w),
working_dir_w,
&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
}
_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()
}
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)
_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()
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_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 {
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 {
for s in argv[:i] {
delete(s, allocator)
}
delete(argv, allocator)
return nil, arg_err
}
argv[i] = arg
}
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
for idx := 0; true; {
if block[idx] == 0x0000 {
zt_count += 1
if block[idx+1] == 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
// 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)
_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 {
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,
}