# Platform-Specific Process Discovery in witr: /proc, ps, and Win32 API Differences

> Discover platform-specific process handling in witr: explore /proc on Linux, ps on macOS/BSD, and Win32 APIs on Windows to understand how witr gathers process data.

- Repository: [Pranshu Parmar/witr](https://github.com/pranshuparmar/witr)
- Tags: internals
- Published: 2026-08-09

---

**witr uses direct /proc filesystem reads on Linux, invokes the ps command on macOS and BSD systems, and calls Win32 APIs on Windows to gather process information, with each implementation populating the same internal model.Process structure.**

The witr repository by pranshuparmar implements portable process inspection across Unix-like and Windows systems. Because operating systems expose process metadata through fundamentally different interfaces, the codebase maintains three distinct platform-specific implementations. Understanding these platform-specific differences in witr's use of /proc, ps, and Win32 API reveals how the tool achieves consistent cross-platform functionality while respecting each OS's native capabilities.

## Linux: Direct /proc Filesystem Access

On Linux systems, witr reads the kernel's pseudo-filesystem directly rather than spawning external utilities. This approach eliminates subprocess overhead and provides deterministic access to process state.

### Implementation in process_linux.go

The `ReadProcess` function in [`internal/proc/process_linux.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/process_linux.go) opens and parses virtual files under `/proc/<pid>/` to construct a `model.Process` instance. It reads `stat` for process state and metrics, `environ` for environment variables, `cwd` for the current working directory via symlink resolution, and `cgroup` for container detection.

```go
// internal/proc/process_linux.go – ReadProcess
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil { … }                     // process vanished while reading
envBytes, _ := os.ReadFile(fmt.Sprintf("/proc/%d/environ", pid))
cwd, _ := os.Readlink(fmt.Sprintf("/proc/%d/cwd", pid))
cgroup, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pid))
// … parse `stat` fields, detect container, health, etc.

```

This implementation checks for zombie processes by examining the state field (`state == "Z"`) and accesses file descriptor listings via `/proc/<pid>/fd` for complete process inspection. The method captures per-process limits and exact command-line arguments without race conditions, provided the initial `/proc/<pid>` directory existence check succeeds.

## macOS and BSD: Parsing ps Output

On Darwin (macOS) and BSD variants, witr invokes the system `ps` command and parses its textual output. This fallback mechanism accommodates platforms that lack a standardized procfs implementation.

### Implementation Details

The `readPIDCommMap` function in [`internal/proc/psenv_unix.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/psenv_unix.go) executes platform-specific ps commands. On macOS, it runs `ps -axo pid=,comm=,args=`, while FreeBSD uses `ps -axww -o pid -o comm -o args`. The code scans the output line-by-line to map PIDs to command information.

```go
// internal/proc/psenv_unix.go – readPIDCommMap
cmd := exec.Command("ps", "-axo", "pid=,comm=")
out, _ := cmd.Output()
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
    // each line: "<pid> <comm>"
    parts := strings.Fields(scanner.Text())
    pid, _ := strconv.Atoi(parts[0])
    pidCommMap[pid] = strings.Join(parts[1:], " ")
}

```

This approach relies on parsing text output, making it vulnerable to format changes in future OS updates, though it provides consistent PID, executable name, and argument retrieval across BSD-family systems. Container detection falls back to environment variable inspection (e.g., checking for `SNAP_NAME`) rather than cgroup analysis.

## Windows: Win32 API System Calls

On Windows, witr bypasses both filesystem abstraction and external processes, instead calling the native Win32 API directly through system DLLs including `kernel32.dll`, `psapi.dll`, and `ntdll.dll`.

### Low-Level Process Inspection

