From bbf7f3243e132ee06662f02daf045ddb9468bba6 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sat, 12 Oct 2019 23:59:33 +0200 Subject: [PATCH 1/2] If tcp_mode is "host", LSP opens specified tcp_port (or random) The resulting port is replaced into a {port} placeholder in the server's command line The language server is expected to connect, after which LSP starts initializing. This is suggested to be the default behaviour by Microsoft, although many servers have implemented a server-owned port. Solves issue #513 --- plugin/core/sessions.py | 22 +++++++++++++++++----- plugin/core/settings.py | 6 ++++-- plugin/core/transports.py | 10 ++++++++++ plugin/core/types.py | 19 +++++++++++++++---- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 624b2aac2..511904230 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1,6 +1,6 @@ from .types import ClientConfig, ClientStates, Settings from .protocol import Request -from .transports import start_tcp_transport +from .transports import start_tcp_transport, start_tcp_listener, TCPTransport, Transport from .rpc import Client, attach_stdio_client from .process import start_server from .url import filename_to_uri @@ -9,7 +9,7 @@ from .protocol import completion_item_kinds, symbol_kinds try: from typing import Callable, Dict, Any, Optional - assert Callable and Dict and Any and Optional + assert Callable and Dict and Any and Optional and Transport except ImportError: pass @@ -34,10 +34,22 @@ def with_client(client: Client) -> 'Session': session = None if config.binary_args: - process = start_server(config.binary_args, project_path, env, settings.log_stderr) + tcp_port = config.tcp_port + server_args = config.binary_args + + if config.tcp_mode == "host": + socket = start_tcp_listener(tcp_port or 0) + tcp_port = socket.getsockname()[1] + server_args = list(s.replace("{port}", str(tcp_port)) for s in config.binary_args) + + process = start_server(server_args, project_path, env, settings.log_stderr) if process: - if config.tcp_port: - transport = start_tcp_transport(config.tcp_port, config.tcp_host) + if config.tcp_mode == "host": + client_socket, address = socket.accept() + transport = TCPTransport(client_socket) # type: Transport + session = with_client(Client(transport, settings)) + elif tcp_port: + transport = start_tcp_transport(tcp_port, config.tcp_host) if transport: session = with_client(Client(transport, settings)) else: diff --git a/plugin/core/settings.py b/plugin/core/settings.py index 123bef97e..56f3bcb0b 100644 --- a/plugin/core/settings.py +++ b/plugin/core/settings.py @@ -180,7 +180,8 @@ def read_client_config(name: str, client_config: 'Dict') -> ClientConfig: client_config.get("initializationOptions", dict()), client_config.get("settings", dict()), client_config.get("env", dict()), - client_config.get("tcp_host", None) + client_config.get("tcp_host", None), + client_config.get("tcp_mode", None) ) @@ -198,5 +199,6 @@ def update_client_config(config: 'ClientConfig', settings: dict) -> 'ClientConfi settings.get("init_options", config.init_options), settings.get("settings", config.settings), settings.get("env", config.env), - settings.get("tcp_host", config.tcp_host) + settings.get("tcp_host", config.tcp_host), + settings.get("tcp_mode", config.tcp_mode) ) diff --git a/plugin/core/transports.py b/plugin/core/transports.py index adb300bb9..fccdd73e8 100644 --- a/plugin/core/transports.py +++ b/plugin/core/transports.py @@ -51,6 +51,16 @@ def state_to_string(state: int) -> str: return StateStrings.get(state, ''.format(state)) +def start_tcp_listener(tcp_port: int) -> socket.socket: + sock = socket.socket() + sock.bind(('', tcp_port)) + port = sock.getsockname()[1] + sock.settimeout(10) + debug('listening on {}:{}'.format('localhost', port)) + sock.listen(1) + return sock + + def start_tcp_transport(port: int, host: 'Optional[str]' = None) -> 'Transport': start_time = time.time() debug('connecting to {}:{}'.format(host or "localhost", port)) diff --git a/plugin/core/types.py b/plugin/core/types.py index 27bb4efc4..af2a0a17b 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -68,14 +68,25 @@ def __init__(self, language_id: str, scopes: 'List[str]', syntaxes: 'List[str]') class ClientConfig(object): - def __init__(self, name: str, binary_args: 'List[str]', tcp_port: 'Optional[int]', scopes: 'List[str]' = [], - syntaxes: 'List[str]' = [], languageId: 'Optional[str]' = None, - languages: 'List[LanguageConfig]' = [], enabled: bool = True, init_options: dict = dict(), - settings: dict = dict(), env: dict = dict(), tcp_host: 'Optional[str]' = None) -> None: + def __init__(self, + name: str, + binary_args: 'List[str]', + tcp_port: 'Optional[int]', + scopes: 'List[str]' = [], + syntaxes: 'List[str]' = [], + languageId: 'Optional[str]' = None, + languages: 'List[LanguageConfig]' = [], + enabled: bool = True, + init_options: dict = dict(), + settings: dict = dict(), + env: dict = dict(), + tcp_host: 'Optional[str]' = None, + tcp_mode: 'Optional[str]' = None) -> None: self.name = name self.binary_args = binary_args self.tcp_port = tcp_port self.tcp_host = tcp_host + self.tcp_mode = tcp_mode if not languages: languages = [LanguageConfig(languageId, scopes, syntaxes)] if languageId else [] self.languages = languages From 89cb5c3dde4ab08afb6f6e58f5efb491ff0f9217 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Mon, 14 Oct 2019 16:48:58 +0200 Subject: [PATCH 2/2] Document TCP configuration, use same timeout for both modes. --- LSP.sublime-settings | 12 ++++++++++++ docs/index.md | 26 ++++++++++++++++++++++---- plugin/core/transports.py | 2 +- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index 19a4be710..77ba53557 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -124,6 +124,18 @@ // // See: https://github.com/Microsoft/language-server-protocol/issues/213 // "languageId": "python", // + // # TCP mode (off unless tcp_mode or tcp_port are set) + // + // // Set to "host" if the server connects to the editor. Otherwise, LSP will connect to the server. + // "tcp_mode": "", + // + // // Port to connect to. If tcp_mode="host", you likely want to leave this empty so LSP selects a random port. + // // The chosen port can be passed as a server argument using a {port} placeholder. + // "tcp_port": 1234, + // + // // Host to connect to if not localhost + // "tcp_host": "", + // // # Optional settings (key-value pairs): // // // Sent to server once using workspace/didChangeConfiguration notification diff --git a/docs/index.md b/docs/index.md index 039d6c955..a1a323c83 100644 --- a/docs/index.md +++ b/docs/index.md @@ -459,17 +459,35 @@ or in multi-language form: } ``` -* `command` - specify a full paths, add arguments (if not specified then tcp_port must be specified) -* `tcp_port` - if not specified then stdin/out are used else sets the tcpport to connect to (if no command is specified then it is assumed that some process is listing on this port) +Most important: + +* `enabled` - enables a language server (default is disabled) + +Values that determine if a server should be started and queried for a given document: + * `scopes` - add language flavours, eg. `source.js`, `source.jsx`. * `syntaxes` - syntaxes that enable LSP features on a document, eg. `Packages/Babel/JavaScript (Babel).tmLanguage` * `languageId` - identifies the language for a document - see https://microsoft.github.io/language-server-protocol/specification#textdocumentitem * `languages` - group scope, syntax and languageId together for servers that support more than one language -* `enabled` - enables a language server (default is disabled) -* `settings` - per-project settings (equivalent to VS Code's Workspace Settings) + +Settings used to start and configure a language server: + +* `command` - must be on PATH or specify a full path, add arguments (can be empty if starting manually, then TCP transport must be configured) * `env` - dict of environment variables to be injected into the language server's process (eg. PYTHONPATH) +* `settings` - per-project settings (equivalent to VS Code's Workspace Settings) * `initializationOptions` - options to send to the server at startup (rarely used) +The default transport is stdio, but TCP is also supported. +The port number can be inserted into the server's arguments by adding a `{port}` placeholder in `command`. + +**Server-owned port** + +Set `tcp_port` and optionally `tcp_host` if server running on another host. + +**Editor-owned port** (servers based on vscode-languageserver-node): + +Set `tcp_mode` to "host", leave `tcp_port` unset for automatic port selection. +`tcp_port` can be set if eg. debugging a server. You may want to check out the LSP source and extend the `TCP_CONNECT_TIMEOUT`. ## Per-project overrides diff --git a/plugin/core/transports.py b/plugin/core/transports.py index fccdd73e8..44b353ac5 100644 --- a/plugin/core/transports.py +++ b/plugin/core/transports.py @@ -55,7 +55,7 @@ def start_tcp_listener(tcp_port: int) -> socket.socket: sock = socket.socket() sock.bind(('', tcp_port)) port = sock.getsockname()[1] - sock.settimeout(10) + sock.settimeout(TCP_CONNECT_TIMEOUT) debug('listening on {}:{}'.format('localhost', port)) sock.listen(1) return sock