24.09., 9:00 - 11:00: Due to updates GitLab will be unavailable for some minutes between 09:00 and 11:00.

server.py 24.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
# server.py - an asynchronous tcp-server to compile sources with DHParser
#
# Copyright 2019  by Eckhart Arnold (arnold@badw.de)
#                 Bavarian Academy of Sciences an Humanities (badw.de)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.  See the License for the specific language governing
# permissions and limitations under the License.


"""
Module `server` contains an asynchronous tcp-server that receives compilation
requests, runs custom compilation functions in a multiprocessing.Pool.

This allows to start a DHParser-compilation environment just once and save the
startup time of DHParser for each subsequent compilation. In particular, with
a just-in-time-compiler like PyPy (https://pypy.org) setting up a
compilation-server is highly recommended, because jit-compilers typically
sacrifice startup-speed for running-speed.

It is up to the compilation function to either return the result of the
compilation in serialized form, or just save the compilation results on the
file system an merely return an success or failure message. Module `server`
does not define any of these message. This is completely up to the clients
of module `server`, i.e. the compilation-modules, to decide.
34

eckhart's avatar
eckhart committed
35 36 37 38 39 40 41
The communication, i.e. requests and responses, follows the json-rpc protocol:

    https://www.jsonrpc.org/specification

For JSON see:

    https://json.org/
42 43
"""

44
# TODO: Test with python 3.5
45 46

import asyncio
47 48
from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor, CancelledError
from concurrent.futures.process import BrokenProcessPool
49
import json
50
from multiprocessing import Process, Queue, Value, Array
eckhart's avatar
eckhart committed
51
import sys
52
import time
53 54
from typing import Callable, Coroutine, Optional, Union, Dict, List, Tuple, Sequence, Set, Any, \
    cast
55

56
from DHParser.syntaxtree import DHParser_JSONEncoder
57
from DHParser.toolkit import get_config_value, re
58
from DHParser.versionnumber import __version__
59

60

eckhart's avatar
eckhart committed
61 62 63 64 65 66 67 68
__all__ = ('RPC_Table',
           'RPC_Type',
           'JSON_Type',
           'SERVER_ERROR',
           'SERVER_OFFLINE',
           'SERVER_STARTING',
           'SERVER_ONLINE',
           'SERVER_TERMINATE',
Eckhart Arnold's avatar
Eckhart Arnold committed
69 70 71 72 73 74
           'USE_DEFAULT_HOST',
           'USE_DEFAULT_PORT',
           'STOP_SERVER_REQUEST',
           'IDENTIFY_REQUEST',
           'ALL_RPCs',
           'identify_server',
eckhart's avatar
eckhart committed
75 76 77
           'Server')


78 79
RPC_Table = Dict[str, Callable]
RPC_Type = Union[RPC_Table, List[Callable], Callable]
eckhart's avatar
eckhart committed
80
JSON_Type = Union[Dict, Sequence, str, int, None]
81

82
RE_IS_JSONRPC = b'\s*{'  # b'\s*(?:{|\[|"|\d|true|false|null)'
83
RE_GREP_URL = b'GET ([^ \n]+) HTTP'
84
RE_FUNCTION_CALL = b'\s*(\w+)\(([^)]*)\)$'
eckhart's avatar
eckhart committed
85

86 87
SERVER_ERROR = "COMPILER-SERVER-ERROR"

eckhart's avatar
eckhart committed
88 89 90 91
SERVER_OFFLINE = 0
SERVER_STARTING = 1
SERVER_ONLINE = 2
SERVER_TERMINATE = 3
eckhart's avatar
eckhart committed
92

93
RESPONSE_HEADER = '''HTTP/1.1 200 OK
94 95 96 97
Date: {date}
Server: DHParser
Accept-Ranges: none
Content-Length: {length}
98
Connection: close
99
Content-Type: text/html; charset=utf-8
100
X-Pad: avoid browser bug
101
'''
102

