From d092a177c097304e81784c2be93e33ce3492af2b Mon Sep 17 00:00:00 2001 From: MSCodeBase Agent Date: Tue, 29 Sep 2026 11:50:10 +0300 Subject: [PATCH] feat(process): distinguishable MCP and llama process names MCP sets Windows console title mscodebase-mcp:- at startup (win32-guarded, logged). Llama embed/rerank spawn via NTFS hardlinks llama-embed.exe/llama-rerank.exe (os.link, same inode, fallback to plain spawn). PID-reuse and port guards accept new names. --- src/core/process_titles.py | 115 +++++++++++++++++++++++++ src/main.py | 15 ++++ src/providers/reranker/llama_runner.py | 33 ++++++- tests/test_process_titles.py | 91 +++++++++++++++++++ 4 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 src/core/process_titles.py create mode 100644 tests/test_process_titles.py diff --git a/src/core/process_titles.py b/src/core/process_titles.py new file mode 100644 index 00000000..2e65a8ad --- /dev/null +++ b/src/core/process_titles.py @@ -0,0 +1,115 @@ +"""Process naming: MCP console title + llama role hardlinks (Windows). + +Minimal, Windows-guarded helpers so Task Manager shows +``mscodebase-mcp:-`` and ``llama-embed.exe`` / +``llama-rerank.exe`` instead of bare ``python.exe`` / +``llama-server.exe``. +""" + +from __future__ import annotations + +import logging +import os +import sys +from pathlib import Path + +logger = logging.getLogger("MSCodebase.process_titles") + +MCP_TITLE_PREFIX = "mscodebase-mcp" + +ROLE_BIN_NAMES = { + "embed": "llama-embed.exe", + "rerank": "llama-rerank.exe", +} + + +def build_mcp_title(project_path: str | Path | None = None) -> str: + """Build ``mscodebase-mcp:-`` title string. + + Reuses the canonical :func:`src.core.artifact_paths.project_hash` + helper — no invented hashing. + """ + name = "unknown" + hash8 = "unknown" + try: + if project_path: + p = Path(project_path) + name = p.name or "unknown" + from src.core.artifact_paths import project_hash + + hash8 = project_hash(p) + except Exception: # noqa: BLE001 + pass + return f"{MCP_TITLE_PREFIX}:{name}-{hash8}" + + +def set_console_title(title: str) -> bool: + """Set Windows console title (win32 only, never raises). + + Returns True if the title was set, False otherwise (non-Windows + or any error). Guards the ``ctypes.windll`` access which does not + exist outside Windows. + """ + try: + if sys.platform != "win32": + return False + import ctypes + + ctypes.windll.kernel32.SetConsoleTitleW(title) + return True + except Exception as e: # noqa: BLE001 + logger.debug(f"set_console_title failed: {e}") + return False + + +def apply_mcp_process_title(project_path: str | Path | None = None) -> str: + """Build + apply (+ log) the MCP console title. Never raises.""" + title = build_mcp_title(project_path) + ok = set_console_title(title) + try: + logger.info(f"Process title: {title} (applied={ok})") + except Exception: # noqa: BLE001 + pass + return title + + +def role_bin_name(role: str, default_name: str = "llama-server.exe") -> str: + """Map a llama role (``embed``/``rerank``) to its hardlink file name.""" + if sys.platform != "win32": + return default_name + return ROLE_BIN_NAMES.get(role, default_name) + + +def ensure_role_hardlink(bin_path: str | Path, role: str) -> Path: + """Create (once) an NTFS hardlink ``llama-.exe`` → ``bin_path``. + + Uses ``os.link`` (same inode, no 100MB+ copy). If the link already + exists, it is reused as-is. On any failure (permissions, FS without + hardlink support) logs and returns the original ``bin_path`` — + callers must fall back to plain spawn and never crash startup. + """ + src = Path(bin_path) + try: + if sys.platform != "win32": + return src + link_name = ROLE_BIN_NAMES.get(role) + if not link_name: + return src + link = src.parent / link_name + if link.exists(): + return link + try: + os.link(str(src), str(link)) + logger.info(f"Hardlink created: {link} -> {src}") + except Exception as e: # noqa: BLE001 + logger.warning(f"Hardlink {link} failed ({e}); using {src}") + return src + return link + except Exception as e: # noqa: BLE001 + logger.warning(f"ensure_role_hardlink failed ({e}); using {src}") + return src + + +def resolve_llama_role_bin(bin_path: str | Path, role: str) -> str: + """Resolve the executable path to spawn for a llama role (str).""" + return str(ensure_role_hardlink(bin_path, role)) diff --git a/src/main.py b/src/main.py index ae8c75a5..ea573ce9 100644 --- a/src/main.py +++ b/src/main.py @@ -214,6 +214,21 @@ def main(): logger.info("MSCodebase Intelligence MCP Server запускается...") logger.info(f"PROJECT_ROOT: {PROJECT_ROOT}") + # ─── Process title (Windows Task Manager naming) ─── + # SetConsoleTitleW so `tasklist /V` shows mscodebase-mcp:-. + # Guarded: win32-only, never raises (see src/core/process_titles.py). + try: + from src.core.process_titles import apply_mcp_process_title + + _title_proj = os.environ.get("MSCODEBASE_PROJECT_PATH") or os.environ.get( + "PROJECT_PATH" + ) + if not _title_proj: + _title_proj = _cli_project_path() or str(Path.cwd()) + apply_mcp_process_title(_title_proj) + except Exception: + pass + # ─── CLI --project-path: явный выбор целевого проекта ─── # Устанавливаем MSCODEBASE_PROJECT_PATH (приоритет над CWD, см. # resolve_project_root). PROJECT_ROOT (корень установки/расширения) НЕ diff --git a/src/providers/reranker/llama_runner.py b/src/providers/reranker/llama_runner.py index dceb17c1..5327a357 100644 --- a/src/providers/reranker/llama_runner.py +++ b/src/providers/reranker/llama_runner.py @@ -297,7 +297,12 @@ def _is_pid_alive(pid: int) -> bool: if kernel32.QueryFullProcessImageNameW( handle, 0, name_buf, ctypes.byref(size) ): - return "llama-server" in Path(name_buf.value).name.lower() + _lname = Path(name_buf.value).name.lower() + return ( + "llama-server" in _lname + or "llama-embed" in _lname + or "llama-rerank" in _lname + ) return False finally: kernel32.CloseHandle(handle) @@ -1058,10 +1063,20 @@ def _spawn_embedder(self, model_key: str) -> bool: flags = ["--embedding", "--pooling", GGUF_MODELS.get(model_key,{}).get("pooling","cls")] if model_key in GGUF_MODELS and model_key != DEFAULT_RERANKER_MODEL else ["--reranking"] + # Role hardlink: spawn via llama-embed.exe so Task Manager shows the + # role (NTFS hardlink, same inode — no binary copy; fallback = plain). + try: + from src.core.process_titles import resolve_llama_role_bin + + _base = _llama_bin_vulkan() if os.getenv("LLAMA_BACKEND","msvc").lower()=="vulkan" else _llama_bin() + _spawn_bin = resolve_llama_role_bin(_base, "embed") + except Exception: + _spawn_bin = str(_llama_bin_vulkan()) if os.getenv("LLAMA_BACKEND","msvc").lower()=="vulkan" else str(_llama_bin()) + try: self._process = _popen_with_job( [ - str(_llama_bin_vulkan()) if os.getenv("LLAMA_BACKEND","msvc").lower()=="vulkan" else str(_llama_bin()), + _spawn_bin, "--host", self._host, "--port", str(self._port), "-m", str(gguf_path), @@ -1156,10 +1171,20 @@ async def _spawn_reranker(self) -> bool: self._ensure_port_free(self.RERANK_PORT) log_fh = None + try: + from src.core.process_titles import resolve_llama_role_bin as _resolve_rerank_bin + + _base_r = _llama_bin_vulkan() if os.getenv("LLAMA_BACKEND","msvc").lower()=="vulkan" else _llama_bin() + try: + _spawn_r = _resolve_rerank_bin(_base_r, "rerank") + except Exception: + _spawn_r = str(_base_r) + except Exception: + _spawn_r = str(_llama_bin_vulkan()) if os.getenv("LLAMA_BACKEND","msvc").lower()=="vulkan" else str(_llama_bin()) try: self._reranker_process = _popen_with_job( [ - str(_llama_bin_vulkan()) if os.getenv("LLAMA_BACKEND","msvc").lower()=="vulkan" else str(_llama_bin()), + _spawn_r, "--host", self._host, "--port", str(self.RERANK_PORT), "-m", str(gguf_path), @@ -1402,7 +1427,7 @@ def _ensure_port_free(self, port: int): ).decode().strip().lower() - if 'llama-server' not in cmd and 'ggml-rpc-server' not in cmd: + if 'llama-server' not in cmd and 'llama-embed' not in cmd and 'llama-rerank' not in cmd and 'ggml-rpc-server' not in cmd: logger.warning(f'⏭️ Порт {port} занят процессом PID {pid} (не llama-server), пропускаем') diff --git a/tests/test_process_titles.py b/tests/test_process_titles.py new file mode 100644 index 00000000..2297a645 --- /dev/null +++ b/tests/test_process_titles.py @@ -0,0 +1,91 @@ +"""Unit tests for process naming (MCP title + llama role hardlinks).""" + +import sys +from pathlib import Path +from unittest.mock import patch + + +def test_build_mcp_title_format(tmp_path): + from src.core import process_titles as pt + from src.core.artifact_paths import project_hash + + p = tmp_path / "MyProj" + p.mkdir() + title = pt.build_mcp_title(p) + assert title == f"mscodebase-mcp:MyProj-{project_hash(p)}" + + +def test_build_mcp_title_none(): + from src.core import process_titles as pt + + assert pt.build_mcp_title(None) == "mscodebase-mcp:unknown-unknown" + + +def test_set_console_title_non_win_returns_false(): + from src.core import process_titles as pt + + with patch.object(sys, "platform", "linux"): + assert pt.set_console_title("x") is False + + +def test_role_bin_name_mapping(): + from src.core import process_titles as pt + + if sys.platform == "win32": + assert pt.role_bin_name("embed") == "llama-embed.exe" + assert pt.role_bin_name("rerank") == "llama-rerank.exe" + assert pt.role_bin_name("other") == "llama-server.exe" + else: + assert pt.role_bin_name("embed") == "llama-server.exe" + + +def test_ensure_role_hardlink_mock_link(tmp_path): + from src.core import process_titles as pt + + src = tmp_path / "llama-server.exe" + src.write_bytes(b"x") + with patch.object(sys, "platform", "win32"): + created = [] + + def fake_link(a, b): + created.append((a, b)) + Path(b).write_bytes(b"x") + + with patch("os.link", side_effect=fake_link): + out = pt.ensure_role_hardlink(src, "embed") + assert out == tmp_path / "llama-embed.exe" + assert out.exists() + assert created + + +def test_ensure_role_hardlink_fallback_on_error(tmp_path): + from src.core import process_titles as pt + + src = tmp_path / "llama-server.exe" + src.write_bytes(b"x") + with patch.object(sys, "platform", "win32"): + with patch("os.link", side_effect=OSError("denied")): + out = pt.ensure_role_hardlink(src, "rerank") + assert out == src + + +def test_ensure_role_hardlink_reuses_existing(tmp_path): + from src.core import process_titles as pt + + src = tmp_path / "llama-server.exe" + src.write_bytes(b"x") + link = tmp_path / "llama-embed.exe" + link.write_bytes(b"x") + with patch.object(sys, "platform", "win32"): + with patch("os.link", side_effect=AssertionError("must not be called")): + assert pt.ensure_role_hardlink(src, "embed") == link + + +def test_ensure_role_hardlink_non_win_no_link(tmp_path): + from src.core import process_titles as pt + + src = tmp_path / "llama-server.exe" + src.write_bytes(b"x") + with patch.object(sys, "platform", "linux"): + with patch("os.link", side_effect=AssertionError("must not be called")): + assert pt.ensure_role_hardlink(src, "embed") == src