The [`process_windows.go`](https://github.com/pranshuparmar/witr/blob/main/process_windows.go) file implements `GetProcessDetailedInfo` to wrap Windows-specific calls. It uses `OpenProcess` to obtain a handle with `PROCESS_QUERY_LIMITED_INFORMATION` access, `QueryFullProcessImageNameW` for the executable path, and `GetProcessTimes` for CPU accounting. The implementation can also read the Process Environment Block (PEB) via `ReadProcessMemory` for additional metadata.

```go
// internal/proc/process_windows.go – GetProcessDetailedInfo
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil { return … }
defer windows.CloseHandle(handle)

var exePath [windows.MAX_PATH]uint16
size := uint32(len(exePath))
err = windows.QueryFullProcessImageName(handle, 0, &exePath[0], &size)
if err != nil { return … }
imageName := windows.UTF16ToString(exePath[:size])
// … retrieve environment, start time, CPU usage via GetProcessTimes, etc.

```

The Windows implementation handles Unicode string translation and interprets structures like `PROCESS_BASIC_INFORMATION` and `IO_COUNTERS`. It requires Windows 10 or later for full API availability and returns "process does not exist" errors when calls fail due to permission or timing issues.

## Performance and Reliability Comparison

Each platform implementation trades different characteristics for portability:

- **Linux (/proc)**: Offers the lowest overhead through direct kernel interface reads without subprocess creation. The approach eliminates race conditions with disappearing processes if the initial `/proc/<pid>` existence check succeeds, and captures comprehensive data including cgroups for container detection.

- **macOS/BSD (ps)**: Incurs higher latency due to subprocess spawning and text parsing overhead. The method depends on consistent `ps` output formatting and provides limited container detection (falling back to environment variables like `SNAP_NAME`), but requires no kernel modules or elevated privileges beyond standard process visibility.

- **Windows (Win32 API)**: Delivers performance comparable to Linux's /proc approach through direct system calls. The implementation depends on specific API availability (requiring Windows 10 or later for full functionality) and handles structure marshaling for `PEB` and `Win32ProcessInfo` to populate the generic model.

## Summary

- **Linux implementation** reads `/proc/<pid>/stat`, `environ`, `cwd`, and `cgroup` files directly in [`internal/proc/process_linux.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/process_linux.go) for minimal overhead and rich metadata extraction.
- **macOS and BSD systems** execute `ps` commands via [`internal/proc/psenv_unix.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/psenv_unix.go) and parse textual output to obtain PID and command information.
- **Windows platform** calls Win32 APIs including `OpenProcess`, `QueryFullProcessImageNameW`, and `GetProcessTimes` in [`internal/proc/process_windows.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/process_windows.go) to translate native structures into the `model.Process` format.
- All three implementations populate the same `model.Process` structure defined in [`pkg/model/process.go`](https://github.com/pranshuparmar/witr/blob/main/pkg/model/process.go), ensuring consistent data models despite divergent data collection mechanisms.
- Container detection varies by platform: Linux uses cgroup analysis, while other platforms rely on environment variable inspection.

## Frequently Asked Questions

### Why does witr use different process discovery methods across platforms?

Operating systems expose process information through incompatible interfaces. Linux provides the `/proc` pseudo-filesystem as a standard kernel feature, BSD systems traditionally expose limited process data requiring userland utilities like `ps`, and Windows maintains proprietary internal structures accessible only through documented Win32 APIs. witr adapts to each platform's native mechanism to maximize compatibility and data completeness without requiring kernel extensions.

### How does witr handle process enumeration on macOS without /proc?

macOS does not mount a procfs by default, so witr falls back to executing the `ps` command with specific output flags (`-axo pid=,comm=,args=`) and parsing the resulting text. This occurs in [`internal/proc/psenv_unix.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/psenv_unix.go), where the `readPIDCommMap` function builds a PID-to-command mapping from the utility's output, providing equivalent functionality to Linux's direct file reads through a subprocess interface.

### What Win32 API functions does witr use for Windows process inspection?

On Windows, witr calls `OpenProcess` to obtain process handles, `QueryFullProcessImageNameW` for executable paths, `GetProcessTimes` for CPU accounting, and can access `ReadProcessMemory` for PEB (Process Environment Block) inspection. These functions from `kernel32.dll` and `psapi.dll` are wrapped in [`internal/proc/process_windows.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/process_windows.go) to populate the same `model.Process` fields that `/proc` provides on Linux.

### Are there performance differences between the Linux and Windows implementations?

Both Linux and Windows implementations provide comparable low-latency process discovery because both use direct system interfaces without spawning subprocesses. The Linux `/proc` approach requires only `open` and `read` syscalls, while Windows uses equivalent lightweight Win32 calls. In contrast, the macOS/BSD `ps` approach introduces measurable overhead due to process creation and text parsing, making it slower for high-frequency process enumeration.