103
ONELINER_HTML = """<!DOCTYPE html>
104
<html lang="en" xml:lang="en">
105
<head>
106
  <meta charset="UTF-8" />
107 108
</head>
<body>
109
<h1>{line}</h1>
110 111
</body>
</html>
112 113
"""

114 115 116
UNKNOWN_FUNC_HTML = ONELINER_HTML.format(
    line="DHParser Error: Function {func} unknown or not registered!")

Eckhart Arnold's avatar
Eckhart Arnold committed
117 118 119
USE_DEFAULT_HOST = ''
USE_DEFAULT_PORT = -1

Eckhart Arnold's avatar
Eckhart Arnold committed
120
STOP_SERVER_REQUEST = b"__STOP_SERVER__"
121
IDENTIFY_REQUEST = "identify()"
Eckhart Arnold's avatar
Eckhart Arnold committed
122 123 124 125


def identify_server():
    return "DHParser " + __version__
eckhart's avatar
eckhart committed
126 127


128
def json_rpc(func: Callable,
129
             params: Union[List[JSON_Type], Dict[str, JSON_Type]]=(),
130 131 132 133
             ID: Optional[int]=None) -> str:
    """Generates a JSON-RPC-call for `func` with parameters `params`"""
    return json.dumps({"jsonrpc": "2.0", "method": func.__name__, "params": params, "id": ID})

eckhart's avatar
eckhart committed
134

135 136 137 138 139 140 141 142
def maybe_int(s: str) -> Union[int, str]:
    """Convert string to int if possible, otherwise return string."""
    try:
        return int(s)
    except ValueError:
        return s


143
def asyncio_run(coroutine: Coroutine, loop=None) -> Any:
144 145
    """Backward compatible version of Pyhon 3.7's `asyncio.run()`"""
    if sys.version_info >= (3, 7):
Eckhart Arnold's avatar
Eckhart Arnold committed
146
        return asyncio.run(coroutine)
147
    else:
148 149 150 151 152 153 154 155
        if loop is None:
            myloop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
        else:
            myloop = loop
        result = myloop.run_until_complete(coroutine)
        if loop is None:
            asyncio.set_event_loop(None)
156
            myloop.run_until_complete(myloop.shutdown_asyncgens())
157
            myloop.close()
158
        return result
eckhart's avatar
eckhart committed
159

160 161

def GMT_timestamp() -> str:
162 163
    return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())

eckhart's avatar
eckhart committed
164

165 166
ALL_RPCs = frozenset('*')  # Magic value denoting all remove procedures

167

168
class Server:
169 170
    def __init__(self, rpc_functions: RPC_Type,
                 cpu_bound: Set[str] = ALL_RPCs,
171
                 blocking: Set[str] = frozenset()):
172 173
        if isinstance(rpc_functions, Dict):
            self.rpc_table = cast(RPC_Table, rpc_functions)  # type: RPC_Table
174
            self.default = tuple(self.rpc_table.keys())[0]   # type: str
175 176
        elif isinstance(rpc_functions, List):
            self.rpc_table = {}
177 178 179
            func_list = cast(List, rpc_functions)
            assert len(func_list) >= 1
            for func in func_list:
180
                self.rpc_table[func.__name__] = func
181
            self.default = func_list[0].__name__
182 183 184
        else:
            assert isinstance(rpc_functions, Callable)
            func = cast(Callable, rpc_functions)
eckhart's avatar
eckhart committed
185
            self.rpc_table = {func.__name__: func}
186 187
            self.default = func.__name__
        assert STOP_SERVER_REQUEST.decode() not in self.rpc_table
188 189
        if IDENTIFY_REQUEST.strip('()') not in self.rpc_table:
            self.rpc_table[IDENTIFY_REQUEST.strip('()')] = identify_server
eckhart's avatar
eckhart committed
190

