NousResearch/hermes-agent
Cross-platform port cleanup for WhatsApp bridge
Cluster 432 · gateway/platforms. Data as of
Nothing open on GitHub. Fix merged in #433 on 6 Mar 2026. Check it covers your case.
The short answer
0open issues
0open PRs
3items grouped
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #433. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #433. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items address the WhatsApp bridge's use of the Linux-only fuser command for orphaned process cleanup, with proposed fixes to replace it with a cross-platform alternative.
Open issues0
Open PRs0
Fixes on record
-
#433 fix(whatsapp): replace Linux-only fuser with cross-platform port cleanup
files:
gateway/platforms/whatsapp.py,tests/gateway/test_whatsapp_connect.pyShow the diff
-
gateway/platforms/whatsapp.py@@ -28,6 +28,41 @@ logger = logging.getLogger(__name__) + +def _kill_port_process(port: int) -> None: + """Kill any process listening on the given TCP port.""" + try: + if _IS_WINDOWS: + # Use netstat to find the PID bound to this port, then taskkill + result = subprocess.run( + ["netstat", "-ano", "-p", "TCP"], + capture_output=True, text=True, timeout=5, + ) + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) >= 5 and parts[3] == "LISTENING": + local_addr = parts[1] + if local_addr.endswith(f":{port}"): + try: + subprocess.run( + ["taskkill", "/PID", parts[4], "/F"], + capture_output=True, timeout=5, + ) + except subprocess.SubprocessError: + pass + else: + result = subprocess.run( + ["fuser", f"{port}/tcp"], + capture_output=True, timeout=5, + ) + if result.returncode == 0: + subprocess.run( + ["fuser", "-k", f"{port}/tcp"], + capture_output=True, timeout=5, + ) + except Exception: + pass + import sys sys.path.insert(0, str(Path(__file__).resolve().parents[2])) @@ -145,21 +180,9 @@ async def connect(self) -> bool: self._session_path.mkdir(parents=True, exist_ok=True) # Kill any orphaned bridge from a previous gateway run - try: - result = subprocess.run( - ["fuser", f"{self._bridge_port}/tcp"], - capture_output=True, timeout=5, - ) - if result.returncode == 0: - # Port is in use — kill the process - subprocess.run( - ["fuser", "-k", f"{self._bridge_port}/tcp"], - capture_output=True, timeout=5, - ) - import time - time.sleep(2) - except Exception: - pass + _kill_port_process(self._bridge_port) + import time + time.sleep(1) # Start the bridge process in its own process group. # Route output to a log file so QR codes, errors, and reconnection @@ -293,13 +316,7 @@ async def disconnect(self) -> None: print(f"[{self.name}] Error stopping bridge: {e}") # Also kill any orphaned bridge processes on our port - try: - subprocess.run( - ["fuser", "-k", f"{self._bridge_port}/tcp"], - capture_output=True, timeout=5, - ) - except Exception: - pass + _kill_port_process(self._bridge_port) self._running = False self._bridge_process = None -
tests/gateway/test_whatsapp_connect.py@@ -268,3 +268,94 @@ async def test_closed_on_unexpected_exception(self): assert result is False mock_fh.close.assert_called_once() assert adapter._bridge_log_fh is None + + +# --------------------------------------------------------------------------- +# _kill_port_process() cross-platform tests +# --------------------------------------------------------------------------- + +class TestKillPortProcess: + """Verify _kill_port_process uses platform-appropriate commands.""" + + def test_uses_netstat_and_taskkill_on_windows(self): + from gateway.platforms.whatsapp import _kill_port_process + + netstat_output = ( + " Proto Local Address Foreign Address State PID\n" + " TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 12345\n" + " TCP 0.0.0.0:3001 0.0.0.0:0 LISTENING 99999\n" + ) + mock_netstat = MagicMock(stdout=netstat_output) + mock_taskkill = MagicMock() + + def run_side_effect(cmd, **kwargs): + if cmd[0] == "netstat": + return mock_netstat + if cmd[0] == "taskkill": + return mock_taskkill + return MagicMock() + + with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \ + patch("gateway.platforms.whatsapp.subprocess.run", side_effect=run_side_effect) as mock_run: + _kill_port_process(3000) + + # netstat called + assert any( + call.args[0][0] == "netstat" for call in mock_run.call_args_list + ) + # taskkill called with correct PID + assert any( + call.args[0] == ["taskkill", "/PID", "12345", "/F"] + for call in mock_run.call_args_list + ) + + def test_does_not_kill_wrong_port_on_windows(self): + from gateway.platforms.whatsapp import _kill_port_process + + netstat_output = ( + " TCP 0.0.0.0:30000 0.0.0.0:0 LISTENING 55555\n" + ) + mock_netstat = MagicMock(stdout=netstat_output) + + with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \ + patch("gateway.platforms.whatsapp.subprocess.run", return_value=mock_netstat) as mock_run: + _kill_port_process(3000) + + # Should NOT call taskkill because port 30000 != 3000 + assert not any( + call.args[0][0] == "taskkill" + for call in mock_run.call_args_list + ) + + def test_uses_fuser_on_linux(self): + from gateway.platforms.whatsapp import _kill_port_process + + mock_check = MagicMock(returncode=0) + + with patch("gateway.platforms.whatsapp._IS_WINDOWS", False), \ + patch("gateway.platforms.whatsapp.subprocess.run", return_value=mock_check) as mock_run: + _kill_port_process(3000) + + calls = [c.args[0] for c in mock_run.call_args_list] + assert ["fuser", "3000/tcp"] in calls + assert ["fuser", "-k", "3000/tcp"] in calls + + def test_skips_fuser_kill_when_port_free(self): + from gateway.platforms.whatsapp import _kill_port_process + + mock_check = MagicMock(returncode=1) # port not in use + + with patch("gateway.platforms.whatsapp._IS_WINDOWS", False), \ + patch("gateway.platforms.whatsapp.subprocess.run", return_value=mock_check) as mock_run: + _kill_port_process(3000) + + calls = [c.args[0] for c in mock_run.call_args_list] + assert ["fuser", "3000/tcp"] in calls + assert ["fuser", "-k", "3000/tcp"] not in calls + + def test_suppresses_exceptions(self): + from gateway.platforms.whatsapp import _kill_port_process + + with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \ + patch("gateway.platforms.whatsapp.subprocess.run", side_effect=OSError("no netstat")): + _kill_port_process(3000) # must not raise
-
1 PR closed without merging
From the close comments: superseded by another PR 1
- #472 fix: cross-platform orphaned process cleanup for WhatsApp bridge · superseded by another PR
Closed issues · 1
- Closed#432WhatsApp bridge uses Linux-only `fuser` command for orphaned process cleanup
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- gateway/platforms
- Merged PRs
- #433
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 6 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.