name: process-detection description: "How dtwiz discovers, identifies, and correlates running OS processes to project directories across Unix and Windows"
Running Process Detection
This skill covers how dtwiz discovers, identifies, and correlates running OS processes to project directories. Process detection is the foundation for auto-instrumentation — it answers "what's running and where does it live?".
We want to reliably map instrumented services to proper ports.
Architecture Overview
pkg/installer/
otel_runtime_scan.go # Shared types + project-process correlation
otel_runtime_scan_unix.go # Unix: ps, lsof
otel_runtime_scan_windows.go # Windows: Get-CimInstance Win32_Process, Get-NetTCPConnection
otel_process.go # ManagedProcess lifecycle (launch, port poll, exit tracking)
otel_process_windows.go # Windows child-adoption (leaf PID discovery, watchPID)
otel_process_other.go # Unix no-op stub for adoptExeclChildren
Core Data Model
type DetectedProcess struct {
PID int
Command string // Full command line string
WorkingDirectory string // Resolved cwd of the process
Description string // Optional enriched metadata
}
A process is identified by three axes: PID, command line, and working directory. Any correlation logic uses at least two of these.
Platform-Specific Process Enumeration
Unix (Linux / macOS)
- List processes:
ps ax -o pid=,command=— one line per process, space-separated PID + full command. - Resolve working directory:
lsof -a -d cwd -p <pid> -Fn— output lines prefixed withncontain the path. - Detect listening port:
lsof -a -i TCP -sTCP:LISTEN -p <pid> -Fn -P— parsenlines, extract port after last:. - Check loaded files:
lsof -p <pid> -Fn— verify files loaded by a process even if not on the command line.
Windows
- List processes:
Get-CimInstance Win32_Processvia PowerShell. Fields:ProcessId,CommandLine,WorkingDirectory. Queried withWhere-Objectfilter onCommandLine. - Resolve working directory: Included in the same CimInstance query (no separate call needed, unlike Unix).
- Detect listening port:
Get-NetTCPConnection -State Listen -OwningProcess <pid>with port exclusion filter. - Check loaded modules:
Get-Process -Id <pid> | ForEach-Object { $_.Modules }to inspect loaded DLLs/files.
Key Differences
| Concern | Unix | Windows |
|---|---|---|
| Process listing | Single ps call, text parsing |
PowerShell CimInstance query |
| Working directory | Separate lsof call per PID |
Included in CimInstance result |
| Port detection | lsof with TCP filter |
Get-NetTCPConnection |
| Performance | Fast (native commands) | Slower (PowerShell startup overhead per call) |
| Parent-child tree | Not directly available from ps |
ParentProcessId field in CimInstance |
| PID of self | os.Getpid() filtered out |
Same |
Process Filtering
detectProcesses(filterTerm, excludeTerms) is the single entry point on both platforms:
- Filter term: Case-insensitive substring match on the command line (e.g.,
"java","python","node","go"). - Exclude terms: Command lines containing any exclude term are dropped. Prevents matching utility/tooling processes (package managers, build tools, dtwiz itself).
- Self-exclusion: The current process PID (
os.Getpid()) is always excluded.
Each runtime defines its own filter term and exclude list. The pattern: include the runtime binary name, exclude known noise processes that aren't the user's application.
Project-Process Correlation
matchingProcessIDs(dirPath, processes) correlates detected processes to a project directory using two independent signals (case-insensitive):
- Working directory starts with the project path — the process was launched from within the project tree.
- Command line contains the project path — the project path appears as an argument.
Either condition matching is sufficient. Multiple processes can match a single project.
Parent-Child PID Resolution
When parent-child relationships between PIDs can't be determined directly (Unix ps output doesn't include PPID), the system falls back to command-line content matching:
- Match processes to projects by checking if the command line contains the project path.
- Use runtime-specific tooling to enumerate processes by metadata (e.g.,
jps -llists JVMs by main class). - On Windows,
ParentProcessIdis available fromGet-CimInstance Win32_Process, enabling direct tree walks.
Wrapper/Launcher Problem (General Pattern)
Many runtimes use wrappers that fork a child process to run the actual app. The wrapper PID is detected first, but the app process is a separate PID.
Unix approach: Use runtime-specific enumeration tools or command-line matching to find the real app process, then probe it for a listening port.
Windows approach: Walk the process tree from the wrapper PID using ParentProcessId, collect descendants matching the runtime binary, then check for listening ports.
os.execl Child Adoption (Windows-only)
Some launchers use os.execl() which on Windows spawns a new process and the launcher exits, breaking PID tracking. On Unix, execl replaces the process in-place (same PID) so this isn't an issue.
Windows solution: Query Win32_Process for processes matching the runtime binary name AND the original entrypoint in CommandLine. Filter to leaf processes (those whose PID doesn't appear as any other match's ParentProcessId). Adopt the leaf PID via windows.OpenProcess(SYNCHRONIZE) + WaitForSingleObject.
The adoptExeclChildren function is a no-op on Unix.
Port Detection
After a process is started or adopted, port detection polls until a listening TCP port appears:
- Excluded ports: 4317, 4318 (OTel Collector gRPC/HTTP) — never reported as the app's port.
- Poll interval: 500ms.
- Timeout: 15 seconds after the settle delay.
- Settle delay: 3 seconds before first poll (let the process bind its port).
Custom portDetector functions can be set per ManagedProcess to handle wrapper/descendant cases where the port is on a child PID rather than the tracked PID.
Process Lifecycle Management
stopProcesses(pids) handles graceful shutdown:
- Unix: Send
SIGINT(graceful) thenprocess.Wait(). - Windows:
killAndWaitProcess()callsKill()and polls until PID is gone (no reliable cross-process signal mechanism on Windows).
Key Principles
- Command line is the universal fallback. When process tree relationships aren't available, substring matching on the full command line correlates processes to projects.
- Platform divergence is isolated in build-tagged files. Shared logic calls platform-specific functions defined in build-constrained files (e.g.,
_unix.gowith//go:build !windows,_windows.gowith//go:build windows). - PowerShell is expensive. Every Windows detection call pays PowerShell startup overhead. Batch queries where possible (single
Get-CimInstancecall withWhere-Objectinstead of per-PID queries). - Exclude noise early. Filter terms and exclude terms prevent false matches from tooling processes (package managers, build daemons, dtwiz itself).
- Two-signal correlation. Matching uses both working directory and command-line content — either is sufficient, both together increase confidence.
- Self-exclusion is mandatory. Always filter
os.Getpid()to avoid instrumenting dtwiz itself. - Port 4317/4318 are always excluded. These are OTel Collector ports and must never be reported as an application's listening port.
- Enrichment is opportunistic. If runtime-specific tools aren't available, detection still works — just with less metadata in
Description. - Adoption handles launcher patterns. When a launcher exits and a child continues (Windows
os.execlpattern), explicit PID adoption re-establishes tracking. - Case-insensitive matching everywhere. All path and command comparisons use
strings.ToLower()to handle filesystem and command casing differences across platforms.