Skip to content
Merged
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
115 changes: 115 additions & 0 deletions src/core/process_titles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Process naming: MCP console title + llama role hardlinks (Windows).

Minimal, Windows-guarded helpers so Task Manager shows
``mscodebase-mcp:<name>-<hash8>`` 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:<name>-<hash8>`` 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-<role>.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))
15 changes: 15 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:<name>-<hash8>.
# 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 (корень установки/расширения) НЕ
Expand Down
33 changes: 29 additions & 4 deletions src/providers/reranker/llama_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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), пропускаем')

Expand Down
91 changes: 91 additions & 0 deletions tests/test_process_titles.py
Original file line number Diff line number Diff line change
@@ -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
Loading