92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
# 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()
|