"""Windows 兼容的 Cursor SDK bridge 管理。""" from __future__ import annotations import codecs import json import logging import os import subprocess import threading import time from pathlib import Path from typing import Any, Mapping import env_config logger = logging.getLogger(__name__) READY_LINE_PREFIX = "cursor-sdk-bridge ready " _bridge_lock = threading.Lock() _bridge_process: subprocess.Popen[bytes] | None = None def _cursor_cwd() -> str: return env_config.env("CURSOR_CWD", r"d:\LY\test\tech") or r"d:\LY\test\tech" def _parse_discovery_line(line: str) -> Mapping[str, Any] | None: if not line.startswith(READY_LINE_PREFIX): return None payload = line[len(READY_LINE_PREFIX) :].strip() loaded = json.loads(payload) if not isinstance(loaded, dict): raise RuntimeError("Bridge discovery payload must be an object") return loaded def _read_discovery_polling(process: subprocess.Popen[bytes], timeout: float = 60) -> Mapping[str, Any]: """不用 selectors,避免 Windows 上 WinError 10038。""" if process.stderr is None: raise RuntimeError("Bridge stderr unavailable") fd = process.stderr.fileno() was_blocking = os.get_blocking(fd) os.set_blocking(fd, False) decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") pending = "" stderr_lines: list[str] = [] deadline = time.monotonic() + timeout try: while time.monotonic() < deadline: try: chunk = os.read(fd, 8192) except BlockingIOError: chunk = b"" if chunk: pending += decoder.decode(chunk) while "\n" in pending: line, pending = pending.split("\n", 1) stderr_lines.append(line) discovery = _parse_discovery_line(line) if discovery is not None: return discovery else: code = process.poll() if code is not None: pending += decoder.decode(b"", final=True) if pending.strip(): stderr_lines.append(pending.strip()) joined = "\n".join(stderr_lines)[-2000:] raise RuntimeError( f"Bridge 启动失败 exit={code}: {joined or '无 stderr 输出'}" ) time.sleep(0.05) finally: os.set_blocking(fd, was_blocking) raise RuntimeError("等待 Cursor bridge 就绪超时") def _auth_token_from_discovery(discovery: Mapping[str, Any]) -> str: token = str(discovery.get("authToken") or "").strip() if token: return token token_file = discovery.get("authTokenFile") if token_file: return Path(str(token_file)).read_text(encoding="utf-8").strip() raise RuntimeError("Bridge discovery 缺少 auth token") def warm_cursor_bridge(force: bool = False) -> None: """启动 cursor-sdk-bridge 并写入 CURSOR_SDK_BRIDGE_* 环境变量。""" global _bridge_process with _bridge_lock: if ( not force and _bridge_process is not None and _bridge_process.poll() is None and os.environ.get("CURSOR_SDK_BRIDGE_URL") and os.environ.get("CURSOR_SDK_BRIDGE_TOKEN") ): return if _bridge_process is not None and _bridge_process.poll() is None: _bridge_process.terminate() try: _bridge_process.wait(timeout=5) except subprocess.TimeoutExpired: _bridge_process.kill() from cursor_sdk._vendor import resolve_bridge_path cwd = _cursor_cwd() argv = [resolve_bridge_path(), "--workspace", cwd] logger.info("启动 Cursor bridge workspace=%s", cwd) process = subprocess.Popen( argv, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, ) try: discovery = _read_discovery_polling(process) except Exception: process.kill() process.wait(timeout=5) raise url = str(discovery.get("url") or "").strip() if not url: host = str(discovery.get("host") or "127.0.0.1") port = discovery.get("port") url = f"http://{host}:{port}" token = _auth_token_from_discovery(discovery) os.environ["CURSOR_SDK_BRIDGE_URL"] = url os.environ["CURSOR_SDK_BRIDGE_TOKEN"] = token _bridge_process = process logger.info("Cursor bridge 就绪: %s", url) def shutdown_cursor_bridge() -> None: global _bridge_process with _bridge_lock: if _bridge_process is None: return if _bridge_process.poll() is None: _bridge_process.terminate() try: _bridge_process.wait(timeout=5) except subprocess.TimeoutExpired: _bridge_process.kill() _bridge_process = None