Skip to content

Commit f14e088

Browse files
committed
Add --node-ipc support
Fix #1612
1 parent 20a1d74 commit f14e088

File tree

3 files changed

+93
-27
lines changed

3 files changed

+93
-27
lines changed

plugin/core/transports.py

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from queue import Queue
88
import http
99
import json
10+
import multiprocessing.connection
1011
import os
1112
import shutil
1213
import socket
@@ -48,26 +49,33 @@ def on_stderr_message(self, message: str) -> None:
4849

4950
class AbstractProcessor(Generic[T]):
5051

51-
def write_data(self, writer: IO[bytes], data: T) -> None:
52+
def write_data(self, writer: IO[bytes], data: T, is_node_ipc: bool) -> None:
5253
raise NotImplementedError()
5354

54-
def read_data(self, reader: IO[bytes]) -> Optional[T]:
55+
def read_data(self, reader: IO[bytes], is_node_ipc: bool) -> Optional[T]:
5556
raise NotImplementedError()
5657

5758

5859
class JsonRpcProcessor(AbstractProcessor[Dict[str, Any]]):
5960

60-
def write_data(self, writer: IO[bytes], data: Dict[str, Any]) -> None:
61+
def write_data(self, writer: IO[bytes], data: Dict[str, Any], is_node_ipc: bool) -> None:
6162
body = self._encode(data)
62-
writer.writelines(("Content-Length: {}\r\n\r\n".format(len(body)).encode('ascii'), body))
63+
if not is_node_ipc:
64+
writer.writelines(("Content-Length: {}\r\n\r\n".format(len(body)).encode('ascii'), body))
65+
else:
66+
writer.write(body + b"\n")
67+
68+
def read_data(self, reader: IO[bytes], is_node_ipc: bool) -> Optional[Dict[str, Any]]:
69+
if not is_node_ipc:
70+
headers = http.client.parse_headers(reader) # type: ignore
71+
try:
72+
body = reader.read(int(headers.get("Content-Length")))
73+
except TypeError:
74+
# Expected error on process stopping. Stop the read loop.
75+
raise StopLoopError()
76+
else:
77+
body = reader.readline()
6378

64-
def read_data(self, reader: IO[bytes]) -> Optional[Dict[str, Any]]:
65-
headers = http.client.parse_headers(reader) # type: ignore
66-
try:
67-
body = reader.read(int(headers.get("Content-Length")))
68-
except TypeError:
69-
# Expected error on process stopping. Stop the read loop.
70-
raise StopLoopError()
7179
try:
7280
return self._decode(body)
7381
except Exception as ex:
@@ -79,7 +87,6 @@ def _encode(data: Dict[str, Any]) -> bytes:
7987
return json.dumps(
8088
data,
8189
ensure_ascii=False,
82-
sort_keys=False,
8390
check_circular=False,
8491
separators=(',', ':')
8592
).encode('utf-8')
@@ -93,7 +100,7 @@ class ProcessTransport(Transport[T]):
93100

