Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions hindsight-embed/hindsight_embed/daemon_embed_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,8 @@ def _is_port_in_use(port: int) -> bool:
@staticmethod
def _find_pid_on_port(port: int) -> int | None:
"""Find the PID of the process listening on a port."""
import platform

try:
if platform.system() == "Windows":
if platform.system() == "Windows":
try:
# Use netstat on Windows
create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0)
result = subprocess.run(
Expand All @@ -344,8 +342,11 @@ def _find_pid_on_port(port: int) -> int | None:
for line in result.stdout.splitlines():
if f"127.0.0.1:{port}" in line and "LISTENING" in line:
return int(line.strip().split()[-1])
else:
# Use lsof on macOS/Linux
except (subprocess.TimeoutExpired, ValueError, OSError):
return None
else:
try:
# lsof is available by default on macOS and common on Linux.
result = subprocess.run(
["lsof", "-ti", f":{port}", "-sTCP:LISTEN"],
capture_output=True,
Expand All @@ -354,8 +355,24 @@ def _find_pid_on_port(port: int) -> int | None:
)
if result.returncode == 0 and result.stdout.strip():
return int(result.stdout.strip().split()[0])
except (subprocess.TimeoutExpired, ValueError, OSError, FileNotFoundError):
pass
except (subprocess.TimeoutExpired, ValueError, OSError):
pass

if platform.system() == "Linux":
try:
# Minimal Linux installations may provide ss but not lsof.
result = subprocess.run(
["ss", "-H", "-ltnp", f"sport = :{port}"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
match = re.search(r"\bpid=(\d+)\b", result.stdout)
if match:
return int(match.group(1))
except (subprocess.TimeoutExpired, ValueError, OSError):
pass
return None

@staticmethod
Expand Down
23 changes: 23 additions & 0 deletions hindsight-embed/tests/test_embed_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,29 @@ def fake_run(*args, **kwargs):
assert calls[0][1]["creationflags"] == 0x08000000


def test_find_pid_on_port_linux_falls_back_to_ss(monkeypatch):
calls = []

def fake_run(command, **kwargs):
calls.append(command)
if command[0] == "lsof":
raise FileNotFoundError

result = MagicMock()
result.returncode = 0
result.stdout = 'LISTEN 0 4096 127.0.0.1:9177 0.0.0.0:* users:(("hindsight-api",pid=15774,fd=19))\n'
return result

monkeypatch.setattr("hindsight_embed.daemon_embed_manager.platform.system", lambda: "Linux")
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.subprocess.run", fake_run)

assert DaemonEmbedManager._find_pid_on_port(9177) == 15774
assert calls == [
["lsof", "-ti", ":9177", "-sTCP:LISTEN"],
["ss", "-H", "-ltnp", "sport = :9177"],
]


def test_stop_ui_kills_recorded_and_configured_ports(tmp_path, monkeypatch):
"""After a UI-port change, stop_ui must kill BOTH the recorded (old, actually
running) port and the configured (new) port — otherwise the old UI orphans."""
Expand Down