diff --git a/mitogen/lxc.py b/mitogen/lxc.py index 71b12221..cd85be4f 100644 --- a/mitogen/lxc.py +++ b/mitogen/lxc.py @@ -48,6 +48,12 @@ class Stream(mitogen.parent.Stream): container = None lxc_attach_path = 'lxc-attach' + eof_error_hint = ( + 'Note: many versions of LXC do not report program execution failure ' + 'meaningfully. Please check the host logs (/var/log) for more ' + 'information.' + ) + def construct(self, container, lxc_attach_path=None, **kwargs): super(Stream, self).construct(**kwargs) self.container = container diff --git a/mitogen/lxd.py b/mitogen/lxd.py index 9e6702f4..a28e1aaa 100644 --- a/mitogen/lxd.py +++ b/mitogen/lxd.py @@ -49,6 +49,12 @@ class Stream(mitogen.parent.Stream): lxc_path = 'lxc' python_path = 'python' + eof_error_hint = ( + 'Note: many versions of LXC do not report program execution failure ' + 'meaningfully. Please check the host logs (/var/log) for more ' + 'information.' + ) + def construct(self, container, lxc_path=None, **kwargs): super(Stream, self).construct(**kwargs) self.container = container diff --git a/mitogen/parent.py b/mitogen/parent.py index 8f7d68ea..4549d877 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -407,6 +407,8 @@ def write_all(fd, s, deadline=None): :raises mitogen.core.TimeoutError: Bytestring could not be written entirely before deadline was exceeded. + :raises mitogen.parent.EofError: + Stream indicated EOF, suggesting the child process has exitted. :raises mitogen.core.StreamError: File descriptor was disconnected before write could complete. """ @@ -430,7 +432,7 @@ def write_all(fd, s, deadline=None): for fd in poller.poll(timeout): n, disconnected = mitogen.core.io_op(os.write, fd, window) if disconnected: - raise mitogen.core.StreamError('EOF on stream during write') + raise EofError('EOF on stream during write') written += n finally: @@ -449,6 +451,8 @@ def iter_read(fds, deadline=None): :raises mitogen.core.TimeoutError: Attempt to read beyond deadline. + :raises mitogen.parent.EofError: + All streams indicated EOF, suggesting the child process has exitted. :raises mitogen.core.StreamError: Attempt to read past end of file. """ @@ -478,10 +482,9 @@ def iter_read(fds, deadline=None): poller.close() if not poller.readers: - raise mitogen.core.StreamError( - u'EOF on stream; last 300 bytes received: %r' % - (b('').join(bits)[-300:].decode('latin1'),) - ) + raise EofError(u'EOF on stream; last 300 bytes received: %r' % + (b('').join(bits)[-300:].decode('latin1'),)) + raise mitogen.core.TimeoutError('read timed out') @@ -501,6 +504,8 @@ def discard_until(fd, s, deadline): :raises mitogen.core.TimeoutError: Attempt to read beyond deadline. + :raises mitogen.parent.EofError: + All streams indicated EOF, suggesting the child process has exitted. :raises mitogen.core.StreamError: Attempt to read past end of file. """ @@ -607,6 +612,14 @@ def wstatus_to_str(status): return 'unknown wait status (%d)' % (status,) +class EofError(mitogen.core.StreamError): + """ + Raised by :func:`iter_read` and :func:`write_all` when EOF is detected by + the child process. + """ + # inherits from StreamError to maintain compatibility. + + class Argv(object): """ Wrapper to defer argv formatting when debug logging is disabled. @@ -1105,6 +1118,16 @@ class Stream(mitogen.core.Stream): msg = 'Child start failed: %s. Command was: %s' % (e, Argv(args)) raise mitogen.core.StreamError(msg) + eof_error_hint = None + + def _adorn_eof_error(self, e): + """ + Used by subclasses to provide additional information in the case of a + failed connection. + """ + if self.eof_error_hint: + e.args = ('%s\n\n%s' % (e.args[0], self.eof_error_hint),) + def connect(self): LOG.debug('%r.connect()', self) self.pid, fd, extra_fd = self.start_child() @@ -1116,6 +1139,10 @@ class Stream(mitogen.core.Stream): try: self._connect_bootstrap(extra_fd) + except EofError: + e = sys.exc_info()[1] + self._adorn_eof_error(e) + raise except Exception: self._reap_child() raise diff --git a/tests/lxc_test.py b/tests/lxc_test.py index 3168aad2..5d8e14d8 100644 --- a/tests/lxc_test.py +++ b/tests/lxc_test.py @@ -1,6 +1,7 @@ import os import mitogen +import mitogen.lxc import unittest2 @@ -11,19 +12,29 @@ def has_subseq(seq, subseq): return any(seq[x:x+len(subseq)] == subseq for x in range(0, len(seq))) -class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): + lxc_attach_path = testlib.data_path('stubs/lxc-attach.py') + def test_okay(self): - lxc_attach_path = testlib.data_path('stubs/lxc-attach.py') context = self.router.lxc( container='container_name', - lxc_attach_path=lxc_attach_path, + lxc_attach_path=self.lxc_attach_path, ) argv = eval(context.call(os.getenv, 'ORIGINAL_ARGV')) - self.assertEquals(argv[0], lxc_attach_path) + self.assertEquals(argv[0], self.lxc_attach_path) self.assertTrue('--clear-env' in argv) self.assertTrue(has_subseq(argv, ['--name', 'container_name'])) + def test_eof(self): + e = self.assertRaises(mitogen.parent.EofError, + lambda: self.router.lxc( + container='container_name', + lxc_attach_path='true', + ) + ) + self.assertTrue(str(e).endswith(mitogen.lxc.Stream.eof_error_hint)) + if __name__ == '__main__': unittest2.main() diff --git a/tests/lxd_test.py b/tests/lxd_test.py index 41e9df15..cae1172c 100644 --- a/tests/lxd_test.py +++ b/tests/lxd_test.py @@ -1,13 +1,15 @@ import os import mitogen +import mitogen.lxd +import mitogen.parent import unittest2 import testlib -class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): def test_okay(self): lxc_path = testlib.data_path('stubs/lxc.py') context = self.router.lxd( @@ -21,6 +23,15 @@ class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): self.assertEquals(argv[2], '--mode=noninteractive') self.assertEquals(argv[3], 'container_name') + def test_eof(self): + e = self.assertRaises(mitogen.parent.EofError, + lambda: self.router.lxd( + container='container_name', + lxc_path='true', + ) + ) + self.assertTrue(str(e).endswith(mitogen.lxd.Stream.eof_error_hint)) + if __name__ == '__main__': unittest2.main()