1
|
|
|
"""Asynchronous msgpack-rpc handling in the event loop pipeline.""" |
2
|
5 |
|
import logging |
3
|
5 |
|
from traceback import format_exc |
4
|
|
|
|
5
|
|
|
|
6
|
5 |
|
logger = logging.getLogger(__name__) |
7
|
5 |
|
debug, info, warn = (logger.debug, logger.info, logger.warning,) |
8
|
|
|
|
9
|
|
|
|
10
|
5 |
|
class AsyncSession(object): |
11
|
|
|
|
12
|
|
|
"""Asynchronous msgpack-rpc layer that wraps a msgpack stream. |
13
|
|
|
|
14
|
|
|
This wraps the msgpack stream interface for reading/writing msgpack |
15
|
|
|
documents and exposes an interface for sending and receiving msgpack-rpc |
16
|
|
|
requests and notifications. |
17
|
|
|
""" |
18
|
|
|
|
19
|
5 |
|
def __init__(self, msgpack_stream): |
20
|
|
|
"""Wrap `msgpack_stream` on a msgpack-rpc interface.""" |
21
|
5 |
|
self._msgpack_stream = msgpack_stream |
22
|
5 |
|
self._next_request_id = 1 |
23
|
5 |
|
self._pending_requests = {} |
24
|
5 |
|
self._request_cb = self._notification_cb = None |
25
|
5 |
|
self._handlers = { |
26
|
|
|
0: self._on_request, |
27
|
|
|
1: self._on_response, |
28
|
|
|
2: self._on_notification |
29
|
|
|
} |
30
|
|
|
|
31
|
5 |
|
def threadsafe_call(self, fn): |
32
|
|
|
"""Wrapper around `MsgpackStream.threadsafe_call`.""" |
33
|
5 |
|
self._msgpack_stream.threadsafe_call(fn) |
34
|
|
|
|
35
|
5 |
|
def request(self, method, args, response_cb): |
36
|
|
|
"""Send a msgpack-rpc request to Nvim. |
37
|
|
|
|
38
|
|
|
A msgpack-rpc with method `method` and argument `args` is sent to |
39
|
|
|
Nvim. The `response_cb` function is called with when the response |
40
|
|
|
is available. |
41
|
|
|
""" |
42
|
5 |
|
request_id = self._next_request_id |
43
|
5 |
|
self._next_request_id = request_id + 1 |
44
|
5 |
|
self._msgpack_stream.send([0, request_id, method, args]) |
45
|
5 |
|
self._pending_requests[request_id] = response_cb |
46
|
|
|
|
47
|
5 |
|
def notify(self, method, args): |
48
|
|
|
"""Send a msgpack-rpc notification to Nvim. |
49
|
|
|
|
50
|
|
|
A msgpack-rpc with method `method` and argument `args` is sent to |
51
|
|
|
Nvim. This will have the same effect as a request, but no response |
52
|
|
|
will be recieved |
53
|
|
|
""" |
54
|
5 |
|
self._msgpack_stream.send([2, method, args]) |
55
|
|
|
|
56
|
5 |
|
def run(self, request_cb, notification_cb): |
57
|
|
|
"""Run the event loop to receive requests and notifications from Nvim. |
58
|
|
|
|
59
|
|
|
While the event loop is running, `request_cb` and `_notification_cb` |
60
|
|
|
will be called whenever requests or notifications are respectively |
61
|
|
|
available. |
62
|
|
|
""" |
63
|
5 |
|
self._request_cb = request_cb |
64
|
5 |
|
self._notification_cb = notification_cb |
65
|
5 |
|
self._msgpack_stream.run(self._on_message) |
66
|
5 |
|
self._request_cb = None |
67
|
5 |
|
self._notification_cb = None |
68
|
|
|
|
69
|
5 |
|
def stop(self): |
70
|
|
|
"""Stop the event loop.""" |
71
|
5 |
|
self._msgpack_stream.stop() |
72
|
|
|
|
73
|
5 |
|
def close(self): |
74
|
5 |
|
"""Close the event loop.""" |
75
|
5 |
|
self._msgpack_stream.close() |
76
|
|
|
|
77
|
|
|
|
78
|
|
|
def _on_message(self, msg): |
79
|
|
|
try: |
80
|
|
|
self._handlers.get(msg[0], self._on_invalid_message)(msg) |
81
|
5 |
|
except Exception: |
|
|
|
|
82
|
|
|
err_str = format_exc(5) |
83
|
|
|
warn(err_str) |
84
|
|
|
self._msgpack_stream.send([1, 0, err_str, None]) |
85
|
|
|
|
86
|
5 |
|
def _on_request(self, msg): |
87
|
5 |
|
# request |
88
|
|
|
# - msg[1]: id |
89
|
|
|
# - msg[2]: method name |
90
|
5 |
|
# - msg[3]: arguments |
91
|
|
|
debug('received request: %s, %s', msg[2], msg[3]) |
92
|
|
|
self._request_cb(msg[2], msg[3], Response(self._msgpack_stream, |
93
|
|
|
msg[1])) |
94
|
|
|
|
95
|
5 |
|
def _on_response(self, msg): |
96
|
5 |
|
# response to a previous request: |
97
|
|
|
# - msg[1]: the id |
98
|
5 |
|
# - msg[2]: error(if any) |
99
|
|
|
# - msg[3]: result(if not errored) |
100
|
|
|
debug('received response: %s, %s', msg[2], msg[3]) |
101
|
|
|
self._pending_requests.pop(msg[1])(msg[2], msg[3]) |
102
|
5 |
|
|
103
|
5 |
|
def _on_notification(self, msg): |
104
|
|
|
# notification/event |
105
|
5 |
|
# - msg[1]: event name |
106
|
|
|
# - msg[2]: arguments |
107
|
|
|
debug('received notification: %s, %s', msg[1], msg[2]) |
108
|
|
|
self._notification_cb(msg[1], msg[2]) |
109
|
|
|
|
110
|
|
|
def _on_invalid_message(self, msg): |
111
|
5 |
|
error = 'Received invalid message %s' % msg |
112
|
|
|
warn(error) |
113
|
|
|
self._msgpack_stream.send([1, 0, error, None]) |
114
|
|
|
|
115
|
|
|
|
116
|
|
|
class Response(object): |
117
|
|
|
|
118
|
|
|
"""Response to a msgpack-rpc request that came from Nvim. |
119
|
5 |
|
|
120
|
|
|
When Nvim sends a msgpack-rpc request, an instance of this class is |
121
|
5 |
|
created for remembering state required to send a response. |
122
|
5 |
|
""" |
123
|
|
|
|
124
|
5 |
|
def __init__(self, msgpack_stream, request_id): |
125
|
|
|
"""Initialize the Response instance.""" |
126
|
|
|
self._msgpack_stream = msgpack_stream |
127
|
|
|
self._request_id = request_id |
128
|
|
|
|
129
|
5 |
|
def send(self, value, error=False): |
130
|
|
|
"""Send the response. |
131
|
|
|
|
132
|
5 |
|
If `error` is True, it will be sent as an error. |
133
|
5 |
|
""" |
134
|
5 |
|
if error: |
135
|
|
|
resp = [1, self._request_id, value, None] |
136
|
|
|
else: |
137
|
|
|
resp = [1, self._request_id, None, value] |
138
|
|
|
debug('sending response to request %d: %s', self._request_id, resp) |
139
|
|
|
self._msgpack_stream.send(resp) |
140
|
|
|
|
Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.
So, unless you specifically plan to handle any error, consider adding a more specific exception.