# Platform-Specific Process Resolution in witr: Linux, macOS, and Windows Differences Explained

> Explore witr's platform specific process resolution strategies for Linux macOS and Windows. Learn how witr adapts to each OS for efficient process detection.

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

---

**`witr` resolves target processes using OS-specific strategies in `internal/target`, with Linux scanning `/proc`, macOS parsing `ps` output, and Windows calling the Toolhelp API—each falling back to native service managers when needed.**

The `witr` command-line tool provides unified process inspection across operating systems, but its implementation adapts to each platform's native APIs. This article breaks down how `pranshuparmar/witr` handles **platform-specific process resolution** on Linux, macOS, and Windows, examining the source code that makes cross-platform targeting possible.

## Linux: /proc Filesystem and systemd Fallback

### Process Enumeration via /proc

On Linux, `witr` walks the `/proc` filesystem directly. In [`internal/target/name_linux.go`](https://github.com/pranshuparmar/witr/blob/main/internal/target/name_linux.go), the `ResolveName` function reads numeric directories representing PIDs and extracts:

- `<pid>/comm` — the executable name (15 bytes max from the kernel)
- `<pid>/cmdline` — the full null-separated command line

Both values are lower-cased and matched against the target name using exact or fuzzy logic.

```go
// Simplified excerpt from name_linux.go
func ResolveName(name string, exact bool) ([]Target, error) {
    entries, _ := os.ReadDir("/proc")
    for _, entry := range entries {
        pid, err := strconv.Atoi(entry.Name())
        if err != nil { continue } // skip non-numeric entries
        
        comm, _ := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
        cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
        // ... matching logic
    }
}

```

### Ancestor Filtering

To prevent `witr` from targeting itself, the implementation calls `procpkg.ResolveAncestry(os.Getpid())` to build a set of PIDs to exclude—including the current process and all its parent shells.

### systemd Service Resolution

When process scanning yields no matches, `witr` falls back to **systemd**. The helper `resolveSystemdServiceMainPID` in [`internal/target/port_linux.go`](https://github.com/pranshuparmar/witr/blob/main/internal/target/port_linux.go) executes:

```bash
systemctl show -p MainPID <service-name>

```

If the service is active, `MainPID` provides the target process ID directly.

## macOS: ps Parsing and launchctl Integration

### Process Discovery with ps

macOS lacks a `/proc` equivalent, so [`internal/target/name_darwin.go`](https://github.com/pranshuparmar/witr/blob/main/internal/target/name_darwin.go) spawns `ps -axo pid=,comm=,args=` and parses its tabular output. The columns map directly to:

- PID (integer)
- Command name (`comm`)
- Full arguments (`args`)

Matching applies the same exact/fuzzy rules as Linux, ensuring consistent behavior across platforms.

### Ancestor Handling

Darwin uses the same `procpkg.ResolveAncestry` approach, building an exclusion set from the current PID upward through the process tree.

### launchd Service Lookup

For macOS services, `witr` queries **launchd**. Before invocation, labels are validated against a strict regex in [`internal/target/port_darwin.go`](https://github.com/pranshuparmar/witr/blob/main/internal/target/port_darwin.go):

```go
var launchdLabelRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)

```

This prevents command injection when constructing the `launchctl` call:

```bash
launchctl print system/<label>

```

The output is parsed to extract the service's running PID.

## Windows: WinAPI Toolhelp and Service Control Manager

### Native Process Enumeration

Windows implementation diverges significantly. Process enumeration lives in [`internal/proc/process_windows.go`](https://github.com/pranshuparmar/witr/blob/main/internal/proc/process_windows.go), using the **Toolhelp32 API**:

```c
// Windows API calls (wrapped via syscall)
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
Process32First(hSnapshot, &procEntry)
Process32Next(hSnapshot, &procEntry)

```

For each process, `witr` retrieves:

- **Executable path** via `QueryFullProcessImageName`
- **Command line** via `NtQueryInformationProcess` or `GetProcessMemoryInfo` heuristics
- **PID** from the `PROCESSENTRY32` structure

### SCM Service Resolution

When a service name is specified, `witr` communicates with the **Service Control Manager** through `advapi32.dll`:

```go
// Excerpt from port_windows.go
mgr, _ := winio.OpenSCManager("", "", windows.SC_MANAGER_CONNECT)
svc, _ := winio.OpenService(mgr, serviceName, windows.SERVICE_QUERY_STATUS)
svc.QueryServiceStatusEx(windows.SC_STATUS_PROCESS_INFO)

```

The `SERVICE_STATUS_PROCESS` structure returns the service's current PID directly.

## Cross-Platform Comparison Table

| Aspect | Linux | macOS | Windows |
|--------|-------|-------|---------|
| **Primary API** | `/proc` filesystem | `ps` command | Toolhelp32 API |
| **Process info source** | `comm`, `cmdline` files | `ps` output columns | `PROCESSENTRY32`, `QueryFullProcessImageName` |
| **Service fallback** | `systemctl` | `launchctl` | Service Control Manager |
| **Service validation** | systemd internal | Regex: `^[a-zA-Z0-9._-]+$` | SCM handle validation |
| **Ancestor resolution** | `procpkg.ResolveAncestry` | `procpkg.ResolveAncestry` | `procpkg.ResolveAncestry` |
| **Build-selected file** | [`name_linux.go`](https://github.com/pranshuparmar/witr/blob/main/name_linux.go) | [`name_darwin.go`](https://github.com/pranshuparmar/witr/blob/main/name_darwin.go) | Windows equivalent |

## Practical Usage

The CLI abstracts all platform differences behind a single `-p` flag:

```bash

# fuzzy match any process containing "node"

witr -p node

# Linux: resolve nginx systemd service

witr -p nginx.service

# macOS: resolve WindowServer launchd service

witr -p com.apple.windowserver

# Windows: resolve Windows Update service

witr -p wuauserv

```

All invocations route through `target.ResolveName()`, with Go build tags selecting the appropriate implementation file at compile time.

## Summary

- **Linux** uses direct `/proc` access with `systemd` fallback—fastest and most detailed, no external process spawning
- **macOS** relies on `ps` parsing and `launchctl`—portable across Darwin versions, requires subprocess execution
- **Windows** calls native WinAPI and SCM—avoids command-line parsing entirely, most robust against formatting changes
- **All platforms** share ancestor exclusion logic via `procpkg.ResolveAncestry` to prevent self-targeting
- **Service integration** is platform-native: `systemctl` (Linux), `launchctl` (macOS), SCM (Windows)

## Frequently Asked Questions

### How does witr prevent targeting its own process?

`witr` calls `procpkg.ResolveAncestry(os.Getpid())` on every platform to build a set of PIDs representing itself and all parent processes. These PIDs are excluded before any matching logic runs. The ancestry resolution uses `/proc` on Linux, `kinfo_proc` syscalls on macOS, and `NtQueryInformationProcess` on Windows.

### Why does macOS use ps instead of native APIs?

Apple's Darwin kernel lacks a stable, documented procfs equivalent. While `libproc` exists, it is less stable across macOS versions than the standard `ps` output format. Spawning `ps` provides reliable PID, command name, and argument access without private API dependencies.

### Can witr resolve services that are not currently running?

No. All three service resolution paths—`systemctl show`, `launchctl print`, and `QueryServiceStatusEx`—return the MainPID only when a service is active. For inactive services, `witr` reports no match and proceeds with process name matching if applicable.

### What happens if multiple processes match the target name?

`witr` returns all matching PIDs. The caller (CLI or programmatic API) decides how to handle multiple results—typically by displaying information for each process or allowing further filtering by additional criteria like port numbers.