1 """Common code for event loop implementations."""
7 logger = logging.getLogger(__name__)
8 debug, info, warn = (logger.debug, logger.info, logger.warning,)
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 default_int_handler = signal.getsignal(signal.SIGINT)
15 main_thread = threading.current_thread()
18 class BaseEventLoop(object):
20 """Abstract base class for all event loops.
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.
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:
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
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:
47 - `_stop()`: Stop the event loop
48 - `_interrupt(data)`: Like `stop()`, but may be called from other threads
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`
55 def __init__(self, transport_type, *args):
56 """Initialize and connect the event loop instance.
58 The only arguments are the transport type and transport-specific
59 configuration, like this:
61 >>> BaseEventLoop('tcp', '127.0.0.1', 7450)
62 Traceback (most recent call last):
64 AttributeError: 'BaseEventLoop' object has no attribute '_init'
65 >>> BaseEventLoop('socket', '/tmp/nvim-socket')
66 Traceback (most recent call last):
68 AttributeError: 'BaseEventLoop' object has no attribute '_init'
69 >>> BaseEventLoop('stdio')
70 Traceback (most recent call last):
72 AttributeError: 'BaseEventLoop' object has no attribute '_init'
73 >>> BaseEventLoop('child',
74 ['nvim', '--embed', '--headless', '-u', 'NONE'])
75 Traceback (most recent call last):
77 AttributeError: 'BaseEventLoop' object has no attribute '_init'
79 This calls the implementation-specific initialization
80 `_init`, one of the `_connect_*` methods(based on `transport_type`)
81 and `_start_reading()`
83 self._transport_type = transport_type
84 self._signames = dict((k, v) for v, k in signal.__dict__.items()
85 if v.startswith('SIG'))
90 getattr(self, '_connect_{}'.format(transport_type))(*args)
91 except Exception as e:
96 def connect_tcp(self, address, port):
97 """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`."""
98 pass # replaces next logging statement
99 #info('Connecting to TCP address: %s:%d', address, port)
100 self._connect_tcp(address, port)
102 def connect_socket(self, path):
103 """Connect to socket at `path`. Delegated to `_connect_socket`."""
104 pass # replaces next logging statement
105 #info('Connecting to %s', path)
106 self._connect_socket(path)
108 def connect_stdio(self):
109 """Connect using stdin/stdout. Delegated to `_connect_stdio`."""
110 pass # replaces next logging statement
111 #info('Preparing stdin/stdout for streaming data')
112 self._connect_stdio()
114 def connect_child(self, argv):
115 """Connect a new Nvim instance. Delegated to `_connect_child`."""
116 pass # replaces next logging statement
117 #info('Spawning a new nvim instance')
118 self._connect_child(argv)
120 def send(self, data):
121 """Queue `data` for sending to Nvim."""
122 pass # replaces next logging statement
123 #debug("Sending '%s'", data)
126 def threadsafe_call(self, fn):
127 """Call a function in the event loop thread.
129 This is the only safe way to interact with a session from other
132 self._threadsafe_call(fn)
134 def run(self, data_cb):
135 """Run the event loop."""
138 if isinstance(self._error, KeyboardInterrupt):
139 # KeyboardInterrupt is not destructive(it may be used in
141 # After throwing KeyboardInterrupt, cleanup the _error field
142 # so the loop may be started again
145 self._on_data = data_cb
146 if threading.current_thread() == main_thread:
147 self._setup_signals([signal.SIGINT, signal.SIGTERM])
148 pass # replaces next logging statement
149 #debug('Entering event loop')
151 pass # replaces next logging statement
152 #debug('Exited event loop')
153 if threading.current_thread() == main_thread:
154 self._teardown_signals()
155 signal.signal(signal.SIGINT, default_int_handler)
159 """Stop the event loop."""
161 pass # replaces next logging statement
162 #debug('Stopped event loop')
165 """Stop the event loop."""
167 pass # replaces next logging statement
168 #debug('Closed event loop')
170 def _on_signal(self, signum):
171 msg = 'Received {}'.format(self._signames[signum])
172 pass # replaces next logging statement
174 if signum == signal.SIGINT and self._transport_type == 'stdio':
175 # When the transport is stdio, we are probably running as a Nvim
176 # child process. In that case, we don't want to be killed by
180 if signum == signal.SIGINT:
181 cls = KeyboardInterrupt
182 self._error = cls(msg)
185 def _on_error(self, error):
186 pass # replaces next logging statement
188 self._error = OSError(error)
191 def _on_interrupt(self):