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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #433 fix(whatsapp): replace Linux-only fuser with cross-platform port cleanup

    merged to main

    files: gateway/platforms/whatsapp.py, tests/gateway/test_whatsapp_connect.py

    Show 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.00 means 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 Lab

At 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.

For your agent

The same facts, typed.