Winix.WhoHolds 0.4.0

Prefix Reserved
dotnet tool install --global Winix.WhoHolds --version 0.4.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Winix.WhoHolds --version 0.4.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Winix.WhoHolds&version=0.4.0
                    
nuke :add-package Winix.WhoHolds --version 0.4.0
                    

whoholds

Find which processes are holding a file lock or binding a network port.

lsof / handle replacement for diagnosing "file is locked" or "port is already in use" errors. Shows process name, PID, and owner for every holder. Works without admin rights for files; elevation improves results for ports.

Install

Scoop (Windows)

scoop bucket add winix https://github.com/Yortw/winix
scoop install winix/whoholds

Winget (Windows, stable releases)

winget install Winix.WhoHolds

.NET Tool (cross-platform)

dotnet tool install -g Winix.WhoHolds

Direct Download

Download native binaries from GitHub Releases.

Usage

whoholds [options] <target>

<target> is either a file path or a port specifier. whoholds auto-detects which you mean:

  • File path — any argument containing a path separator, or an argument that names an existing file on disk.
  • :port — leading : is unambiguous: whoholds :8080 always queries port 8080.
  • Bare number — whoholds 8080 queries port 8080 only if 8080 is not a file that exists on disk.

Examples

# Find what holds a DLL that can't be replaced
whoholds myapp.dll

# Find what is bound to port 8080 (TCP + UDP, IPv4 + IPv6)
whoholds :8080

# Bare number — treated as port if no file named "8080" exists
whoholds 8080

# Emit only PIDs (one per line) — suitable for piping
whoholds myapp.dll --pid-only

# Kill everything holding the file (pipe PIDs to wargs)
whoholds myapp.dll --pid-only | wargs taskkill /PID {} /F

# Machine-readable output for scripting
whoholds :8080 --json

# Show structured metadata about the tool itself
whoholds --describe

Options

Option Description
--pid-only Output only PIDs (one per line). Suitable for piping to wargs or kill.
--full-path, -l Show the full executable path instead of just the process name. Requires elevation for system processes.
--json Output results as a JSON object on stdout (suite convention — pipe-friendly for jq). Envelope fields: tool, version, exit_code, exit_reason, plus processes array on success or error string on failure. Each process entry includes pid, name, path, state, and resource.
--describe Output structured tool metadata as JSON (flags, examples, composability).
--color[=auto\|always\|never] Coloured output: auto (default when omitted), always, or never.
--no-color Disable coloured output.
--help Show help and exit.
--version Show version and exit.

Exit Codes

Code Meaning
0 Success — query completed (even if no holders found).
1 Target not found or query error.
125 Usage error (bad arguments).

Elevation Warning

whoholds always prints a warning to stderr when it is not running elevated (administrator on Windows, root on Linux/macOS). This is deliberate: without elevation, the Restart Manager API (Windows) and lsof (Linux/macOS) may only see processes belonging to the current user. A file held by a system service or another user's process will not appear in the results.

The warning reads:

Warning: Not elevated — only showing current user's processes.

This prevents the frustrating "it says nothing is holding the file, but I still can't delete it" scenario. If you see no holders but the problem persists, re-run elevated.

What changes when elevated

Feature Normal Elevated
File lock detection Current user's processes only All processes
Port binding detection Current user's processes only All processes
--full-path / -l May show empty paths for system processes (access denied) Full executable paths for all processes
TCP state column Always shown for port queries Always shown for port queries

For most developer workflows (your IDE, build tools, dev servers), normal mode is sufficient. Elevation is needed when a system service or another user's process holds the lock.

Platform Notes

Platform Implementation
Windows File locks: Win32 Restart Manager API (no admin needed for current-user processes). Ports: IP Helper GetExtendedTcpTable / GetExtendedUdpTable (covers TCP + UDP, IPv4 + IPv6).
Linux / macOS Delegates to lsof, which must be installed. On most distributions and macOS, lsof is present by default.

Colour

  • Process names are highlighted for quick scanning.
  • Elevation warning is shown in yellow (stderr).
  • --no-color suppresses all ANSI colour output.
  • Respects the NO_COLOR environment variable (no-color.org).

Part of Winix

whoholds is part of the Winix CLI toolkit.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.4.0 142 6/13/2026
0.3.0 127 5/26/2026
0.3.0-rc2 125 5/10/2026
0.2.0 120 4/16/2026
0.2.0-test4 117 4/15/2026

## [0.3.0] - 2026-05-10

### Changed
- `--json` output now goes to **stdout** (was stderr) per suite convention. Pipe-friendly for `jq`; matches `man`, `winix`, `treex`, `files`.

### Fixed
- `lsof` rows whose command name contains spaces (multi-word names like `Google Chrome`) are no longer silently dropped. Pre-fix the fixed-column-index parser truncated at the first whitespace boundary, hiding a class of legitimate locks from the result list.
- Backend API failures (e.g. `lsof` exit non-zero, RM probe error) now route to **exit 1** with a stderr diagnostic. Pre-fix backend failures resulted in a silent empty result list with exit 0, indistinguishable from "no locks found".
- File-not-found path returns **exit 1** (was 125) per the documented contract — file-not-found is a target-state error, not a usage error.
- Stuck child stream reads (`lsof` hanging on a stale fd, RM probe wedged on a hung process) now surface a categorised stderr diagnostic with a timeout warning rather than silent hang.
- Windows `RM_UNIQUE_PROCESS` struct layout corrected — process names ≥ 64 chars no longer truncated mid-string.
- Restart Manager probe now retries on the eventual-consistency edge where a freshly-opened lock isn't yet visible to RM.
- `--version` output no longer carries the `+gitsha` SourceLink suffix.

### Added
- Library seam `Winix.WhoHolds.Cli.Run` for orchestration testing without process spawning.

### Documentation
- README and man page corrected: JSON example shape and field list now match the actual emitter.
- Elevation-warning text unified across surfaces.
- Wargs pipe example unified to `taskkill /PID {} /F` form for Windows users.

### Internal
- UTF-8 console adoption via `ConsoleEnv.UseUtf8Streams`.
- Standard `<PackageTags>` set on the NuGet package.
- ProcessRunner test seam added so backend integration tests can pin shell-output parsing without spawning real `lsof` / RM calls.
- Platform-skipped tests migrated to `SkippableFact + Skip.IfNot`.

See full changelog at https://github.com/Yortw/winix/blob/main/src/whoholds/CHANGELOG.md