mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(whatsapp): kill bridge process tree on Windows disconnect
This commit is contained in:
parent
735996d2ad
commit
3821921ef7
2 changed files with 60 additions and 10 deletions
|
|
@ -66,6 +66,37 @@ def _kill_port_process(port: int) -> None:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _terminate_bridge_process(proc, *, force: bool = False) -> None:
|
||||
"""Terminate the bridge process using process-tree semantics where possible."""
|
||||
if _IS_WINDOWS:
|
||||
cmd = ["taskkill", "/PID", str(proc.pid), "/T"]
|
||||
if force:
|
||||
cmd.append("/F")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
if force:
|
||||
proc.kill()
|
||||
else:
|
||||
proc.terminate()
|
||||
return
|
||||
|
||||
if result.returncode != 0:
|
||||
details = (result.stderr or result.stdout or "").strip()
|
||||
raise OSError(details or f"taskkill failed for PID {proc.pid}")
|
||||
return
|
||||
|
||||
import signal
|
||||
|
||||
sig = signal.SIGTERM if not force else signal.SIGKILL
|
||||
os.killpg(os.getpgid(proc.pid), sig)
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
|
|
@ -537,22 +568,14 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
|||
"""Stop the WhatsApp bridge and clean up any orphaned processes."""
|
||||
if self._bridge_process:
|
||||
try:
|
||||
# Kill the entire process group so child node processes die too
|
||||
import signal
|
||||
try:
|
||||
if _IS_WINDOWS:
|
||||
self._bridge_process.terminate()
|
||||
else:
|
||||
os.killpg(os.getpgid(self._bridge_process.pid), signal.SIGTERM)
|
||||
_terminate_bridge_process(self._bridge_process, force=False)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
self._bridge_process.terminate()
|
||||
await asyncio.sleep(1)
|
||||
if self._bridge_process.poll() is None:
|
||||
try:
|
||||
if _IS_WINDOWS:
|
||||
self._bridge_process.kill()
|
||||
else:
|
||||
os.killpg(os.getpgid(self._bridge_process.pid), signal.SIGKILL)
|
||||
_terminate_bridge_process(self._bridge_process, force=True)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
self._bridge_process.kill()
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -453,6 +453,33 @@ class TestKillPortProcess:
|
|||
class TestHttpSessionLifecycle:
|
||||
"""Verify persistent aiohttp.ClientSession is created and cleaned up."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_uses_taskkill_tree_on_windows(self):
|
||||
"""Windows disconnect should target the bridge process tree, not just the parent PID."""
|
||||
adapter = _make_adapter()
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.pid = 12345
|
||||
mock_proc.poll.side_effect = [0]
|
||||
adapter._bridge_process = mock_proc
|
||||
adapter._poll_task = None
|
||||
adapter._http_session = None
|
||||
adapter._running = True
|
||||
adapter._session_lock_identity = None
|
||||
|
||||
with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \
|
||||
patch("gateway.platforms.whatsapp.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run, \
|
||||
patch("gateway.platforms.whatsapp.asyncio.sleep", new_callable=AsyncMock):
|
||||
await adapter.disconnect()
|
||||
|
||||
mock_run.assert_called_once_with(
|
||||
["taskkill", "/PID", "12345", "/T"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
mock_proc.terminate.assert_not_called()
|
||||
mock_proc.kill.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_closed_on_disconnect(self):
|
||||
"""disconnect() should close self._http_session."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue