Three issues raised by ultrareview on top of the previous review pass:
1. (regression) findPidByLocalTcpPort accepted any TCP socket whose
insi_lport matched, including LISTEN sockets. The original
`lsof | grep "${port}->"` excluded LISTEN entries implicitly via the
"->" filter (LISTEN renders as `*:port (LISTEN)`, no `->`). When a
long-running daemon LISTENs on a port that coincides with a client's
ephemeral source port, proc_listpids hits the lower-PID daemon first
and misattributes the request; with the new 30s pid cache the wrong
PID stuck for 30s of follow-up requests. Now skip sockets with
insi_fport == 0 (the LISTEN signature) to mirror the old grep filter
exactly.
2. (pre-existing) getProcess in the macOS path unconditionally appended
".app" to the executable path, producing non-existent paths like
"/usr/bin/curl.app" for non-bundle binaries. This poisoned the icon
cache with empty bytes for 5 minutes. Now only append when the path
actually contains ".app/"; standalone binaries use the executable
path verbatim.
3. (nit) pidBuf/sockBuf were allocated before the outer try, leaking on
the (extremely rare) case that the second calloc throws. Moved both
allocations inside the try with null-aware free in finally, matching
the pattern the inner fdBuf loop already uses.
Verified:
- LISTEN-only port -> FFI returns null (matches lsof+grep behavior)
- ESTABLISHED outbound -> FFI returns the correct client PID
- 500 concurrent curl through proxy -> single ProxyPin process, no
fork leaks (original #763 fix preserved)
- Guard against `getProcess` returning null in the macOS FFI path
(`proc_pidpath` can fail with EPERM or for an already-exited PID).
Stop unwrapping with `!` and only populate the ProcessInfoCache on
success; the previous code threw NullThrownError there, which the
outer try/catch silently swallowed and prevented caching of valid
later results.
- Add a 5-second negative cache for ports whose owner can't be
resolved (typical for short-lived clients that exited before we
scanned). Without it every such request triggered a full PID list
rescan, defeating the point of the pid cache for the failure path.
- Add a debug-only assert that the host is little-endian. The libproc
parsing reads `(int)htons(port)` by treating the low two bytes as a
big-endian uint16, which requires a little-endian host. All shipping
macOS hardware satisfies this; the assert fails loudly if the
assumption is ever broken (stripped from release builds).
Re-verified on macOS 26.4: 500 concurrent curl through the proxy,
single ProxyPin process throughout, 9099 freed immediately on exit.
- Add 30s (host:port) -> pid cache in ProcessInfoUtils so the synchronous
libproc scan stays off the request hot path for keep-alive flows
(typical HTTP client reuses the same source port across requests in
a connection).
- Lift sockView ByteData out of the inner fd loop so the buffer wrapper
is reused across all fds in a scan (saves per-fd allocations).
- Bail out of the scan after 16 consecutive proc_pidfdinfo size
mismatches, in case a future macOS bumps the socket_fdinfo layout --
avoids wasted syscalls instead of scanning every fd just to return
null at the end.
Re-verified end-to-end on macOS 26.4: 500 concurrent curl, 0 fork
leaks, 9099 freed immediately on app exit, no errors in logs.
Replace Process.run('lsof') and Process.run('ps') in the request hot
path with direct libproc syscalls (proc_listpids, proc_pidinfo,
proc_pidfdinfo, proc_pidpath) via dart:ffi.
Each Process.run on macOS goes through fork()+execvp(). In a
multi-threaded Dart VM the forked child can deadlock before exec on
mutexes that were held by other threads at fork time (POSIX fork()
only clones the calling thread, leaving cloned mutex state with no
owners in the child). Under proxy load (~500 concurrent requests),
roughly 2% of the fork()s deadlock; the orphaned children inherit the
listening proxy fd and pin it for the lifetime of the system, so the
port stays bound even after the parent app exits.
libproc syscalls do not spawn child processes, so they avoid the fork
problem entirely. They are also about an order of magnitude faster
than spawning lsof per request.
Windows and Linux paths are unchanged (independent follow-up).
Verified end-to-end on macOS 26.4 / Darwin 25.4:
- 500 concurrent curl through the proxy: 0 fork leaks (was ~2% before)
- Process icons still resolve correctly for .app bundles (Chrome,
Safari, Lark, etc.)
- 9099 frees immediately on app exit (no orphan listeners)
Fixes#763