191 192 193 194 195 196 197 198
        # see: https://docs.python.org/3/library/asyncio-eventloop.html#executing-code-in-thread-or-process-pools
        self.cpu_bound = frozenset(self.rpc_table.keys()) if cpu_bound == ALL_RPCs else cpu_bound
        self.blocking = frozenset(self.rpc_table.keys()) if blocking == ALL_RPCs else blocking
        self.blocking = self.blocking - self.cpu_bound  # cpu_bound property takes precedence

        assert not (self.cpu_bound - self.rpc_table.keys())
        assert not (self.blocking - self.rpc_table.keys())

eckhart's avatar
eckhart committed
199
        self.max_source_size = get_config_value('max_rpc_size')  #type: int
eckhart's avatar
eckhart committed
200 201
        self.server_messages = Queue()  # type: Queue
        self.server_process = None  # type: Optional[Process]
202

203 204 205 206 207
        # shared variables
        self.stage = Value('b', SERVER_OFFLINE)  # type: Value
        self.host = Array('c', b' ' * 1024)      # type: Array
        self.port = Value('H', 0)                # type: Value

208 209
        # if the server is run in a separate process, the following variables
        # should only be accessed from the server process
Eckhart Arnold's avatar
Eckhart Arnold committed
210 211 212 213
        self.server = None        # type: Optional[asyncio.AbstractServer]
        self.stop_response = ''   # type: str
        self.pp_executor = None   # type: Optional[ProcessPoolExecutor]
        self.tp_executor = None   # type: Optional[ThreadPoolExecutor]
eckhart's avatar
eckhart committed
214

215 216
        self.loop = None  # just for python 3.5 compatibility...

eckhart's avatar
eckhart committed
217 218 219
    async def handle_request(self,
                             reader: asyncio.StreamReader,
                             writer: asyncio.StreamWriter):
220
        rpc_error = None    # type: Optional[Tuple[int, str]]
221
        json_id = None      # type: Optional[int]
222 223 224 225
        obj = {}            # type: Dict
        result = None       # type: JSON_Type
        raw = None          # type: JSON_Type
        kill_switch = False # type: bool
226

eckhart's avatar
eckhart committed
227
        data = await reader.read(self.max_source_size + 1)
228
        oversized = len(data) > self.max_source_size
Eckhart Arnold's avatar
Eckhart Arnold committed
229

230 231 232 233 234 235
        def http_response(html: str) -> bytes:
            gmt = GMT_timestamp()
            encoded_html = html.encode()
            response = RESPONSE_HEADER.format(date=gmt, length=len(encoded_html))
            return response.encode() + encoded_html

236
        async def execute(executor: Executor, method: Callable, params: Union[Dict, Sequence]):
237
            nonlocal result, rpc_error
238 239 240 241
            has_kw_params = isinstance(params, Dict)
            assert has_kw_params or isinstance(params, Sequence)
            loop = asyncio.get_running_loop() if sys.version_info >= (3, 7) \
                else asyncio.get_event_loop()
242 243 244 245 246 247 248 249 250 251 252
            try:
                if executor is None:
                    result = method(**params) if has_kw_params else method(*params)
                elif has_kw_params:
                    result = await loop.run_in_executor(executor, method, **params)
                else:
                    result = await loop.run_in_executor(executor, method, *params)
            except TypeError as e:
                rpc_error = -32602, "Invalid Params: " + str(e)
            except NameError as e:
                rpc_error = -32601, "Method not found: " + str(e)
253 254
            except BrokenProcessPool as e:
                rpc_error = -32050, "Broken Executor: " + str(e)
255
            except Exception as e:
di68kap's avatar
di68kap committed
256
                rpc_error = -32000, "Server Error " + str(type(e)) + ': ' + str(e)
257

