# NousResearch/hermes-agent · Cross-platform port cleanup for WhatsApp bridge

## Open now

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

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #433. Verify it addresses your case.
- **Recommended:** read PR #433 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-433`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #432: WhatsApp bridge uses Linux-only \`fuser\` command for orphaned process cleanup

## Prior & competing PRs

- **#433: fix(whatsapp): replace Linux-only fuser with cross-platform port cleanup**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/platforms/whatsapp.py`, `tests/gateway/test_whatsapp_connect.py`
  - fix hunks:
  - `gateway/platforms/whatsapp.py`

    ```diff
    @@ -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]))
     
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```
- **#472: fix: cross-platform orphaned process cleanup for WhatsApp bridge**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._

## Fix status

- Merged to main in PR #433 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-06`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `gateway/platforms`
