# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. """Debugging support for LSP.""" import os import pathlib import runpy import sys import threading def update_sys_path(path_to_add: str) -> None: """Add given path to `sys.path`.""" if path_to_add not in sys.path and os.path.isdir(path_to_add): sys.path.append(path_to_add) def _debug_endpoint() -> tuple[str, int]: host = os.getenv("NXPS_DEBUG_HOST", "127.0.0.1") raw_port = os.getenv("NXPS_DEBUG_PORT", "5678") try: port = int(raw_port) except ValueError as error: raise RuntimeError(f"Invalid NXPS_DEBUG_PORT: {raw_port!r}") from error if not 1 <= port <= 65535: raise RuntimeError(f"NXPS_DEBUG_PORT is outside the valid range: {port}") return host, port def _connect_debugger(debugpy, host: str, port: int, timeout: float = 30.0) -> None: errors: list[BaseException] = [] def attach() -> None: # debugpy.connect() cannot be retried: after a refused connection a second # call terminates the process silently. Connect exactly once. try: debugpy.connect((host, port)) debugpy.wait_for_client() except BaseException as error: # pylint: disable=broad-exception-caught errors.append(error) # A stale debugpy adapter from an earlier debug session can still own the port. # It accepts the connection but never attaches, so bound the wait. waiter = threading.Thread(target=attach, daemon=True) waiter.start() waiter.join(timeout) if waiter.is_alive(): raise RuntimeError( f"Connected to {host}:{port}, but no VS Code debug session attached within " f"{timeout:.0f} seconds. A stale debugpy adapter probably still owns the " f"port; stop it (e.g. 'fuser -k {port}/tcp') and restart debugging." ) if errors: raise RuntimeError( f"No debugpy listener on {host}:{port}. Start the launch configuration " "'Python debug server (hidden)' (e.g. via a 'Debug Extension and Python' " "compound) before the language server." ) from errors[0] def main() -> None: # Ensure debugger is loaded before we load anything else, so server # initialization and module-level feature registration can be debugged. debugger_path = os.getenv("DEBUGPY_PATH") if not debugger_path: raise RuntimeError("DEBUGPY_PATH is missing in Python debug mode") if pathlib.Path(debugger_path).name.casefold() == "debugpy": debugger_path = os.fspath(pathlib.Path(debugger_path).parent) update_sys_path(debugger_path) # pylint: disable=wrong-import-position,import-error import debugpy host, port = _debug_endpoint() print(f"debugpy: waiting for VS Code at {host}:{port}", file=sys.stderr) try: _connect_debugger(debugpy, host, port) except RuntimeError as error: print(f"debugpy: {error}", file=sys.stderr, flush=True) # debugpy's background threads can keep the interpreter alive; exit hard. os._exit(1) print("debugpy: VS Code attached; starting language server", file=sys.stderr) server_path = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py") runpy.run_path(server_path, run_name="__main__") if __name__ == "__main__": main()