258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
        async def run(method_name: str, method: Callable, params: Union[Dict, Sequence]):
            # run method either a) directly if it is short running or
            # b) in a thread pool if it contains blocking io or
            # c) in a process pool if it is cpu bound
            # see: https://docs.python.org/3/library/asyncio-eventloop.html
            #      #executing-code-in-thread-or-process-pools
            executor = self.pp_executor if method_name in self.cpu_bound else \
                self.tp_executor if method_name in self.blocking else None
            await execute(executor, method, params)
            if rpc_error is not None and rpc_error[0] == -32050:
                # if process pool is broken, try again:
                self.pp_executor.shutdown(wait=True)
                self.pp_executor = ProcessPoolExecutor()
                await execute(self.pp_executor, method, params)

273
        if data.startswith(b'GET'):
274
            # HTTP request
275 276
            m = re.match(RE_GREP_URL, data)
            # m = RX_GREP_URL(data.decode())
277
            if m:
Eckhart Arnold's avatar
Eckhart Arnold committed
278 279
                func_name, argument = m.group(1).decode().strip('/').split('/', 1) + [None]
                if func_name.encode() == STOP_SERVER_REQUEST:
280 281
                    writer.write(http_response(ONELINER_HTML.format(line=self.stop_response)))
                    kill_switch = True
282
                else:
Eckhart Arnold's avatar
Eckhart Arnold committed
283 284
                    func = self.rpc_table.get(func_name,
                                              lambda _: UNKNOWN_FUNC_HTML.format(func=func_name))
Eckhart Arnold's avatar
Eckhart Arnold committed
285
                    # result = func(argument) if argument is not None else func()
286
                    await run(func.__name__, func, (argument,) if argument else ())
287 288 289 290
                    if rpc_error is None:
                        if isinstance(result, str):
                            writer.write(http_response(result))
                        else:
291 292
                            writer.write(http_response(
                                json.dumps(result, indent=2, cls=DHParser_JSONEncoder)))
Eckhart Arnold's avatar
Eckhart Arnold committed
293
                    else:
294
                        writer.write(http_response(rpc_error[1]))
295

296
        elif not re.match(RE_IS_JSONRPC, data):
297 298
            # plain data
            if oversized:
