From d69b95dbbc33e60a5e8e36e2dbae233d65641a9a Mon Sep 17 00:00:00 2001 From: testercengdong Date: Tue, 12 May 2026 10:16:42 +0800 Subject: [PATCH] fix(macos,process_info): tighten error handling and add endian assert - 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. --- lib/network/util/process_info.dart | 16 ++++++++++++++-- lib/network/util/process_info_macos.dart | 7 +++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/network/util/process_info.dart b/lib/network/util/process_info.dart index 088d6fc..9082ada 100644 --- a/lib/network/util/process_info.dart +++ b/lib/network/util/process_info.dart @@ -45,6 +45,12 @@ class ProcessInfoUtils { // libproc scan runs on the main isolate. static final _pidCache = ExpiringCache(const Duration(seconds: 30)); + // Negative cache for ports whose owner can't be resolved (e.g. the client + // process has already exited by the time we scan). Without this, every + // short-lived connection forces a full PID-list rescan on every request. + // Short TTL so a real owner that appears soon after is not masked. + static final _pidNotFoundCache = ExpiringCache(const Duration(seconds: 5)); + static Future getProcessByPort(InetSocketAddress socketAddress, String cacheKeyPre) async { try { if (Platform.isAndroid) { @@ -59,10 +65,14 @@ class ProcessInfoUtils { } var addrKey = "${socketAddress.host}:${socketAddress.port}"; + if (_pidNotFoundCache.get(addrKey) == true) return null; var pid = _pidCache.get(addrKey); if (pid == null) { pid = await _getPid(socketAddress); - if (pid == null) return null; + if (pid == null) { + _pidNotFoundCache.set(addrKey, true); + return null; + } _pidCache.set(addrKey, pid); } @@ -71,7 +81,9 @@ class ProcessInfoUtils { if (processInfo != null) return processInfo; processInfo = await getProcess(pid); - processInfoCache.set(cacheKey, processInfo!); + if (processInfo != null) { + processInfoCache.set(cacheKey, processInfo); + } return processInfo; } catch (e) { logger.e("getProcessByPort error: $e"); diff --git a/lib/network/util/process_info_macos.dart b/lib/network/util/process_info_macos.dart index 7ee02f8..635e8f2 100644 --- a/lib/network/util/process_info_macos.dart +++ b/lib/network/util/process_info_macos.dart @@ -93,6 +93,13 @@ class MacosProcessInfo { /// are spawned. Typical cost on a desktop with ~500 processes and ~10k /// total fds is a few milliseconds. static int? findPidByLocalTcpPort(int localPort) { + // The insi_lport read below assumes a little-endian host: `(int)htons(port)` + // stores the network-byte-order 16-bit port in the low two bytes of the + // int32 field. All shipping macOS hardware (x86_64 / arm64) is + // little-endian; this assert exists to fail loudly rather than return + // wrong port values if that ever changes. Stripped in release builds. + assert(Endian.host == Endian.little, 'libproc parsing requires little-endian host'); + final pidBufSize = _procListPids(_kProcAllPids, 0, nullptr, 0); if (pidBufSize <= 0) return null;