Engineering

Built for Windows without a Windows box.

By Albert 7 min read

le 0.1.25 runs on Windows: le.exe for amd64 and arm64, as a zip on every release. Nobody on this project owns a Windows machine. The port was written and verified on a Mac, and the only place it ever executed for real was a GitHub Actions runner. That constraint decided the shape of the code more than Windows did, and the shape is the part worth writing down.

Tag the spawning, not the parsing

The TUI was already cross-platform — Bubble Tea doesn't care. What's platform-bound is the layer under it: who is listening (lsof on macOS and Linux, netstat -ano on Windows), who is that process (ps, or one Win32_Process query), and how do you stop it (SIGTERM, or taskkill). The obvious layout is one //go:build windows file per package holding all of it.

That layout would have been undevelopable here, because tagged code doesn't compile on the machine you're sitting at, and untestable, because it can't run there either. So the split is drawn somewhere else. Every parser is untagged and pure: scan/netstat.go turns netstat output and a JSON dump of Win32_Process rows into exactly the struct the ps parsers produce; kill/taskkill.go recognises taskkill's refusal and builds the re-read scripts; intel/windows.go classifies Windows system processes and names installed software from its install root. All of it compiles on macOS, and all of it is tested on macOS against captured output. Only the code that actually spawns netstat, PowerShell, taskkill and OpenProcess carries the build tag.

The payoff is a one-line check that runs anywhere: GOOS=windows go vet ./... type-checks every tagged file and every test file for Windows, in seconds, on a Mac. A unix-only symbol leaking into an untagged test — which happened, twice — fails there instead of after a fifteen-minute Windows runner queue. CI got a cross job that does nothing but this, on ubuntu, and it has been the fastest red light in the pipeline ever since.

The State column lies in German

The first version of the netstat parser looked for the word LISTENING. That word is localised. On a German Windows the column says ABHÖREN, on a French one ÉCOUTE, and a parser keyed on the English string returns an empty table for every one of those users — no error, nothing to grep, just a tool that reports nothing is listening on a machine with forty listeners.

A listening TCP socket has a structural tell that no locale touches: its foreign address is the zero endpoint, 0.0.0.0:0 or [::]:0. That is what the parser keys on now, with the English word accepted as a second signal. The test fixture has a row with no LISTENING token at all, and it has to be found.

The one property that had to survive

le's promise is that a stop goes to the process you looked at. PIDs get recycled; the guard against that is the start time, captured at scan and re-read from the same source immediately before signalling — refuse if it moved. On unix that source is ps lstart, and the unix journal has an entry about the day single-digit dates got a second space of padding and every stop on the 1st through the 9th was refused as a recycled PID.

On Windows the source is Win32_Process.CreationDate, and the capture and the re-read both render it with ToString('o') — ISO 8601 at full precision, from the same expression in the same interpreter. A live process compares byte-for-byte equal to itself. A recycled one can't. There is no padding hazard to normalise away, because there was never a second spelling.

Three tests that run the real thing

Parsers tested against captured output prove the parsing. They prove nothing about whether the real netstat on a real Windows box emits what was captured, or whether PowerShell survives the argv quoting Go applies on Windows. That needs execution, and execution needs the runner.

So three untagged tests run the whole chain end to end, on whatever platform they happen to be on: open a listener, scan, and find the test process on the port it holds with a non-empty start time; feed that row to the recycle guard and confirm it accepts its own PID, refuses a mismatched start time, and holds on the command-line fallback; probe our own PID and a nonexistent one. On the Mac they run against lsof and ps, which is how the unix side of the split was verified not to have moved. On windows-latest they are the only thing that has ever exercised the Windows glue for real.

On the first run that reached them, all three passed. Every failure that run produced was in the TUI package's tests — fixtures that use /bin/sh as a stat-able binary and /Users/me as a home — and one of those was pointing at a real gap: the detail pane resolved binaries only for paths starting with /. It reads quoted argv[0]s and drive letters now, and the reveal / open / terminal launchers got Windows branches. The fixtures skip where /bin/sh is absent, named for what they need rather than for an OS.

What three red runs taught

Run one never reached the tests. gofmt -l . listed the entire tree, because actions/checkout on the Windows runner honours core.autocrlf and every file arrived with CRLF endings. Formatting is a property of the source, not the OS; it's checked on the unix runners now.

Run two failed on ubuntu, and the Windows result was cancelled before it existed. A three-OS matrix with fail-fast on produces exactly one platform's answer per run, which is the opposite of the information it exists to produce. Off now.

The ubuntu failure itself was a test earning its keep: system_test.go insists every housekeeping executable is an absolute path, so a same-named binary earlier on a user's PATH can never run with their privileges. The new Linux seam had resolvectl and xdg-open as bare names. The Windows file had the same gap and no test to catch it; both resolve through /usr/bin and %SystemRoot%\System32 now.

Found in passing

The browser launcher was /usr/bin/open, hard-coded. That is macOS's launcher. It does not exist on Linux, so le open 3000 on Linux waited for the port and then failed, every time, since the day the command shipped. Nobody noticed because nobody ran it on Linux; "works on macOS and Linux" was true of the scanner and assumed for everything downstream of it.

The journal entry generalises it: a second platform is a claim about every absolute path in the tree, not just the scanner. grep -rn '/usr/bin' before you claim one. Each hit is tagged, seamed, or a bug.

Two honest blanks

The DIR column is empty on Windows. Another process's working directory lives in its PEB behind privileged reads, so le stop --dir matches nothing there rather than guessing. And the graceful stop is taskkill /PID, which posts a close message to a window — a console server with no window can't honour it. When that happens, le refuses and prints the taskkill /F command instead of escalating on its own, because "TERM, never kill -9 by default" is the promise and Windows doesn't get an exception to it. Both are written down in COMPATIBILITY.md.

le.exe, on the Releases page.

Unzip, put it on your PATH, run le. Free and MIT. Mac and Linux: brew install alikatgh/tap/le