Skip to content

Commit ebdf90f

Browse files
committed
Merge pull request #169 from bfredl/asyncerr
better handling of exceptions in async contexts
2 parents 956f030 + 57747f6 commit ebdf90f

File tree

6 files changed

+75
-24
lines changed

6 files changed

+75
-24
lines changed

neovim/__init__.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
__all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session',
1717
'start_host', 'autocmd', 'command', 'encoding', 'function',
1818
'plugin', 'rpc_export', 'Host', 'DecodeHook', 'Nvim',
19-
'SessionHook', 'shutdown_hook', 'attach')
19+
'SessionHook', 'shutdown_hook', 'attach', 'setup_logging')
2020

2121

2222
def start_host(session=None):
@@ -43,23 +43,8 @@ def start_host(session=None):
4343
if not plugins:
4444
sys.exit('must specify at least one plugin as argument')
4545

46-
logger = logging.getLogger(__name__)
47-
if 'NVIM_PYTHON_LOG_FILE' in os.environ:
48-
logfile = (os.environ['NVIM_PYTHON_LOG_FILE'].strip() +
49-
'_' + str(os.getpid()))
50-
handler = logging.FileHandler(logfile, 'w')
51-
handler.formatter = logging.Formatter(
52-
'%(asctime)s [%(levelname)s @ '
53-
'%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s')
54-
logging.root.addHandler(handler)
55-
level = logging.INFO
56-
if 'NVIM_PYTHON_LOG_LEVEL' in os.environ:
57-
l = getattr(logging,
58-
os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(),
59-
level)
60-
if isinstance(l, int):
61-
level = l
62-
logger.setLevel(level)
46+
setup_logging()
47+
6348
if not session:
6449
session = stdio_session()
6550
host = Host(Nvim.from_session(session))
@@ -95,6 +80,27 @@ def attach(session_type, address=None, port=None, path=None, argv=None):
9580
return Nvim.from_session(session)
9681

9782

83+
def setup_logging():
84+
"""Setup logging according to environment variables."""
85+
logger = logging.getLogger(__name__)
86+
if 'NVIM_PYTHON_LOG_FILE' in os.environ:
87+
logfile = (os.environ['NVIM_PYTHON_LOG_FILE'].strip() +
88+
'_' + str(os.getpid()))
89+
handler = logging.FileHandler(logfile, 'w')
90+
handler.formatter = logging.Formatter(
91+
'%(asctime)s [%(levelname)s @ '
92+
'%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s')
93+
logging.root.addHandler(handler)
94+
level = logging.INFO
95+
if 'NVIM_PYTHON_LOG_LEVEL' in os.environ:
96+
l = getattr(logging,
97+
os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(),
98+
level)
99+
if isinstance(l, int):
100+
level = l
101+
logger.setLevel(level)
102+
103+
98104
# Required for python 2.6
99105
class NullHandler(logging.Handler):
100106
def emit(self, record):

neovim/api/nvim.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import functools
33
import os
44

5+
from traceback import format_exc, format_stack
6+
57
from msgpack import ExtType
68

79
from .buffer import Buffer
@@ -204,9 +206,9 @@ def out_write(self, msg):
204206
"""Print `msg` as a normal message."""
205207
return self._session.request('vim_out_write', msg)
206208

207-
def err_write(self, msg):
209+
def err_write(self, msg, async=False):
208210
"""Print `msg` as an error message."""
209-
return self._session.request('vim_err_write', msg)
211+
return self._session.request('vim_err_write', msg, async=async)
210212

211213
def quit(self, quit_command='qa!'):
212214
"""Send a quit command to Nvim.
@@ -226,6 +228,29 @@ def new_highlight_source(self):
226228
"""Return new src_id for use with Buffer.add_highlight."""
227229
return self.current.buffer.add_highlight("", 0, src_id=0)
228230

231+
def async_call(self, fn, *args, **kwargs):
232+
"""Schedule `fn` to be called by the event loop soon.
233+
234+
This function is thread-safe, and is the only way code not
235+
on the main thread could interact with nvim api objects.
236+
237+
This function can also be called in a synchronous
238+
event handler, just before it returns, to defer execution
239+
that shouldn't block neovim.
240+
"""
241+
call_point = ''.join(format_stack(None, 5)[:-1])
242+
243+
def handler():
244+
try:
245+
fn(*args, **kwargs)
246+
except Exception as err:
247+
msg = ("error caught while executing async callback:\n"
248+
"{!r}\n{}\n \nthe call was requested at\n{}"
249+
.format(err, format_exc(5), call_point))
250+
self.err_write(msg, async=True)
251+
raise
252+
self._session.threadsafe_call(handler)
253+
229254

230255
class Current(object):
231256

neovim/msgpack_rpc/session.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ def __init__(self, async_session):
3131
def threadsafe_call(self, fn, *args, **kwargs):
3232
"""Wrapper around `AsyncSession.threadsafe_call`."""
3333
def handler():
34-
fn(*args, **kwargs)
34+
try:
35+
fn(*args, **kwargs)
36+
except Exception:
37+
warn("error caught while excecuting async callback\n%s\n",
38+
format_exc())
3539

3640
def greenlet_wrapper():
3741
gr = greenlet.greenlet(handler)
@@ -197,6 +201,7 @@ def handler():
197201
except Exception:
198202
warn("error caught while processing notification '%s %s': %s",
199203
name, args, format_exc())
204+
200205
debug('greenlet %s is now dying...', gr)
201206

202207
gr = greenlet.greenlet(handler)

neovim/plugin/host.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import os
77
import os.path
88

9+
from traceback import format_exc
10+
911
from ..api import DecodeHook
1012
from ..compat import IS_PYTHON3, find_module
1113

12-
1314
__all__ = ('Host')
1415

1516
logger = logging.getLogger(__name__)
@@ -76,7 +77,13 @@ def _on_notification(self, name, args):
7677
return
7778

7879
debug('calling notification handler for "%s", args: "%s"', name, args)
79-
handler(*args)
80+
try:
81+
handler(*args)
82+
except Exception as err:
83+
msg = ("error caught in async handler '{} {}':\n{!r}\n{}"
84+
.format(name, args, err, format_exc(5)))
85+
self.nvim.err_write(msg, async=True)
86+
raise
8087

8188
def _load(self, plugins):
8289
for path in plugins:

test/test_common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from nose.tools import eq_ as eq
88

9+
neovim.setup_logging()
910

1011
if 'NVIM_CHILD_ARGV' in os.environ:
1112
vim = neovim.attach('child', argv=json.loads(os.environ['NVIM_CHILD_ARGV']))
@@ -17,7 +18,6 @@
1718
# with Python2
1819
vim = vim.with_hook(neovim.DecodeHook())
1920

20-
2121
cleanup_func = ''':function BeforeEachTest()
2222
set all&
2323
redir => groups

test/test_concurrency.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,11 @@ def test_interrupt_from_another_thread():
99
timer = Timer(0.5, lambda: session.threadsafe_call(lambda: session.stop()))
1010
timer.start()
1111
eq(vim.session.next_message(), None)
12+
13+
14+
@with_setup(setup=cleanup)
15+
def test_exception_in_threadsafe_call():
16+
# an exception in a threadsafe_call shouldn't crash the entire host
17+
vim.session.threadsafe_call(lambda: [vim.eval("3"), undefined_variable])
18+
vim.session.threadsafe_call(lambda: vim.session.stop())
19+
vim.session.run(None, None)

0 commit comments

Comments
 (0)