Skip to content

Commit 8b705bc

Browse files
ferozcogatesn
authored andcommitted
Add support for asynchronously handling requests (#261)
1 parent 099eb36 commit 8b705bc

23 files changed

+812
-416
lines changed

pyls/__main__.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import logging
55
import logging.config
66
import sys
7-
8-
from . import language_server
9-
from .python_ls import PythonLanguageServer
7+
from .python_ls import start_io_lang_server, start_tcp_lang_server, PythonLanguageServer
108

119
LOG_FORMAT = "%(asctime)s UTC - %(levelname)s - %(name)s - %(message)s"
1210

@@ -51,10 +49,10 @@ def main():
5149
_configure_logger(args.verbose, args.log_config, args.log_file)
5250

5351
if args.tcp:
54-
language_server.start_tcp_lang_server(args.host, args.port, PythonLanguageServer)
52+
start_tcp_lang_server(args.host, args.port, PythonLanguageServer)
5553
else:
5654
stdin, stdout = _binary_stdio()
57-
language_server.start_io_lang_server(stdin, stdout, PythonLanguageServer)
55+
start_io_lang_server(stdin, stdout, PythonLanguageServer)
5856

5957

6058
def _binary_stdio():

pyls/dispatcher.py

-30
This file was deleted.

pyls/json_rpc_server.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Copyright 2017 Palantir Technologies, Inc.
2+
import json
3+
import logging
4+
import threading
5+
6+
from jsonrpc.jsonrpc2 import JSONRPC20Response, JSONRPC20BatchRequest, JSONRPC20BatchResponse
7+
from jsonrpc.jsonrpc import JSONRPCRequest
8+
from jsonrpc.exceptions import JSONRPCInvalidRequestException
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
class JSONRPCServer(object):
14+
""" Read/Write JSON RPC messages """
15+
16+
def __init__(self, rfile, wfile):
17+
self.pending_request = {}
18+
self.rfile = rfile
19+
self.wfile = wfile
20+
self.write_lock = threading.Lock()
21+
22+
def close(self):
23+
with self.write_lock:
24+
self.wfile.close()
25+
self.rfile.close()
26+
27+
def get_messages(self):
28+
"""Generator that produces well structured JSON RPC message.
29+
30+
Yields:
31+
message: received message
32+
33+
Note:
34+
This method is not thread safe and should only invoked from a single thread
35+
"""
36+
while not self.rfile.closed:
37+
request_str = self._read_message()
38+
39+
if request_str is None:
40+
break
41+
if isinstance(request_str, bytes):
42+
request_str = request_str.decode("utf-8")
43+
44+
try:
45+
try:
46+
message_blob = json.loads(request_str)
47+
request = JSONRPCRequest.from_data(message_blob)
48+
if isinstance(request, JSONRPC20BatchRequest):
49+
self._add_batch_request(request)
50+
messages = request
51+
else:
52+
messages = [request]
53+
except JSONRPCInvalidRequestException:
54+
# work around where JSONRPC20Reponse expects _id key
55+
message_blob['_id'] = message_blob['id']
56+
# we do not send out batch requests so no need to support batch responses
57+
messages = [JSONRPC20Response(**message_blob)]
58+
except (KeyError, ValueError):
59+
log.exception("Could not parse message %s", request_str)
60+
continue
61+
62+
for message in messages:
63+
yield message
64+
65+
def write_message(self, message):
66+
""" Write message to out file descriptor.
67+
68+
Args:
69+
message (JSONRPCRequest, JSONRPCResponse): body of the message to send
70+
"""
71+
with self.write_lock:
72+
if self.wfile.closed:
73+
return
74+
elif isinstance(message, JSONRPC20Response) and message._id in self.pending_request:
75+
batch_response = self.pending_request[message._id](message)
76+
if batch_response is not None:
77+
message = batch_response
78+
79+
log.debug("Sending %s", message)
80+
body = message.json
81+
content_length = len(body)
82+
response = (
83+
"Content-Length: {}\r\n"
84+
"Content-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n"
85+
"{}".format(content_length, body)
86+
)
87+
self.wfile.write(response.encode('utf-8'))
88+
self.wfile.flush()
89+
90+
def _read_message(self):
91+
"""Reads the contents of a message.
92+
93+
Returns:
94+
body of message if parsable else None
95+
"""
96+
line = self.rfile.readline()
97+
98+
if not line:
99+
return None
100+
101+
content_length = _content_length(line)
102+
103+
# Blindly consume all header lines
104+
while line and line.strip():
105+
line = self.rfile.readline()
106+
107+
if not line:
108+
return None
109+
110+
# Grab the body
111+
return self.rfile.read(content_length)
112+
113+
def _add_batch_request(self, requests):
114+
pending_requests = [request for request in requests if not request.is_notification]
115+
if not pending_requests:
116+
return
117+
118+
batch_request = {'pending': len(pending_requests), 'resolved': []}
119+
for request in pending_requests:
120+
def cleanup_message(response):
121+
batch_request['pending'] -= 1
122+
batch_request['resolved'].append(response)
123+
del self.pending_request[request._id]
124+
return JSONRPC20BatchResponse(batch_request['resolved']) if batch_request['pending'] == 0 else None
125+
self.pending_request[request._id] = cleanup_message
126+
127+
128+
def _content_length(line):
129+
"""Extract the content length from an input line."""
130+
if line.startswith(b'Content-Length: '):
131+
_, value = line.split(b'Content-Length: ')
132+
value = value.strip()
133+
try:
134+
return int(value)
135+
except ValueError:
136+
raise ValueError("Invalid Content-Length header: {}".format(value))
137+
138+
return None

pyls/language_server.py

-95
This file was deleted.

0 commit comments

Comments
 (0)