299
                writer.write("Source code too large! Only %i MB allowed"
300
                             % (self.max_source_size // (1024 ** 2)))
301
            elif data.startswith(STOP_SERVER_REQUEST):
Eckhart Arnold's avatar
Eckhart Arnold committed
302
                writer.write(self.stop_response.encode())
303
                kill_switch = True
304
            else:
305 306 307 308 309 310 311 312
                m = re.match(RE_FUNCTION_CALL, data)
                if m:
                    func_name = m.group(1).decode()
                    argstr = m.group(2).decode()
                    if argstr:
                        argument = tuple(maybe_int(s.strip('" \'')) for s in argstr.split(','))
                    else:
                        argument = ()
313
                else:
314
                    func_name = self.default
315
                    argument = (data.decode(),)
316
                err_func = lambda arg: 'Function %s no found!' % func_name
317
                func = self.rpc_table.get(func_name, err_func)
318
                await run(func_name, func, argument)
319 320 321 322
                if rpc_error is None:
                    if isinstance(result, str):
                        writer.write(result.encode())
                    else:
323
                        writer.write(json.dumps(result, cls=DHParser_JSONEncoder).encode())
324
                else:
325
                    writer.write(rpc_error[1].encode())
326

327 328 329 330 331
        else:
            # JSON RPC
            if oversized:
                rpc_error = -32600, "Invaild Request: Source code too large! Only %i MB allowed" \
                            % (self.max_source_size // (1024 ** 2))
Eckhart Arnold's avatar
Eckhart Arnold committed
332

333
            if rpc_error is None:
334
                try:
335
                    raw = json.loads(data.decode())
336 337 338 339 340 341
                except json.decoder.JSONDecodeError as e:
                    rpc_error = -32700, "JSONDecodeError: " + str(e) + str(data)

            if rpc_error is None:
                if isinstance(raw, Dict):
                    obj = cast(Dict, raw)
342
                    json_id = obj.get('id', None)
343 344 345 346
                else:
                    rpc_error = -32700, 'Parse error: Request does not appear to be an RPC-call!?'

            if rpc_error is None:
Eckhart Arnold's avatar
Eckhart Arnold committed
347
                if obj.get('jsonrpc', '0.0') < '2.0':
348 349 350 351
                    rpc_error = -32600, 'Invalid Request: jsonrpc version 2.0 needed, version "' \
                                        ' "%s" found.' % obj.get('jsonrpc', b'unknown')
                elif 'method' not in obj:
                    rpc_error = -32600, 'Invalid Request: No method specified.'
Eckhart Arnold's avatar
Eckhart Arnold committed
352 353
                elif obj['method'] == STOP_SERVER_REQUEST.decode():
                    result = self.stop_response
354
                    kill_switch = True
355 356 357 358 359 360
                elif obj['method'] not in self.rpc_table:
                    rpc_error = -32601, 'Method not found: ' + str(obj['method'])
                else:
                    method_name = obj['method']
                    method = self.rpc_table[method_name]
                    params = obj['params'] if 'params' in obj else ()
361
                    await run(method_name, method, params)
362

di68kap's avatar
di68kap committed
363 364 365 366 367 368
            try:
                error = cast(Dict[str, str], result['error'])
                rpc_error = error['code'], error['message']
            except TypeError:
                pass  # result is not a dictionary, never mind

369
            if rpc_error is None:
370 371 372
                if json_id is not None:
                    json_result = {"jsonrpc": "2.0", "result": result, "id": json_id}
                    writer.write(json.dumps(json_result, cls=DHParser_JSONEncoder).encode())
373 374 375
            else:
                writer.write(('{"jsonrpc": "2.0", "error": {"code": %i, "message": "%s"}, "id": %s}'
                              % (rpc_error[0], rpc_error[1], json_id)).encode())
376
        await writer.drain()
377
        if kill_switch:
378
            # TODO: terminate processes and threads! Is this needed??
379 380
            self.stage.value = SERVER_TERMINATE
            self.server.close()
381 382
            if self.loop is not None:
                self.loop.stop()
383

Eckhart Arnold's avatar
Eckhart Arnold committed
384 385 386 387 388 389
    async def serve(self, host: str = USE_DEFAULT_HOST, port: int = USE_DEFAULT_PORT):
        if host == USE_DEFAULT_HOST:
            host = get_config_value('server_default_host')
        if port == USE_DEFAULT_PORT:
            port = get_config_value('server_default_port')
        assert port >= 0
390 391 392 393 394 395
        # with ProcessPoolExecutor() as p, ThreadPoolExecutor() as t:
        try:
            if self.pp_executor is None:
                self.pp_executor = ProcessPoolExecutor()
            if self.tp_executor is None:
                self.tp_executor = ThreadPoolExecutor()
Eckhart Arnold's avatar
Eckhart Arnold committed
396
            self.stop_response = "DHParser server at {}:{} stopped!".format(host, port)
397 398
            self.host.value = host.encode()
            self.port.value = port
399
            self.server = cast(asyncio.base_events.Server,
Eckhart Arnold's avatar
Eckhart Arnold committed
400
                               await asyncio.start_server(self.handle_request, host, port))
401 402 403 404
            async with self.server:
                self.stage.value = SERVER_ONLINE
                self.server_messages.put(SERVER_ONLINE)
                await self.server.serve_forever()
405 406 407 408 409 410 411
        finally:
            if self.tp_executor is not None:
                self.tp_executor.shutdown(wait=True)
                self.tp_executor = None
            if self.pp_executor is not None:
                self.pp_executor.shutdown(wait=True)
                self.pp_executor = None
412

413
    def serve_py35(self, host: str = USE_DEFAULT_HOST, port: int = USE_DEFAULT_PORT, loop=None):
414 415 416 417 418 419 420 421 422 423 424
        if host == USE_DEFAULT_HOST:
            host = get_config_value('server_default_host')
        if port == USE_DEFAULT_PORT:
            port = get_config_value('server_default_port')
        assert port >= 0
        with ProcessPoolExecutor() as p, ThreadPoolExecutor() as t:
            self.pp_executor = p
            self.tp_executor = t
            self.stop_response = "DHParser server at {}:{} stopped!".format(host, port)
            self.host.value = host.encode()
            self.port.value = port
425 426 427 428 429
            if loop is None:
                self.loop = asyncio.new_event_loop()
                asyncio.set_event_loop(self.loop)
            else:
                self.loop = loop
430 431 432
            self.server = cast(
                asyncio.base_events.Server,
                self.loop.run_until_complete(
433
                    asyncio.start_server(self.handle_request, host, port, loop=self.loop)))
434 435 436 437 438
            try:
                self.stage.value = SERVER_ONLINE
                self.server_messages.put(SERVER_ONLINE)
                self.loop.run_forever()
            finally:
439 440
                if loop is None:
                    asyncio.set_event_loop(None)
441
                    self.loop.run_until_complete(self.loop.shutdown_asyncgens())
442
                    self.loop.close()
443
                self.server.close()
444
                asyncio_run(self.server.wait_closed())
445

446 447 448 449
    def _empty_message_queue(self):
        while not self.server_messages.empty():
            self.server_messages.get()

450
    def run_server(self, host: str = USE_DEFAULT_HOST, port: int = USE_DEFAULT_PORT, loop=None):
451 452 453 454
        """
        Starts a DHParser-Server. This function will not return until the
        DHParser-Server ist stopped by sending a STOP_SERVER_REQUEST.
        """
Eckhart Arnold's avatar
Eckhart Arnold committed
455
        assert self.stage.value == SERVER_OFFLINE
eckhart's avatar
eckhart committed
456
        self.stage.value = SERVER_STARTING
457 458
        self._empty_message_queue()
        try:
459 460 461
            if sys.version_info >= (3, 7):
                asyncio.run(self.serve(host, port))
            else:
462
                self.serve_py35(host, port, loop)
463
        except CancelledError:
464 465 466 467 468 469
            pass
        self.pp_executor = None
        self.tt_exectuor = None
        asyncio_run(self.server.wait_closed())
        self.server_messages.put(SERVER_OFFLINE)
        self.stage.value = SERVER_OFFLINE
eckhart's avatar
eckhart committed
470

eckhart's avatar
eckhart committed
471
    def wait_until_server_online(self):
eckhart's avatar
eckhart committed
472
        if self.stage.value != SERVER_ONLINE:
473 474
            message = self.server_messages.get()
            if message != SERVER_ONLINE:
eckhart's avatar
eckhart committed
475 476 477
                raise AssertionError('could not start server!?')
            assert self.stage.value == SERVER_ONLINE

Eckhart Arnold's avatar
Eckhart Arnold committed
478
    def spawn_server(self, host: str = USE_DEFAULT_HOST, port: int = USE_DEFAULT_PORT):
479 480 481 482 483
        """
        Start DHParser-Server in a separate process and return.
        """
        if self.server_process:
            assert not self.server_process.is_alive()
484 485
            if sys.version_info >= (3, 7):
                self.server_process.close()
486 487 488 489
            self.server_process = None
        self._empty_message_queue()
        self.server_process = Process(
            target=self.run_server, args=(host, port), name="DHParser-Server")
eckhart's avatar
eckhart committed
490 491 492
        self.server_process.start()
        self.wait_until_server_online()

493 494 495 496 497 498 499 500 501 502 503 504 505 506
    async def termination_request(self):
        try:
            host, port = self.host.value.decode(), self.port.value
            reader, writer = await asyncio.open_connection(host, port)
            writer.write(STOP_SERVER_REQUEST)
            await reader.read(500)
            while self.stage.value != SERVER_OFFLINE \
                    and self.server_messages.get() != SERVER_OFFLINE:
                pass
            writer.close()
        except ConnectionRefusedError:
            pass

    def terminate_server(self):
507 508 509 510
        """
        Terminates the server process.
        """
        try:
511 512
            if self.stage.value in (SERVER_STARTING, SERVER_ONLINE):
                asyncio_run(self.termination_request())
513 514 515 516 517
            if self.server_process and self.server_process.is_alive():
                if self.stage.value in (SERVER_STARTING, SERVER_ONLINE):
                    self.stage.value = SERVER_TERMINATE
                    self.server_process.terminate()
                self.server_process.join()
518 519
                if sys.version_info >= (3, 7):
                    self.server_process.close()
520 521 522 523 524 525 526 527 528 529 530 531 532 533
                self.server_process = None
                self.stage.value = SERVER_OFFLINE
        except AssertionError as debugger_err:
            print('If this message appears out of debugging mode, it is an error!')

    def wait_for_termination(self):
        """
        Waits for the termination of the server-process. Termination of the
        server-process can be triggered by sending a STOP_SERVER_REQUEST to the
        server.
        """
        if self.stage.value in (SERVER_STARTING, SERVER_ONLINE, SERVER_TERMINATE):
            while self.server_messages.get() != SERVER_OFFLINE:
                pass
534
        self.terminate_server()
di68kap's avatar
di68kap committed
535 536


537 538 539 540 541 542 543
#######################################################################
#
# Language-Server base class
#
#######################################################################


di68kap's avatar
di68kap committed
544 545 546 547 548 549 550
class LanguageServer(Server):
    """Template for the implementation of a language server.
    See: https://microsoft.github.io/language-server-protocol/"""

    cpu_bound = ALL_RPCs    # type: Set[str]
    blocking = frozenset()  # type: Set[str]

Eckhart Arnold's avatar
Eckhart Arnold committed
551 552 553 554
    def __init__(self, additional_rpcs: RPC_Table,
                 cpu_bound: Set[str] = ALL_RPCs,
                 blocking: Set[str] = frozenset()):
        assert isinstance(additional_rpcs, Dict)
di68kap's avatar
di68kap committed
555
        rpc_table = dict()  # type: RPC_Table
Eckhart Arnold's avatar
Eckhart Arnold committed
556
        rpc_table.update(additional_rpcs)
di68kap's avatar
di68kap committed
557 558 559 560 561
        for attr in dir(self):
            if attr.startswith('rpc_'):
                name = attr[4:].replace('_', '/')
                func = getattr(self, attr)
                rpc_table[name] = func
Eckhart Arnold's avatar
Eckhart Arnold committed
562
        super().__init__(rpc_table, cpu_bound, blocking)
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
        self._server_initialized = False
        self._client_initialized = False

    def initialize(self,
                   processId: Optional[int],
                   rootPath: Optional[str],
                   rootUri: Optional[str],
                   initializeOptions: JSON_Type,
                   capabilities: JSON_Type,
                   trace: str,
                   workspaceFolders: List[Dict[str, str]]):
        # return {'capabilities': {}}
        return {"jsonrpc": "2.0",
                "error": {"code": -322002,
                          "message": 'Language Server Error: "initialize" is not implemented!'},
                "id": 0}

    def rpc_initialize(self, **kwargs):
        if self._server_initialized:
            return {"jsonrpc": "2.0",
583 584
                    "error": {"code": -322002,
                    "message": "Server has already been initialized."},
585 586 587 588 589 590 591 592 593 594 595 596
                    "id": 0}
        else:
            result = self.initialize(**kwargs)
            if 'error' not in result:
                self._server_initialized = True
            return result

    def rpc_initialized(self):
        if self._client_initialized:
            pass  # clients must not reply to notifations!
            # print('double notification!')
            # return {"jsonrpc": "2.0",
597 598
            #         "error": {"code": -322002,
            #         "message": "Initialize Notification already received!"},
599 600 601
            #         "id": 0}
        else:
            self._client_initialized = True
di68kap's avatar
di68kap committed
602