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
34 changes: 33 additions & 1 deletion install.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@
IS_WINDOWS = sys.platform == "win32"
PYTHON_EXE = VENV_DIR / "Scripts" / "python.exe" if IS_WINDOWS else VENV_DIR / "bin" / "python3"
# Запуск MCP из расширения без окна консоли (pythonw, инцидент 2026-08-14)
# Task Manager показывает имя exe (у pythonw нет консоли для title, PR #59),
# поэтому MCP запускается через NTFS-хардлинк mscodebase-mcp.exe → pythonw.exe
# (тот же приём, что llama-embed/llama-rerank; см. src/core/process_titles.py).
MCP_PYTHON = VENV_DIR / "Scripts" / "pythonw.exe" if IS_WINDOWS else PYTHON_EXE
MCP_EXE = VENV_DIR / "Scripts" / "mscodebase-mcp.exe" if IS_WINDOWS else PYTHON_EXE
VENV_SITE_PACKAGES = (
VENV_DIR / "Lib" / "site-packages"
if IS_WINDOWS
Expand Down Expand Up @@ -295,6 +299,28 @@ def _fix_ghosts() -> int:
return len(candidates)


def _ensure_mcp_hardlink() -> Path:
"""Создаёт (once) NTFS-хардлинк mscodebase-mcp.exe → pythonw.exe.

Тот же приём, что llama-embed/llama-rerank: os.link (та же inode,
без копии). Молча пропускает при любой ошибке — вызовы падают
назад на plain pythonw.exe и установка не роняется.
"""
try:
sys.path.insert(0, str(PROJECT_ROOT))
from src.core.process_titles import ensure_mcp_hardlink

return ensure_mcp_hardlink(MCP_PYTHON)
except Exception as e:
logger.debug("_ensure_mcp_hardlink failed: %s", e)
return MCP_PYTHON
finally:
try:
sys.path.remove(str(PROJECT_ROOT))
except ValueError:
pass


# ─── Process cleanup (cross-platform, wmic-free) ───────────
def _kill_by_cmdline_windows(pattern: str) -> int:
"""Убивает процессы, чья командная строка содержит pattern.
Expand Down Expand Up @@ -526,13 +552,15 @@ def step_venv(lines, lang):
if VENV_DIR.exists() and PYTHON_EXE.exists():
r = _run(f'"{PYTHON_EXE}" --version', timeout=10)
if r and r.returncode == 0:
_ensure_mcp_hardlink()
lines.append((C.GRN, f"✓ {VENV_DIR}"))
return
lines.append((C.YEL, "⚠ venv found but broken, recreating"))
_safe_rmtree(VENV_DIR)

r = _run(f'"{sys.executable}" -m venv "{VENV_DIR}"', timeout=60)
if r and r.returncode == 0 and PYTHON_EXE.exists():
_ensure_mcp_hardlink()
lines.append((C.GRN, f"✓ {VENV_DIR}"))
else:
raise RuntimeError("venv creation failed")
Expand Down Expand Up @@ -803,7 +831,11 @@ def step_zedcfg(lines, lang):
# install.py НАСТРАИВАЕТ MCP в settings.json Zed через patch_zed_settings().
# Явный путь к venv РАСШИРЕНИЯ (не command=None — get_python_path вернул бы
# venv проекта) + pythonw.exe без окна консоли. Инцидент 2026-08-14.
cmd = f"{MCP_PYTHON} -u -m src.main"
# Имя exe — то, что показывает Task Manager (у pythonw нет консоли для
# title): запускаемся через mscodebase-mcp.exe-хардлинк → pythonw.exe,
# fallback — plain pythonw при ошибке создания линка.
mcp_exe = str(_ensure_mcp_hardlink()) if IS_WINDOWS else str(MCP_PYTHON)
cmd = f"{mcp_exe} -u -m src.main"
if patch_zed_settings(
cmd,
mode="global",
Expand Down
64 changes: 55 additions & 9 deletions src/core/process_titles.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

MCP_TITLE_PREFIX = "mscodebase-mcp"

MCP_EXE_NAME = "mscodebase-mcp.exe"

ROLE_BIN_NAMES = {
"embed": "llama-embed.exe",
"rerank": "llama-rerank.exe",
Expand Down Expand Up @@ -80,19 +82,15 @@ def role_bin_name(role: str, default_name: str = "llama-server.exe") -> str:
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``.
def _ensure_hardlink(src: Path, link_name: str) -> Path:
"""Shared NTFS-hardlink helper (os.link, same inode, never raises).

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.
If ``link_name`` already exists next to ``src`` it is reused as-is.
On any failure returns ``src`` so callers fall back to plain spawn.
"""
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
Expand All @@ -106,10 +104,58 @@ def ensure_role_hardlink(bin_path: str | Path, role: str) -> Path:
return src
return link
except Exception as e: # noqa: BLE001
logger.warning(f"ensure_role_hardlink failed ({e}); using {src}")
logger.warning(f"_ensure_hardlink failed ({e}); using {src}")
return src


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)
link_name = ROLE_BIN_NAMES.get(role)
if not link_name:
return src
return _ensure_hardlink(src, link_name)


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))


def mcp_exe_name(default_name: str = "pythonw.exe") -> str:
"""Return the MCP hardlink file name (``mscodebase-mcp.exe`` on win32).

Falls back to ``default_name`` off-Windows so POSIX spawns keep
using the venv interpreter unchanged.
"""
if sys.platform != "win32":
return default_name
return MCP_EXE_NAME


def ensure_mcp_hardlink(pythonw_path: str | Path) -> Path:
"""Create (once) an NTFS hardlink ``mscodebase-mcp.exe`` → ``pythonw``.

Same pattern as :func:`ensure_role_hardlink` (os.link, same inode,
no copy). Console-title approach cannot work because the MCP runs
under windowless ``pythonw.exe`` (no console to title) — the exe
name is what Task Manager shows. Never raises: on any failure
returns the original ``pythonw_path`` for plain-spawn fallback.
"""
return _ensure_hardlink(Path(pythonw_path), MCP_EXE_NAME)


def resolve_mcp_exe(pythonw_path: str | Path) -> str:
"""Resolve the executable path to spawn for the MCP (str).

Returns the ``mscodebase-mcp.exe`` hardlink when it exists (or can
be created); otherwise falls back to the given ``pythonw_path`` —
e.g. when the hardlink is missing and creation failed.
"""
return str(ensure_mcp_hardlink(pythonw_path))
66 changes: 66 additions & 0 deletions tests/test_process_titles.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,69 @@ def test_ensure_role_hardlink_non_win_no_link(tmp_path):
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


def test_mcp_exe_name_win():
from src.core import process_titles as pt

with patch.object(sys, "platform", "win32"):
assert pt.mcp_exe_name() == "mscodebase-mcp.exe"


def test_mcp_exe_name_non_win_fallback():
from src.core import process_titles as pt

with patch.object(sys, "platform", "linux"):
assert pt.mcp_exe_name() == "pythonw.exe"
assert pt.mcp_exe_name("python3") == "python3"


def test_ensure_mcp_hardlink_mock_link(tmp_path):
from src.core import process_titles as pt

src = tmp_path / "pythonw.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_mcp_hardlink(src)
assert out == tmp_path / "mscodebase-mcp.exe"
assert out.exists()
assert created == [(str(src), str(tmp_path / "mscodebase-mcp.exe"))]


def test_ensure_mcp_hardlink_fallback_on_error(tmp_path):
from src.core import process_titles as pt

src = tmp_path / "pythonw.exe"
src.write_bytes(b"x")
with patch.object(sys, "platform", "win32"):
with patch("os.link", side_effect=OSError("denied")):
assert pt.ensure_mcp_hardlink(src) == src


def test_resolve_mcp_exe_fallback_when_link_missing(tmp_path):
from src.core import process_titles as pt

src = tmp_path / "pythonw.exe"
src.write_bytes(b"x")
with patch.object(sys, "platform", "win32"):
with patch("os.link", side_effect=OSError("denied")):
assert pt.resolve_mcp_exe(src) == str(src)


def test_resolve_mcp_exe_reuses_existing(tmp_path):
from src.core import process_titles as pt

src = tmp_path / "pythonw.exe"
src.write_bytes(b"x")
link = tmp_path / "mscodebase-mcp.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.resolve_mcp_exe(src) == str(link)
Loading