|
| 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 |
0 commit comments