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
44 changes: 44 additions & 0 deletions hindsight-embed/hindsight_embed/daemon_embed_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,43 @@ def _parse_float_env(name: str, default: float) -> float:
return default


def _find_linux_pid_via_proc(port: int, proc_root: Path = Path("/proc")) -> int | None:
"""Resolve a listening TCP socket to its owner without external tools."""
socket_inodes: set[str] = set()
for table in ("tcp", "tcp6"):
try:
lines = (proc_root / "net" / table).read_text().splitlines()[1:]
except OSError:
continue

for line in lines:
fields = line.split()
if len(fields) > 9 and fields[3] == "0A":
try:
local_port = int(fields[1].rsplit(":", 1)[1], 16)
except (IndexError, ValueError):
continue
if local_port == port:
socket_inodes.add(fields[9])

if not socket_inodes:
return None

for process_dir in proc_root.iterdir():
if not process_dir.name.isdigit():
continue
try:
descriptors = (process_dir / "fd").iterdir()
for descriptor in descriptors:
target = descriptor.readlink()
match = re.fullmatch(r"socket:\[(\d+)]", str(target))
if match and match.group(1) in socket_inodes:
return int(process_dir.name)
except OSError:
continue
return None


def _safe_non_negative_float(value: float, fallback: float) -> float:
"""Return a finite non-negative float, or fallback for invalid values."""
return value if math.isfinite(value) and value >= 0 else fallback
Expand Down Expand Up @@ -452,6 +489,13 @@ def _posix_listening_pids(port: int) -> list[int]:
if "users:" not in line:
continue
pids.extend(int(match) for match in re.findall(r"pid=(\d+)", line))
if pids:
return pids

if platform.system() == "Linux":
pid = _find_linux_pid_via_proc(port)
if pid is not None:
return [pid]
return pids

@staticmethod
Expand Down
32 changes: 31 additions & 1 deletion hindsight-embed/tests/test_embed_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from unittest.mock import MagicMock, patch

from hindsight_embed import get_embed_manager
from hindsight_embed.daemon_embed_manager import DaemonEmbedManager
from hindsight_embed.daemon_embed_manager import DaemonEmbedManager, _find_linux_pid_via_proc


def _mock_sentence_transformers_present(monkeypatch):
Expand Down Expand Up @@ -384,6 +384,36 @@ def fake_run(*args, **kwargs):
assert calls[0][1]["creationflags"] == 0x08000000


def test_find_linux_pid_via_proc_matches_listening_socket_inode(tmp_path):
proc_root = tmp_path / "proc"
(proc_root / "net").mkdir(parents=True)
(proc_root / "net" / "tcp").write_text(
"header\n"
" 0: 0100007F:23D9 00000000:0000 0A 00000000:00000000 00:00000000 "
"00000000 1000 0 424242 1 0000000000000000 100 0 0 10 0\n"
)
(proc_root / "net" / "tcp6").write_text("header\n")
fd_dir = proc_root / "15774" / "fd"
fd_dir.mkdir(parents=True)
(fd_dir / "19").symlink_to("socket:[424242]")

assert _find_linux_pid_via_proc(9177, proc_root) == 15774
assert _find_linux_pid_via_proc(9178, proc_root) is None


def test_listening_pids_linux_falls_back_to_proc(monkeypatch):
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.platform.system", lambda: "Linux")
monkeypatch.setattr(
"hindsight_embed.daemon_embed_manager.subprocess.run",
MagicMock(side_effect=FileNotFoundError),
)
proc_lookup = MagicMock(return_value=15774)
monkeypatch.setattr("hindsight_embed.daemon_embed_manager._find_linux_pid_via_proc", proc_lookup)

assert DaemonEmbedManager._listening_pids(9177) == [15774]
proc_lookup.assert_called_once_with(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