import errno import fcntl import os import signal import subprocess import sys import tempfile import time import mock import unittest2 import testlib from testlib import Popen__terminate import mitogen.parent try: file except NameError: from io import FileIO as file def wait_for_child(pid, timeout=1.0): deadline = time.time() + timeout while timeout < time.time(): try: target_pid, status = os.waitpid(pid, os.WNOHANG) if target_pid == pid: return except OSError: e = sys.exc_info()[1] if e.args[0] == errno.ECHILD: return time.sleep(0.05) assert False, "wait_for_child() timed out" @mitogen.core.takes_econtext def call_func_in_sibling(ctx, econtext, sync_sender): recv = ctx.call_async(time.sleep, 99999) sync_sender.send(None) recv.get().unpickle() def wait_for_empty_output_queue(sync_recv, context): # wait for sender to submit their RPC. Since the RPC is sent first, the # message sent to this sender cannot arrive until we've routed the RPC. sync_recv.get() router = context.router broker = router.broker while True: # Now wait for the RPC to exit the output queue. stream = router.stream_by_id(context.context_id) if broker.defer_sync(lambda: stream.protocol.pending_bytes()) == 0: return time.sleep(0.1) class GetDefaultRemoteNameTest(testlib.TestCase): func = staticmethod(mitogen.parent.get_default_remote_name) @mock.patch('os.getpid') @mock.patch('getpass.getuser') @mock.patch('socket.gethostname') def test_slashes(self, mock_gethostname, mock_getuser, mock_getpid): # Ensure slashes appearing in the remote name are replaced with # underscores. mock_gethostname.return_value = 'box' mock_getuser.return_value = 'ECORP\\Administrator' mock_getpid.return_value = 123 self.assertEquals("ECORP_Administrator@box:123", self.func()) class ReturncodeToStrTest(testlib.TestCase): func = staticmethod(mitogen.parent.returncode_to_str) def test_return_zero(self): self.assertEquals(self.func(0), 'exited with return code 0') def test_return_one(self): self.assertEquals(self.func(1), 'exited with return code 1') def test_sigkill(self): self.assertEquals(self.func(-signal.SIGKILL), 'exited due to signal %s (SIGKILL)' % (int(signal.SIGKILL),) ) # can't test SIGSTOP without POSIX sessions rabbithole class ReapChildTest(testlib.RouterMixin, testlib.TestCase): def test_connect_timeout(self): # Ensure the child process is reaped if the connection times out. options = mitogen.parent.Options( old_router=self.router, max_message_size=self.router.max_message_size, python_path=testlib.data_path('python_never_responds.py'), connect_timeout=0.5, ) conn = mitogen.parent.Connection(options, router=self.router) self.assertRaises(mitogen.core.TimeoutError, lambda: conn.connect(context=mitogen.core.Context(None, 1234)) ) wait_for_child(conn.proc.pid) e = self.assertRaises(OSError, lambda: os.kill(conn.proc.pid, 0) ) self.assertEquals(e.args[0], errno.ESRCH) class StreamErrorTest(testlib.RouterMixin, testlib.TestCase): def test_direct_eof(self): e = self.assertRaises(mitogen.core.StreamError, lambda: self.router.local( python_path='true', connect_timeout=3, ) ) prefix = mitogen.parent.Connection.eof_error_msg self.assertTrue(e.args[0].startswith(prefix)) def test_via_eof(self): # Verify FD leakage does not keep failed process open. local = self.router.local() e = self.assertRaises(mitogen.core.StreamError, lambda: self.router.local( via=local, python_path='echo', connect_timeout=3, ) ) expect = mitogen.parent.Connection.eof_error_msg self.assertTrue(expect in e.args[0]) def test_direct_enoent(self): e = self.assertRaises(mitogen.core.StreamError, lambda: self.router.local( python_path='derp', connect_timeout=3, ) ) prefix = 'Child start failed: [Errno 2] No such file or directory' self.assertTrue(e.args[0].startswith(prefix)) def test_via_enoent(self): local = self.router.local() e = self.assertRaises(mitogen.core.StreamError, lambda: self.router.local( via=local, python_path='derp', connect_timeout=3, ) ) s = 'Child start failed: [Errno 2] No such file or directory' self.assertTrue(s in e.args[0]) class ContextTest(testlib.RouterMixin, testlib.TestCase): def test_context_shutdown(self): local = self.router.local() pid = local.call(os.getpid) local.shutdown(wait=True) wait_for_child(pid) self.assertRaises(OSError, lambda: os.kill(pid, 0)) class OpenPtyTest(testlib.TestCase): func = staticmethod(mitogen.parent.openpty) def test_pty_returned(self): master_fp, slave_fp = self.func() try: self.assertTrue(master_fp.isatty()) self.assertTrue(isinstance(master_fp, file)) self.assertTrue(slave_fp.isatty()) self.assertTrue(isinstance(slave_fp, file)) finally: master_fp.close() slave_fp.close() @mock.patch('os.openpty') def test_max_reached(self, openpty): openpty.side_effect = OSError(errno.ENXIO) e = self.assertRaises(mitogen.core.StreamError, lambda: self.func()) msg = mitogen.parent.OPENPTY_MSG % (openpty.side_effect,) self.assertEquals(e.args[0], msg) @unittest2.skipIf(condition=(os.uname()[0] != 'Linux'), reason='Fallback only supported on Linux') @mock.patch('os.openpty') def test_broken_linux_fallback(self, openpty): openpty.side_effect = OSError(errno.EPERM) master_fp, slave_fp = self.func() try: st = os.fstat(master_fp.fileno()) self.assertEquals(5, os.major(st.st_rdev)) flags = fcntl.fcntl(master_fp.fileno(), fcntl.F_GETFL) self.assertTrue(flags & os.O_RDWR) st = os.fstat(slave_fp.fileno()) self.assertEquals(136, os.major(st.st_rdev)) flags = fcntl.fcntl(slave_fp.fileno(), fcntl.F_GETFL) self.assertTrue(flags & os.O_RDWR) finally: master_fp.close() slave_fp.close() class TtyCreateChildTest(testlib.TestCase): func = staticmethod(mitogen.parent.tty_create_child) def test_dev_tty_open_succeeds(self): # In the early days of UNIX, a process that lacked a controlling TTY # would acquire one simply by opening an existing TTY. Linux and OS X # continue to follow this behaviour, however at least FreeBSD moved to # requiring an explicit ioctl(). Linux supports it, but we don't yet # use it there and anyway the behaviour will never change, so no point # in fixing things that aren't broken. Below we test that # getpass-loving apps like sudo and ssh get our slave PTY when they # attempt to open /dev/tty, which is what they both do on attempting to # read a password. tf = tempfile.NamedTemporaryFile() try: proc = self.func([ 'bash', '-c', 'exec 2>%s; echo hi > /dev/tty' % (tf.name,) ]) deadline = time.time() + 5.0 mitogen.core.set_block(proc.stdin.fileno()) # read(3) below due to https://bugs.python.org/issue37696 self.assertEquals(mitogen.core.b('hi\n'), proc.stdin.read(3)) waited_pid, status = os.waitpid(proc.pid, 0) self.assertEquals(proc.pid, waited_pid) self.assertEquals(0, status) self.assertEquals(mitogen.core.b(''), tf.read()) proc.stdout.close() finally: tf.close() class DisconnectTest(testlib.RouterMixin, testlib.TestCase): def test_child_disconnected(self): # Easy mode: process notices its own directly connected child is # disconnected. c1 = self.router.local() recv = c1.call_async(time.sleep, 9999) c1.shutdown(wait=True) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) self.assertEquals(e.args[0], self.router.respondent_disconnect_msg) def test_indirect_child_disconnected(self): # Achievement unlocked: process notices an indirectly connected child # is disconnected. c1 = self.router.local() c2 = self.router.local(via=c1) recv = c2.call_async(time.sleep, 9999) c2.shutdown(wait=True) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) self.assertEquals(e.args[0], self.router.respondent_disconnect_msg) def test_indirect_child_intermediary_disconnected(self): # Battlefield promotion: process notices indirect child disconnected # due to an intermediary child disconnecting. c1 = self.router.local() c2 = self.router.local(via=c1) recv = c2.call_async(time.sleep, 9999) c1.shutdown(wait=True) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) self.assertEquals(e.args[0], self.router.respondent_disconnect_msg) def test_near_sibling_disconnected(self): # Hard mode: child notices sibling connected to same parent has # disconnected. c1 = self.router.local() c2 = self.router.local() # Let c1 call functions in c2. self.router.stream_by_id(c1.context_id).protocol.auth_id = mitogen.context_id c1.call(mitogen.parent.upgrade_router) sync_recv = mitogen.core.Receiver(self.router) recv = c1.call_async(call_func_in_sibling, c2, sync_sender=sync_recv.to_sender()) wait_for_empty_output_queue(sync_recv, c2) c2.shutdown(wait=True) e = self.assertRaises(mitogen.core.CallError, lambda: recv.get().unpickle()) s = 'mitogen.core.ChannelError: ' + self.router.respondent_disconnect_msg self.assertTrue(e.args[0].startswith(s), str(e)) def test_far_sibling_disconnected(self): # God mode: child of child notices child of child of parent has # disconnected. c1 = self.router.local(name='c1') c11 = self.router.local(name='c11', via=c1) c2 = self.router.local(name='c2') c22 = self.router.local(name='c22', via=c2) # Let c1 call functions in c2. self.router.stream_by_id(c1.context_id).protocol.auth_id = mitogen.context_id c11.call(mitogen.parent.upgrade_router) sync_recv = mitogen.core.Receiver(self.router) recv = c11.call_async(call_func_in_sibling, c22, sync_sender=sync_recv.to_sender()) wait_for_empty_output_queue(sync_recv, c22) c22.shutdown(wait=True) e = self.assertRaises(mitogen.core.CallError, lambda: recv.get().unpickle()) s = 'mitogen.core.ChannelError: ' + self.router.respondent_disconnect_msg self.assertTrue(e.args[0].startswith(s)) if __name__ == '__main__': unittest2.main()