Skip to content

Commit ef3d029

Browse files
authored
fix: improve EOFError exception when remote nvim crashes #589
Problem: When the remote Nvim instance is aborted or terminates, the pynvim client will face an exception `OSError: EOF`. It is not very clear what is wrong and why EOF is received. Solution: This happens when the remote the nvim process terminates unexpectedly (or a tcp/file socket is broken or gets disconnected). We can provide a bit more detailed information about why the asyncio session is stopping, through asyncio Protocol. An `EOFError` will be raised instead of OSError. For example, during pynvim's unit tests we may see: ``` EOFError: process_exited: pid = 40000, return_code = -6 ``` which means that the Nvim subprocess (pid = 40000) exited unexpectedly after getting SIGABRT (signal 6). Other error messages (different signals such as SIGSEGV/segfault) or connection lost (when connected through socket, etc.) are also possible.
1 parent 581d7a8 commit ef3d029

File tree

2 files changed

+33
-9
lines changed

2 files changed

+33
-9
lines changed

pynvim/msgpack_rpc/event_loop/asyncio.py

+21-7
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ def __init__(self, on_data, on_error):
4747
@override
4848
def connection_made(self, transport):
4949
"""Used to signal `asyncio.Protocol` of a successful connection."""
50-
del transport # no-op
50+
self._transport = transport
5151

5252
@override
5353
def connection_lost(self, exc: Optional[Exception]) -> None:
5454
"""Used to signal `asyncio.Protocol` of a lost connection."""
55-
debug(f"connection_lost: exc = {exc}")
56-
self._on_error(exc if exc else EOFError())
55+
warn(f"connection_lost: exc = {exc}")
56+
57+
self._on_error(exc if exc else EOFError("connection_lost"))
5758

5859
@override
5960
def data_received(self, data: bytes) -> None:
@@ -63,11 +64,19 @@ def data_received(self, data: bytes) -> None:
6364
@override
6465
def pipe_connection_lost(self, fd: int, exc: Optional[Exception]) -> None:
6566
"""Used to signal `asyncio.SubprocessProtocol` of a lost connection."""
66-
debug("pipe_connection_lost: fd = %s, exc = %s", fd, exc)
67+
68+
assert isinstance(self._transport, asyncio.SubprocessTransport)
69+
debug_info = {'fd': fd, 'exc': exc, 'pid': self._transport.get_pid()}
70+
warn(f"pipe_connection_lost {debug_info}")
71+
6772
if os.name == 'nt' and fd == 2: # stderr
6873
# On windows, ignore piped stderr being closed immediately (#505)
6974
return
70-
self._on_error(exc if exc else EOFError())
75+
76+
# pipe_connection_lost() *may* be called before process_exited() is
77+
# called, when a Nvim subprocess crashes (SIGABRT). Do not handle
78+
# errors here, as errors will be handled somewhere else
79+
# self._on_error(exc if exc else EOFError("pipe_connection_lost"))
7180

7281
@override
7382
def pipe_data_received(self, fd, data):
@@ -81,8 +90,13 @@ def pipe_data_received(self, fd, data):
8190
@override
8291
def process_exited(self) -> None:
8392
"""Used to signal `asyncio.SubprocessProtocol` when the child exits."""
84-
debug("process_exited")
85-
self._on_error(EOFError())
93+
assert isinstance(self._transport, asyncio.SubprocessTransport)
94+
pid = self._transport.get_pid()
95+
return_code = self._transport.get_returncode()
96+
97+
warn("process_exited, pid = %s, return_code = %s", pid, return_code)
98+
err = EOFError(f"process_exited: pid = {pid}, return_code = {return_code}")
99+
self._on_error(err)
86100

87101

88102
class AsyncioEventLoop(BaseEventLoop):

pynvim/msgpack_rpc/event_loop/base.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ def run(self, data_cb: Callable[[bytes], None]) -> None:
194194
signal.signal(signal.SIGINT, default_int_handler)
195195
self._on_data = None
196196

197+
# eventloop was stopped due to an error, re-raise it
198+
# (e.g. connection lost when subprocess nvim dies)
199+
if self._error:
200+
# Note: traceback is not preserved and attached for some reason,
201+
# should be somewhere from msgpack_rpc.event_loop.asyncio.Protocol
202+
raise self._error
203+
197204
@abstractmethod
198205
def _run(self) -> None:
199206
raise NotImplementedError()
@@ -234,8 +241,11 @@ def _on_signal(self, signum: signal.Signals) -> None:
234241
self.stop()
235242

236243
def _on_error(self, exc: Exception) -> None:
237-
debug(str(exc))
238-
self._error = exc
244+
warn('on_error: %s', repr(exc))
245+
if self._error is None:
246+
# ignore subsequent exceptions, it's enough to raise only
247+
# the first exception arrived
248+
self._error = exc
239249
self.stop()
240250

241251
def _on_interrupt(self) -> None:

0 commit comments

Comments
 (0)