94101
def __init__(self, name: str, process: subprocess.Popen, socket: Optional[socket.socket], reader: IO[bytes],
95102
writer: IO[bytes], stderr: Optional[IO[bytes]], processor: AbstractProcessor[T],
96-
callback_object: TransportCallbacks[T]) -> None:
103+
callback_object: TransportCallbacks[T], is_node_ipc: bool) -> None:
97104
self._closed = False
98105
self._process = process
99106
self._socket = socket
@@ -105,6 +112,7 @@ def __init__(self, name: str, process: subprocess.Popen, socket: Optional[socket
105112
self._writer_thread = threading.Thread(target=self._write_loop, name='{}-writer'.format(name))
106113
self._stderr_thread = threading.Thread(target=self._stderr_loop, name='{}-stderr'.format(name))
107114
self._callback_object = weakref.ref(callback_object)
115+
self._is_node_ipc = is_node_ipc
108116
self._send_queue = Queue(0) # type: Queue[Union[T, None]]
109117
self._reader_thread.start()
110118
self._writer_thread.start()
@@ -137,7 +145,7 @@ def __del__(self) -> None:
137145
def _read_loop(self) -> None:
138146
try:
139147
while self._reader:
140-
payload = self._processor.read_data(self._reader)
148+
payload = self._processor.read_data(self._reader, self._is_node_ipc)
141149
if payload is None:
142150
continue
143151

@@ -190,8 +198,9 @@ def _write_loop(self) -> None:
190198
d = self._send_queue.get()
191199
if d is None:
192200
break
193-
self._processor.write_data(self._writer, d)
194-
self._writer.flush()
201+
self._processor.write_data(self._writer, d, self._is_node_ipc)
202+
if not self._is_node_ipc:
203+
self._writer.flush()
195204
except (BrokenPipeError, AttributeError):
196205
pass
197206
except Exception as ex:
@@ -223,24 +232,59 @@ def _stderr_loop(self) -> None:
223232
json_rpc_processor = JsonRpcProcessor()
224233

225234

235+
class NodeIpcIO():
236+
_buf = bytearray()
237+
_lines = 0
238+
239+
def __init__(self, conn: multiprocessing.connection._ConnectionBase):
240+
self._fd = conn.fileno()
241+
self._read = conn._read # type: ignore
242+
self._write = conn._write # type: ignore
243+
244+
# https://github.com/python/cpython/blob/330f1d58282517bdf1f19577ab9317fa9810bf95/Lib/multiprocessing/connection.py#L378-L392
245+
def readline(self) -> bytearray:
246+
while self._lines == 0:
247+
chunk = self._read(self._fd, 65536) # type: bytes
248+
self._buf += chunk
249+
self._lines += chunk.count(b'\n')
250+
251+
self._lines -= 1
252+
line, _, self._buf = self._buf.partition(b'\n')
253+
return line
254+
255+
# https://github.com/python/cpython/blob/330f1d58282517bdf1f19577ab9317fa9810bf95/Lib/multiprocessing/connection.py#L369-L376
256+
def write(self, data: bytes) -> None:
257+
while len(data):
258+
n = self._write(self._fd, data) # type: int
259+
data = data[n:]
260+
261+
226262
def create_transport(config: TransportConfig, cwd: Optional[str],
227263
callback_object: TransportCallbacks) -> Transport[Dict[str, Any]]:
264+
stderr = subprocess.PIPE
265+
pass_fds = () # type: Union[Tuple[()], Tuple[int]]
228266
if config.tcp_port is not None:
229267
assert config.tcp_port is not None
230268
if config.tcp_port < 0:
231269
stdout = subprocess.PIPE
232270
else:
233271
stdout = subprocess.DEVNULL
234272
stdin = subprocess.DEVNULL
235-
else:
273+
elif not config.node_ipc:
236274
stdout = subprocess.PIPE
237275
stdin = subprocess.PIPE
276+
else:
277+
stdout = subprocess.PIPE
278+
stdin = subprocess.DEVNULL
279+
stderr = subprocess.STDOUT
280+
pass_fds = (config.node_ipc.child_conn.fileno(),)
281+
238282
startupinfo = _fixup_startup_args(config.command)
239283
sock = None # type: Optional[socket.socket]
240284
process = None # type: Optional[subprocess.Popen]
241285

242286
def start_subprocess() -> subprocess.Popen:
243-
return _start_subprocess(config.command, stdin, stdout, subprocess.PIPE, startupinfo, config.env, cwd)
287+
return _start_subprocess(config.command, stdin, stdout, stderr, startupinfo, config.env, cwd, pass_fds)
244288

245289
if config.listener_socket:
246290
assert isinstance(config.tcp_port, int) and config.tcp_port > 0
@@ -258,13 +302,16 @@ def start_subprocess() -> subprocess.Popen:
258302
raise RuntimeError("Failed to connect on port {}".format(config.tcp_port))
259303
reader = sock.makefile('rwb') # type: ignore
260304
writer = reader
261-
else:
305+
elif not config.node_ipc:
262306
reader = process.stdout # type: ignore
263307
writer = process.stdin # type: ignore
308+
else:
309+
reader = writer = NodeIpcIO(config.node_ipc.parent_conn) # type: ignore
264310
if not reader or not writer:
265311
raise RuntimeError('Failed initializing transport: reader: {}, writer: {}'.format(reader, writer))
266-
return ProcessTransport(config.name, process, sock, reader, writer, process.stderr, json_rpc_processor,
267-
callback_object)
312+
stderr_reader = process.stdout if config.node_ipc else process.stderr
313+
return ProcessTransport(config.name, process, sock, reader, writer, stderr_reader, json_rpc_processor,
314+
callback_object, bool(config.node_ipc))
268315

269316

270317
_subprocesses = weakref.WeakSet() # type: weakref.WeakSet[subprocess.Popen]
@@ -312,7 +359,8 @@ def _start_subprocess(
312359
stderr: int,
313360
startupinfo: Any,
314361
env: Dict[str, str],
315-
cwd: Optional[str]
362+
cwd: Optional[str],
363+
pass_fds: Union[Tuple[()], Tuple[int]]
316364
) -> subprocess.Popen:
317365
debug("starting {} in {}".format(args, cwd if cwd else os.getcwd()))
318366
process = subprocess.Popen(
@@ -322,7 +370,8 @@ def _start_subprocess(
322370
stderr=stderr,
323371
startupinfo=startupinfo,
324372
env=env,
325-
cwd=cwd)
373+
cwd=cwd,
374+
pass_fds=pass_fds)
326375
_subprocesses.add(process)
327376
return process
328377

plugin/core/types.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
from wcmatch.glob import BRACE
1111
from wcmatch.glob import globmatch
1212
from wcmatch.glob import GLOBSTAR
13+
import collections
1314
import contextlib
1415
import fnmatch
16+
import multiprocessing
17+
import multiprocessing.connection
1518
import os
1619
import posixpath
1720
import socket
@@ -605,24 +608,34 @@ def map_from_remote_to_local(self, uri: str) -> Tuple[str, bool]:
605608
return _translate_path(uri, self._remote, self._local)
606609

607610

611+
NodeIpc = collections.namedtuple('NodeIpc', 'parent_conn,child_conn')
612+
613+
608614
class TransportConfig:
609-
__slots__ = ("name", "command", "tcp_port", "env", "listener_socket")
615+
__slots__ = ("name", "command", "tcp_port", "env", "listener_socket", "node_ipc")
610616

611617
def __init__(
612618
self,
613619
name: str,
614620
command: List[str],
615621
tcp_port: Optional[int],
616622
env: Dict[str, str],
617-
listener_socket: Optional[socket.socket]
623+
listener_socket: Optional[socket.socket],
624+
node_ipc: Optional[NodeIpc]
618625
) -> None:
619626
if not command and not tcp_port:
620627
raise ValueError('neither "command" nor "tcp_port" is provided; cannot start a language server')
628+
if node_ipc and (tcp_port or listener_socket):
629+
raise ValueError(
630+
'"tcp_port" and "listener_socket" can\'t be provided in "--node-ipc" mode; ' +
631+
'cannot start a language server'
632+
)
621633
self.name = name
622634
self.command = command
623635
self.tcp_port = tcp_port
624636
self.env = env
625637
self.listener_socket = listener_socket
638+
self.node_ipc = node_ipc
626639

627640

628641
class ClientConfig:
@@ -790,7 +803,11 @@ def resolve_transport_config(self, variables: Dict[str, str]) -> TransportConfig
790803
env[key] = sublime.expand_variables(value, variables) + os.path.pathsep + env[key]
791804
else:
792805
env[key] = sublime.expand_variables(value, variables)
793-
return TransportConfig(self.name, command, tcp_port, env, listener_socket)
806+
node_ipc = None
807+
if '--node-ipc' in command:
808+
node_ipc = NodeIpc(*multiprocessing.Pipe())
809+
env["NODE_CHANNEL_FD"] = str(node_ipc.child_conn.fileno())
810+
return TransportConfig(self.name, command, tcp_port, env, listener_socket, node_ipc)
794811

795812
def set_view_status(self, view: sublime.View, message: str) -> None:
796813
if sublime.load_settings("LSP.sublime-settings").get("show_view_status"):

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Tox (http://tox.testrun.org/) is a tool for running tests
1+
# Tox (https://github.com/tox-dev/tox) is a tool for running tests
22
# in multiple virtualenvs. This configuration file will run the
33
# test suite on all supported python versions. To use it, "pip install tox"
44
# and then run "tox" from this directory.

0 commit comments

Comments
 (0)