|
1
|
|
|
"""Common code for event loop implementations.""" |
|
2
|
6 |
|
import logging |
|
3
|
6 |
|
import signal |
|
4
|
6 |
|
import threading |
|
5
|
|
|
|
|
6
|
|
|
|
|
7
|
6 |
|
logger = logging.getLogger(__name__) |
|
8
|
6 |
|
debug, info, warn = (logger.debug, logger.info, logger.warning,) |
|
9
|
|
|
|
|
10
|
|
|
|
|
11
|
|
|
# When signals are restored, the event loop library may reset SIGINT to SIG_DFL |
|
12
|
|
|
# which exits the program. To be able to restore the python interpreter to it's |
|
13
|
|
|
# default state, we keep a reference to the default handler |
|
14
|
6 |
|
default_int_handler = signal.getsignal(signal.SIGINT) |
|
15
|
6 |
|
main_thread = threading.current_thread() |
|
16
|
|
|
|
|
17
|
|
|
|
|
18
|
6 |
|
class BaseEventLoop(object): |
|
19
|
|
|
|
|
20
|
|
|
"""Abstract base class for all event loops. |
|
21
|
|
|
|
|
22
|
|
|
Event loops act as the bottom layer for Nvim sessions created by this |
|
23
|
|
|
library. They hide system/transport details behind a simple interface for |
|
24
|
|
|
reading/writing bytes to the connected Nvim instance. |
|
25
|
|
|
|
|
26
|
|
|
This class exposes public methods for interacting with the underlying |
|
27
|
|
|
event loop and delegates implementation-specific work to the following |
|
28
|
|
|
methods, which subclasses are expected to implement: |
|
29
|
|
|
|
|
30
|
|
|
- `_init()`: Implementation-specific initialization |
|
31
|
|
|
- `_connect_tcp(address, port)`: connect to Nvim using tcp/ip |
|
32
|
|
|
- `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or |
|
33
|
|
|
named pipe. |
|
34
|
|
|
- `_connect_stdio()`: Use stdin/stdout as the connection to Nvim |
|
35
|
|
|
- `_connect_child(argv)`: Use the argument vector `argv` to spawn an |
|
36
|
|
|
embedded Nvim that has its stdin/stdout connected to the event loop. |
|
37
|
|
|
- `_start_reading()`: Called after any of _connect_* methods. Can be used |
|
38
|
|
|
to perform any post-connection setup or validation. |
|
39
|
|
|
- `_send(data)`: Send `data`(byte array) to Nvim. The data is only |
|
40
|
|
|
- `_run()`: Runs the event loop until stopped or the connection is closed. |
|
41
|
|
|
calling the following methods when some event happens: |
|
42
|
|
|
actually sent when the event loop is running. |
|
43
|
|
|
- `_on_data(data)`: When Nvim sends some data. |
|
44
|
|
|
- `_on_signal(signum)`: When a signal is received. |
|
45
|
|
|
- `_on_error(message)`: When a non-recoverable error occurs(eg: |
|
46
|
|
|
connection lost) |
|
47
|
|
|
- `_stop()`: Stop the event loop |
|
48
|
|
|
- `_interrupt(data)`: Like `stop()`, but may be called from other threads |
|
49
|
|
|
this. |
|
50
|
|
|
- `_setup_signals(signals)`: Add implementation-specific listeners for |
|
51
|
|
|
for `signals`, which is a list of OS-specific signal numbers. |
|
52
|
|
|
- `_teardown_signals()`: Removes signal listeners set by `_setup_signals` |
|
53
|
|
|
""" |
|
54
|
|
|
|
|
55
|
6 |
|
def __init__(self, transport_type, *args): |
|
56
|
|
|
"""Initialize and connect the event loop instance. |
|
57
|
|
|
|
|
58
|
|
|
The only arguments are the transport type and transport-specific |
|
59
|
|
|
configuration, like this: |
|
60
|
|
|
|
|
61
|
|
|
>>> BaseEventLoop('tcp', '127.0.0.1', 7450) |
|
62
|
|
|
Traceback (most recent call last): |
|
63
|
|
|
... |
|
64
|
|
|
AttributeError: 'BaseEventLoop' object has no attribute '_init' |
|
65
|
|
|
>>> BaseEventLoop('socket', '/tmp/nvim-socket') |
|
66
|
|
|
Traceback (most recent call last): |
|
67
|
|
|
... |
|
68
|
|
|
AttributeError: 'BaseEventLoop' object has no attribute '_init' |
|
69
|
|
|
>>> BaseEventLoop('stdio') |
|
70
|
|
|
Traceback (most recent call last): |
|
71
|
|
|
... |
|
72
|
|
|
AttributeError: 'BaseEventLoop' object has no attribute '_init' |
|
73
|
|
|
>>> BaseEventLoop('child', |
|
74
|
|
|
['nvim', '--embed', '--headless', '-u', 'NONE']) |
|
75
|
|
|
Traceback (most recent call last): |
|
76
|
|
|
... |
|
77
|
|
|
AttributeError: 'BaseEventLoop' object has no attribute '_init' |
|
78
|
|
|
|
|
79
|
|
|
This calls the implementation-specific initialization |
|
80
|
|
|
`_init`, one of the `_connect_*` methods(based on `transport_type`) |
|
81
|
|
|
and `_start_reading()` |
|
82
|
|
|
""" |
|
83
|
6 |
|
self._transport_type = transport_type |
|
84
|
6 |
|
self._signames = dict((k, v) for v, k in signal.__dict__.items() |
|
85
|
|
|
if v.startswith('SIG')) |
|
86
|
6 |
|
self._on_data = None |
|
87
|
6 |
|
self._error = None |
|
88
|
6 |
|
self._init() |
|
89
|
6 |
|
try: |
|
90
|
6 |
|
getattr(self, '_connect_{}'.format(transport_type))(*args) |
|
91
|
|
|
except Exception as e: |
|
92
|
|
|
self.close() |
|
93
|
|
|
raise e |
|
94
|
6 |
|
self._start_reading() |
|
95
|
|
|
|
|
96
|
6 |
|
def connect_tcp(self, address, port): |
|
97
|
|
|
"""Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`.""" |
|
98
|
|
|
info('Connecting to TCP address: %s:%d', address, port) |
|
99
|
|
|
self._connect_tcp(address, port) |
|
100
|
|
|
|
|
101
|
6 |
|
def connect_socket(self, path): |
|
102
|
|
|
"""Connect to socket at `path`. Delegated to `_connect_socket`.""" |
|
103
|
|
|
info('Connecting to %s', path) |
|
104
|
|
|
self._connect_socket(path) |
|
105
|
|
|
|
|
106
|
6 |
|
def connect_stdio(self): |
|
107
|
|
|
"""Connect using stdin/stdout. Delegated to `_connect_stdio`.""" |
|
108
|
|
|
info('Preparing stdin/stdout for streaming data') |
|
109
|
|
|
self._connect_stdio() |
|
110
|
|
|
|
|
111
|
6 |
|
def connect_child(self, argv): |
|
112
|
|
|
"""Connect a new Nvim instance. Delegated to `_connect_child`.""" |
|
113
|
|
|
info('Spawning a new nvim instance') |
|
114
|
|
|
self._connect_child(argv) |
|
115
|
|
|
|
|
116
|
6 |
|
def send(self, data): |
|
117
|
|
|
"""Queue `data` for sending to Nvim.""" |
|
118
|
6 |
|
debug("Sending '%s'", data) |
|
119
|
6 |
|
self._send(data) |
|
120
|
|
|
|
|
121
|
6 |
|
def threadsafe_call(self, fn): |
|
122
|
|
|
"""Call a function in the event loop thread. |
|
123
|
|
|
|
|
124
|
|
|
This is the only safe way to interact with a session from other |
|
125
|
|
|
threads. |
|
126
|
|
|
""" |
|
127
|
6 |
|
self._threadsafe_call(fn) |
|
128
|
|
|
|
|
129
|
6 |
|
def run(self, data_cb): |
|
130
|
|
|
"""Run the event loop.""" |
|
131
|
6 |
|
if self._error: |
|
132
|
|
|
err = self._error |
|
133
|
|
|
if isinstance(self._error, KeyboardInterrupt): |
|
134
|
|
|
# KeyboardInterrupt is not destructive(it may be used in |
|
135
|
|
|
# the REPL). |
|
136
|
|
|
# After throwing KeyboardInterrupt, cleanup the _error field |
|
137
|
|
|
# so the loop may be started again |
|
138
|
|
|
self._error = None |
|
139
|
|
|
raise err |
|
140
|
6 |
|
self._on_data = data_cb |
|
141
|
6 |
|
if threading.current_thread() == main_thread: |
|
142
|
6 |
|
self._setup_signals([signal.SIGINT, signal.SIGTERM]) |
|
143
|
6 |
|
debug('Entering event loop') |
|
144
|
6 |
|
self._run() |
|
145
|
6 |
|
debug('Exited event loop') |
|
146
|
6 |
|
if threading.current_thread() == main_thread: |
|
147
|
6 |
|
self._teardown_signals() |
|
148
|
6 |
|
signal.signal(signal.SIGINT, default_int_handler) |
|
149
|
6 |
|
self._on_data = None |
|
150
|
|
|
|
|
151
|
6 |
|
def stop(self): |
|
152
|
|
|
"""Stop the event loop.""" |
|
153
|
6 |
|
self._stop() |
|
154
|
6 |
|
debug('Stopped event loop') |
|
155
|
|
|
|
|
156
|
6 |
|
def close(self): |
|
157
|
|
|
"""Stop the event loop.""" |
|
158
|
|
|
self._close() |
|
159
|
|
|
debug('Closed event loop') |
|
160
|
|
|
|
|
161
|
6 |
|
def _on_signal(self, signum): |
|
162
|
|
|
msg = 'Received {}'.format(self._signames[signum]) |
|
163
|
|
|
debug(msg) |
|
164
|
|
|
if signum == signal.SIGINT and self._transport_type == 'stdio': |
|
165
|
|
|
# When the transport is stdio, we are probably running as a Nvim |
|
166
|
|
|
# child process. In that case, we don't want to be killed by |
|
167
|
|
|
# ctrl+C |
|
168
|
|
|
return |
|
169
|
|
|
cls = Exception |
|
170
|
|
|
if signum == signal.SIGINT: |
|
171
|
|
|
cls = KeyboardInterrupt |
|
172
|
|
|
self._error = cls(msg) |
|
173
|
|
|
self.stop() |
|
174
|
|
|
|
|
175
|
6 |
|
def _on_error(self, error): |
|
176
|
|
|
debug(error) |
|
177
|
|
|
self._error = IOError(error) |
|
178
|
|
|
self.stop() |
|
179
|
|
|
|
|
180
|
6 |
|
def _on_interrupt(self): |
|
181
|
|
|
self.stop() |
|
182
|
|
|
|