Skip to content

Commit ed21c6a

Browse files
drknowhowclaude
andcommitted
Auto force-kill stale server holding port 18925 on startup
_try_shutdown_existing() now has two stages: 1. Graceful: send {"command":"SHUTDOWN"} via WebSocket — works with updated servers 2. Force: if port is still held after graceful attempt (e.g. old code without SHUTDOWN handler), find the owning PID via netstat (Windows) or lsof (Unix) and kill it directly This means Claude Code sessions are fully self-healing — no manual process killing needed even if a stale --ws-only server is running old code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7b06399 commit ed21c6a

1 file changed

Lines changed: 54 additions & 5 deletions

File tree

mcp-server/sentinel_mcp.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,66 @@ async def _ws_handler(websocket: WebSocketServerProtocol) -> None:
4242
print("[sentinel-mcp] Extension disconnected")
4343

4444

45+
def _port_in_use(port: int) -> bool:
46+
"""Return True if something is listening on 127.0.0.1:<port>."""
47+
import socket
48+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
49+
s.settimeout(0.3)
50+
return s.connect_ex(("127.0.0.1", port)) == 0
51+
52+
53+
def _force_kill_port(port: int) -> None:
54+
"""Kill whichever process is listening on <port>."""
55+
import platform, subprocess
56+
try:
57+
if platform.system() == "Windows":
58+
out = subprocess.run(["netstat", "-ano"], capture_output=True, text=True).stdout
59+
for line in out.splitlines():
60+
if f"127.0.0.1:{port}" in line and "LISTENING" in line:
61+
pid = line.split()[-1]
62+
subprocess.run(
63+
["powershell", "-Command", f"Stop-Process -Id {pid} -Force"],
64+
capture_output=True,
65+
)
66+
print(f"[sentinel-mcp] Force-killed PID {pid} holding port {port}")
67+
break
68+
else:
69+
# macOS / Linux
70+
out = subprocess.run(
71+
["lsof", "-ti", f"tcp:{port}"], capture_output=True, text=True
72+
).stdout.strip()
73+
for pid in out.splitlines():
74+
subprocess.run(["kill", "-9", pid], capture_output=True)
75+
print(f"[sentinel-mcp] Force-killed PID {pid} holding port {port}")
76+
except Exception as e:
77+
print(f"[sentinel-mcp] Force-kill failed: {e}")
78+
79+
4580
async def _try_shutdown_existing() -> None:
46-
"""If a --ws-only server is holding port 18925, tell it to exit so we can take over.
47-
The extension will reconnect to our new server within a few seconds."""
81+
"""Release port 18925 so this instance can take over.
82+
83+
1. Try a graceful SHUTDOWN message (works if the other server is up to date).
84+
2. If the port is still held afterwards, force-kill the owning process.
85+
The extension reconnects automatically within a few seconds.
86+
"""
87+
if not _port_in_use(18925):
88+
return # nothing to evict
89+
90+
# Graceful attempt
4891
try:
4992
ws = await asyncio.wait_for(websockets.connect("ws://127.0.0.1:18925"), timeout=1.5)
5093
await ws.send(json.dumps({"command": "SHUTDOWN"}))
5194
await ws.close()
52-
await asyncio.sleep(1.0) # wait for the port to be released
53-
print("[sentinel-mcp] Signalled existing server to stop")
95+
await asyncio.sleep(1.0)
96+
print("[sentinel-mcp] Sent SHUTDOWN to existing server")
5497
except Exception:
55-
pass # nothing running — fine
98+
pass
99+
100+
# If still occupied, force-kill
101+
if _port_in_use(18925):
102+
print("[sentinel-mcp] Port still held — force-killing")
103+
_force_kill_port(18925)
104+
await asyncio.sleep(0.5)
56105

57106

58107
async def _start_ws_server() -> None:

0 commit comments

Comments
 (0)