From 72da291b243813b0fad604daec605e1e84848990 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 16:53:06 +0100 Subject: [PATCH 001/662] docs: fix up incomplete Temporary Files section. --- docs/ansible.rst | 57 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/docs/ansible.rst b/docs/ansible.rst index 263d2b10..c6e9d553 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -427,11 +427,9 @@ specific variables with a particular linefeed style. Temporary Files ~~~~~~~~~~~~~~~ -Temporary file handling in Ansible is incredibly tricky business, and the exact -behaviour varies across major releases. - -Ansible creates a variety of temporary files and directories depending on its -operating mode. +Temporary file handling in Ansible is tricky, and the precise behaviour varies +across major versions. A variety of temporary files and directories are +created, depending on the operating mode: In the best case when pipelining is enabled and no temporary uploads are required, for each task Ansible will create one directory below a @@ -470,19 +468,19 @@ In summary, for each task Ansible may create one or more of: Mitogen for Ansible ^^^^^^^^^^^^^^^^^^^ -Temporary h -Temporary directory handling is fiddly and varies across major Ansible -releases. - +As Mitogen can execute new-style modules from RAM, and transfer files to target +user accounts without first writing an intermediary file in any separate login +account, handling is relatively simplified. Temporary directories must exist to maintain compatibility with Ansible, as many modules introspect :data:`sys.argv` to find a directory where they may write files, however only one directory exists for the lifetime of each -interpreter, its location is consistent for each target account, and it is -always privately owned by that account. +interpreter, its location is consistent for each account, and it is always +privately owned by that account. -The paths below are tried until one is found that is writeable and lives on a -filesystem with ``noexec`` disabled: +During startup, the persistent remote interpreter tries the paths below until +one is found that is writeable and lives on a filesystem with ``noexec`` +disabled: 1. ``$variable`` and tilde-expanded ``remote_tmp`` setting from ``ansible.cfg`` @@ -496,10 +494,35 @@ filesystem with ``noexec`` disabled: 8. ``/usr/tmp`` 9. Current working directory -The directory is created once at startup, and subdirectories are automatically -created and destroyed for every new task. Management of subdirectories happens -on the controller, but management of the parent directory happens entirely on -the target. +The directory is created at startup and recursively destroyed during interpeter +shutdown. Subdirectories are automatically created and destroyed by the +controller for each task that requires them. + + +Round-trip Avoidance +^^^^^^^^^^^^^^^^^^^^ + +Mitogen avoids many round-trips due to temporary file handling that are present +in regular Ansible: + +* During task startup, it is not necessary to wait until the target has + succeeded in creating a temporary directory. Instead, any failed attempt to + create the directory will cause any subsequent RPC belonging to the same task + to fail with the error that occurred. + +* As temporary directories are privately owned by the target user account, + operations relating to modifying the directory to support cross-account + access are avoided. + +* An explicit work-around is included to avoid the `copy` and `template` + actions needlessly triggering a round-trip to set their temporary file as + executable. + +* During task shutdown, it is not necessary to wait to learn if the target has + succeeded in deleting a temporary directory, since any error that may occur + can is logged asynchronously via the logging framework, and the persistent + remote interpreter arranges for all subdirectories to be destroyed during + interpreter shutdown. .. _ansible_process_env: From d7d40f112336c8df88536e03b4b4a1b0bb0649e4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 22:16:09 +0100 Subject: [PATCH 002/662] issue #76: reduce Context duplication during unpickling When unpickling a context, arrange for there to be a single instance representing that context, managed by the corresponding router. This context_by_id() was already in use by parent.py, it just needs to move down. This to eventually reach the point where a single Context exists that needs 'disconnect' fired on it, so all sleeping receivers are definitely woken. --- mitogen/core.py | 11 ++++++++++- mitogen/parent.py | 9 --------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index dadf0924..d6134694 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1279,7 +1279,7 @@ def _unpickle_context(router, context_id, name): (isinstance(name, UnicodeType) and len(name) < 100)) ): raise TypeError('cannot unpickle Context: bad input') - return router.context_class(router, context_id, name) + return router.context_by_id(context_id, name=name) class Poller(object): @@ -1732,6 +1732,15 @@ class Router(object): _, (_, func, _) = self._handle_map.popitem() func(Message.dead()) + def context_by_id(self, context_id, via_id=None, create=True, name=None): + context = self._context_by_id.get(context_id) + if create and not context: + context = self.context_class(self, context_id, name=name) + if via_id is not None: + context.via = self.context_by_id(via_id) + self._context_by_id[context_id] = context + return context + def register(self, context, stream): _v and LOG.debug('register(%r, %r)', context, stream) self._stream_by_id[context.context_id] = stream diff --git a/mitogen/parent.py b/mitogen/parent.py index a57ca20b..d8a1a633 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1581,15 +1581,6 @@ class Router(mitogen.core.Router): def allocate_id(self): return self.id_allocator.allocate() - def context_by_id(self, context_id, via_id=None, create=True): - context = self._context_by_id.get(context_id) - if create and not context: - context = self.context_class(self, context_id) - if via_id is not None: - context.via = self.context_by_id(via_id) - self._context_by_id[context_id] = context - return context - connection_timeout_msg = u"Connection timed out." def _connect(self, klass, name=None, **kwargs): From babe3eec319bb72cbb36cf0a5f8a2ce6aa320509 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 23:07:49 +0100 Subject: [PATCH 003/662] issue #76: record egress context IDs Used in a subsequent change to broadcast DEL_ROUTE to potentially interested children. --- mitogen/core.py | 6 ++++++ tests/router_test.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/mitogen/core.py b/mitogen/core.py index d6134694..9c4063fa 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1085,6 +1085,9 @@ class Stream(BasicStream): self._output_buf = collections.deque() self._input_buf_len = 0 self._output_buf_len = 0 + #: Routing records the dst_id of every message arriving from this + #: stream. Any arriving DEL_ROUTE is rebroadcast for any such ID. + self.egress_ids = set() def construct(self): pass @@ -1838,6 +1841,9 @@ class Router(object): if in_stream.auth_id is not None: msg.auth_id = in_stream.auth_id + # Maintain a set of IDs the source ever communicated with. + in_stream.egress_ids.add(msg.dst_id) + if msg.dst_id == mitogen.context_id: return self._invoke(msg, in_stream) diff --git a/tests/router_test.py b/tests/router_test.py index 68474e00..d0e4f539 100644 --- a/tests/router_test.py +++ b/tests/router_test.py @@ -314,5 +314,16 @@ class UnidirectionalTest(testlib.RouterMixin, testlib.TestCase): self.assertTrue('policy refused message: ' in logs.stop()) +class EgressIdsTest(testlib.RouterMixin, testlib.TestCase): + def test_egress_ids_populated(self): + # Ensure Stream.egress_ids is populated on message reception. + c1 = self.router.fork() + stream = self.router.stream_by_id(c1.context_id) + self.assertEquals(set(), stream.egress_ids) + + c1.call(time.sleep, 0) + self.assertEquals(set([mitogen.context_id]), stream.egress_ids) + + if __name__ == '__main__': unittest2.main() From b9bafb78afab62e2d6e23effc77ca43e5e9809bc Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 23:08:35 +0100 Subject: [PATCH 004/662] issue #76: add stub DEL_ROUTE handler to core.py. This handler knows how to fire 'disconnect' event on reception of a DEL_ROUTE, and nothing more. --- mitogen/core.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mitogen/core.py b/mitogen/core.py index 9c4063fa..5d509152 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1719,10 +1719,27 @@ class Router(object): self._last_handle = itertools.count(1000) #: handle -> (persistent?, func(msg)) self._handle_map = {} + self.add_handler(self._on_del_route, DEL_ROUTE) def __repr__(self): return 'Router(%r)' % (self.broker,) + def _on_del_route(self, msg): + """ + Stub DEL_ROUTE handler; fires 'disconnect' events on the corresponding + member of :attr:`_context_by_id`. This handler is replaced by + :class:`mitogen.parent.RouteMonitor` in an upgraded context. + """ + LOG.error('%r._on_del_route() %r', self, msg) + if not msg.is_dead: + target_id_s, _, name = msg.data.partition(b(':')) + target_id = int(target_id_s, 10) + if target_id not in self._context_by_id: + LOG.debug('DEL_ROUTE for unknown ID %r: %r', target_id, msg) + return + + fire(self._context_by_id[target_id], 'disconnect') + def on_stream_disconnect(self, stream): for context in self._context_by_id.values(): stream_ = self._stream_by_id.get(context.context_id) From 431051f69b9cde0a8980298a0e87cc13ee23c795 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 23:09:33 +0100 Subject: [PATCH 005/662] issue #76: parent: broadcast DEL_ROUTE to interested parties Now rather than simply propagate DEL_ROUTE upwards towards the parent, we broadcast it downward to any stream that ever sent a message toward any of the routes that have just become disconnected. --- mitogen/parent.py | 58 +++++++++++++++++++++++++++++++------------- tests/parent_test.py | 57 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 17 deletions(-) diff --git a/mitogen/parent.py b/mitogen/parent.py index d8a1a633..f3e39ebe 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -69,6 +69,7 @@ from mitogen.core import IOLOG IS_WSL = 'Microsoft' in os.uname()[2] +itervalues = getattr(dict, 'itervalues', dict.values) if mitogen.core.PY3: xrange = range @@ -543,6 +544,7 @@ def _upgrade_broker(broker): len(old.readers), len(old.writers)) +@mitogen.core.takes_econtext def upgrade_router(econtext): if not isinstance(econtext.router, Router): # TODO econtext.broker.defer(_upgrade_broker, econtext.broker) @@ -911,9 +913,6 @@ class Stream(mitogen.core.Stream): def __init__(self, *args, **kwargs): super(Stream, self).__init__(*args, **kwargs) self.sent_modules = set(['mitogen', 'mitogen.core']) - #: List of contexts reachable via this stream; used to cleanup routes - #: during disconnection. - self.routes = set([self.remote_id]) def construct(self, max_message_size, remote_name=None, python_path=None, debug=False, connect_timeout=None, profiling=False, @@ -1428,28 +1427,45 @@ class RouteMonitor(object): persist=True, policy=is_immediate_child, ) + #: Mapping of Stream instance to integer context IDs reachable via the + #: stream; used to cleanup routes during disconnection. + self._routes_by_stream = {} - def propagate(self, handle, target_id, name=None): - # self.parent is None in the master. - if not self.parent: - return - + def _send_one(self, stream, handle, target_id, name): data = str(target_id) if name: data = '%s:%s' % (target_id, mitogen.core.b(name)) - self.parent.send( + stream.send( mitogen.core.Message( handle=handle, data=data.encode('utf-8'), + dst_id=stream.remote_id, ) ) + def propagate(self, handle, target_id, name=None): + # self.parent is None in the master. + if self.parent: + self._send_one(self.parent, handle, target_id, name) + + def child_propagate(self, handle, target_id): + """ + For DEL_ROUTE, we additionally want to broadcast the message to any + stream that has ever communicated with the disconnecting ID, so + core.py's :meth:`mitogen.core.Router._on_del_route` can turn the + message into a disconnect event. + """ + for stream in itervalues(self.router._stream_by_id): + if target_id in stream.egress_ids: + self._send_one(stream, mitogen.core.DEL_ROUTE, target_id, None) + def notice_stream(self, stream): """ When this parent is responsible for a new directly connected child stream, we're also responsible for broadcasting DEL_ROUTE upstream if/when that child disconnects. """ + self._routes_by_stream[stream] = set([stream.remote_id]) self.propagate(mitogen.core.ADD_ROUTE, stream.remote_id, stream.name) mitogen.core.listen( @@ -1462,11 +1478,12 @@ class RouteMonitor(object): """ Respond to disconnection of a local stream by """ - LOG.debug('%r is gone; propagating DEL_ROUTE for %r', - stream, stream.routes) - for target_id in stream.routes: + routes = self._routes_by_stream.pop(stream) + LOG.debug('%r is gone; propagating DEL_ROUTE for %r', stream, routes) + for target_id in routes: self.router.del_route(target_id) self.propagate(mitogen.core.DEL_ROUTE, target_id) + self.child_propagate(mitogen.core.DEL_ROUTE, target_id) context = self.router.context_by_id(target_id, create=False) if context: @@ -1489,7 +1506,7 @@ class RouteMonitor(object): return LOG.debug('Adding route to %d via %r', target_id, stream) - stream.routes.add(target_id) + self._routes_by_stream[stream].add(target_id) self.router.add_route(target_id, stream) self.propagate(mitogen.core.ADD_ROUTE, target_id, target_name) @@ -1505,14 +1522,21 @@ class RouteMonitor(object): target_id, stream, registered_stream) return - LOG.debug('Deleting route to %d via %r', target_id, stream) - stream.routes.discard(target_id) - self.router.del_route(target_id) - self.propagate(mitogen.core.DEL_ROUTE, target_id) context = self.router.context_by_id(target_id, create=False) if context: + LOG.debug('%r: Firing local disconnect for %r', self, context) mitogen.core.fire(context, 'disconnect') + LOG.debug('Deleting route to %d via %r', target_id, stream) + routes = self._routes_by_stream.get(stream) + if routes: + routes.discard(target_id) + + self.router.del_route(target_id) + if stream.remote_id != mitogen.parent_id: + self.propagate(mitogen.core.DEL_ROUTE, target_id) + self.child_propagate(mitogen.core.DEL_ROUTE, target_id) + class Router(mitogen.core.Router): context_class = Context diff --git a/tests/parent_test.py b/tests/parent_test.py index c9ccaf3f..a450ee8a 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -30,6 +30,11 @@ def wait_for_child(pid, timeout=1.0): assert False, "wait_for_child() timed out" +@mitogen.core.takes_econtext +def call_func_in_sibling(ctx, econtext): + ctx.call(time.sleep, 99999) + + class GetDefaultRemoteNameTest(testlib.TestCase): func = staticmethod(mitogen.parent.get_default_remote_name) @@ -298,5 +303,57 @@ class WriteAllTest(unittest2.TestCase): proc.terminate() +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.fork() + 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], mitogen.core.ChannelError.local_msg) + + def test_indirect_child_disconnected(self): + # Achievement unlocked: process notices an indirectly connected child + # is disconnected. + c1 = self.router.fork() + c2 = self.router.fork(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], mitogen.core.ChannelError.local_msg) + + def test_indirect_child_intermediary_disconnected(self): + # Battlefield promotion: process notices indirect child disconnected + # due to an intermediary child disconnecting. + c1 = self.router.fork() + c2 = self.router.fork(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], mitogen.core.ChannelError.local_msg) + + def test_sibling_disconnected(self): + # Hard mode: child notices sibling connected to same parent has + # disconnected. + c1 = self.router.fork() + c2 = self.router.fork() + + # Let c1 call functions in c2. + self.router.stream_by_id(c1.context_id).auth_id = mitogen.context_id + c1.call(mitogen.parent.upgrade_router) + + recv = c1.call_async(call_func_in_sibling, c2) + c2.shutdown(wait=True) + e = self.assertRaises(mitogen.core.CallError, + lambda: recv.get().unpickle()) + self.assertTrue(e.args[0].startswith( + 'mitogen.core.ChannelError: Channel closed by local end.' + )) + + if __name__ == '__main__': unittest2.main() From bd71a2760e23679bc66aec2d375c5a531c028a5d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 23:16:48 +0100 Subject: [PATCH 006/662] docs: describe disconnect propagation; closes #76. --- docs/howitworks.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 2a4623eb..66716124 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -551,6 +551,28 @@ and trigger ``DEL_ROUTE`` messages propagated upstream for each route associated with that stream if the stream is disconnected for any reason. +Disconnect Propagation +###################### + +To ensure timely shutdown when a failure occurs, where some context is awaiting +a response from another context that has become disconnected, +:class:`mitogen.core.Router` additionally records the destination context ID of +every message received on a particular stream. + +When ``DEL_ROUTE`` is generated locally or received on some other stream, +:class:`mitogen.parent.RouteMonitor` uses this to find every stream that ever +communicated with the route that is about to go away, and forwards the message +to each found. + +The recipient ``DEL_ROUTE`` handler in turn uses the message to find any +:class:`mitogen.core.Context` in the local process corresponding to the +disappearing route, and if found, fires a ``disconnected`` event on it. + +Any interested party, such as :class:`mitogen.core.Receiver`, may subscribe to +the event and use it to abort any threads that were asleep waiting for a reply +that will never arrive. + + Example ####### From 96b88cc70f3e2a7c472bbb7a157c8645b995075e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 23:24:34 +0100 Subject: [PATCH 007/662] issue #76: docs: update Changelog. --- docs/changelog.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 099b253b..f0ad3f9f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,35 @@ Release Notes +v0.2.4 (2018-??-??) +------------------ + +Mitogen for Ansible +~~~~~~~~~~~~~~~~~~~ + +Enhancements +^^^^^^^^^^^^ + +Fixes +^^^^^ + +Core Library +~~~~~~~~~~~~ + +* `#76 `_: routing maintains the set + of destination context ID ever received on each stream, and when + disconnection occurs, propagates ``DEL_ROUTE`` messages downwards towards + every stream that ever communicated with a disappearing peer, rather than + simply toward parents. + + Conversations between nodes in any level of the connection tree should + correctly receive ``DEL_ROUTE`` messages when a participant disconnects, + allowing receivers to be woken with :class:`mitogen.core.ChannelError` to + signal the connection has broken, even when one participant is not a parent + of the other. + + + v0.2.3 (2018-10-23) ------------------- From f3e19d81da185140e78b20c873ffe768202a86a1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 23 Oct 2018 23:26:12 +0100 Subject: [PATCH 008/662] docs: reorder sections --- docs/howitworks.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 66716124..5e2c10f5 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -551,28 +551,6 @@ and trigger ``DEL_ROUTE`` messages propagated upstream for each route associated with that stream if the stream is disconnected for any reason. -Disconnect Propagation -###################### - -To ensure timely shutdown when a failure occurs, where some context is awaiting -a response from another context that has become disconnected, -:class:`mitogen.core.Router` additionally records the destination context ID of -every message received on a particular stream. - -When ``DEL_ROUTE`` is generated locally or received on some other stream, -:class:`mitogen.parent.RouteMonitor` uses this to find every stream that ever -communicated with the route that is about to go away, and forwards the message -to each found. - -The recipient ``DEL_ROUTE`` handler in turn uses the message to find any -:class:`mitogen.core.Context` in the local process corresponding to the -disappearing route, and if found, fires a ``disconnected`` event on it. - -Any interested party, such as :class:`mitogen.core.Receiver`, may subscribe to -the event and use it to abort any threads that were asleep waiting for a reply -that will never arrive. - - Example ####### @@ -597,6 +575,28 @@ When ``sudo:node22a:webapp`` wants to send a message to .. image:: images/route.png +Disconnect Propagation +###################### + +To ensure timely shutdown when a failure occurs, where some context is awaiting +a response from another context that has become disconnected, +:class:`mitogen.core.Router` additionally records the destination context ID of +every message received on a particular stream. + +When ``DEL_ROUTE`` is generated locally or received on some other stream, +:class:`mitogen.parent.RouteMonitor` uses this to find every stream that ever +communicated with the route that is about to go away, and forwards the message +to each found. + +The recipient ``DEL_ROUTE`` handler in turn uses the message to find any +:class:`mitogen.core.Context` in the local process corresponding to the +disappearing route, and if found, fires a ``disconnected`` event on it. + +Any interested party, such as :class:`mitogen.core.Receiver`, may subscribe to +the event and use it to abort any threads that were asleep waiting for a reply +that will never arrive. + + .. _source-verification: Source Verification From fba52a0edfbb6966213692a582c1ae39df713165 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 24 Oct 2018 13:02:46 +0100 Subject: [PATCH 009/662] issue #76: add API for ansible_mitogen to get route list Earlier commit moved Stream.routes attribute into a private map belonging to RouteMonitor, to make upgrades smoother. This adds a new accessor method to RouteMonitor. --- ansible_mitogen/services.py | 3 ++- mitogen/parent.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index 199f2116..eae8fc68 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -256,8 +256,9 @@ class ContextService(mitogen.service.Service): # in _latches_by_key below. self._lock.acquire() try: + routes = self.router.route_monitor.get_routes(stream) for context, key in list(self._key_by_context.items()): - if context.context_id in stream.routes: + if context.context_id in routes: LOG.info('Dropping %r due to disconnect of %r', context, stream) self._response_by_key.pop(key, None) diff --git a/mitogen/parent.py b/mitogen/parent.py index f3e39ebe..5e0157d6 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1443,12 +1443,15 @@ class RouteMonitor(object): ) ) - def propagate(self, handle, target_id, name=None): - # self.parent is None in the master. - if self.parent: - self._send_one(self.parent, handle, target_id, name) + def _propagate(self, handle, target_id, name=None): + if not self.parent: + # self.parent is None in the master. + return + + stream = self.router.stream_by_id(self.parent.context_id) + self._send_one(stream, handle, target_id, name) - def child_propagate(self, handle, target_id): + def _child_propagate(self, handle, target_id): """ For DEL_ROUTE, we additionally want to broadcast the message to any stream that has ever communicated with the disconnecting ID, so @@ -1466,14 +1469,23 @@ class RouteMonitor(object): if/when that child disconnects. """ self._routes_by_stream[stream] = set([stream.remote_id]) - self.propagate(mitogen.core.ADD_ROUTE, stream.remote_id, - stream.name) + self._propagate(mitogen.core.ADD_ROUTE, stream.remote_id, + stream.name) mitogen.core.listen( obj=stream, name='disconnect', func=lambda: self._on_stream_disconnect(stream), ) + def get_routes(self, stream): + """ + Return the set of context IDs reachable on a stream. + + :param mitogen.core.Stream stream: + :returns: set([int]) + """ + return self._routes_by_stream.get(stream) or set() + def _on_stream_disconnect(self, stream): """ Respond to disconnection of a local stream by @@ -1482,8 +1494,8 @@ class RouteMonitor(object): LOG.debug('%r is gone; propagating DEL_ROUTE for %r', stream, routes) for target_id in routes: self.router.del_route(target_id) - self.propagate(mitogen.core.DEL_ROUTE, target_id) - self.child_propagate(mitogen.core.DEL_ROUTE, target_id) + self._propagate(mitogen.core.DEL_ROUTE, target_id) + self._child_propagate(mitogen.core.DEL_ROUTE, target_id) context = self.router.context_by_id(target_id, create=False) if context: @@ -1508,7 +1520,7 @@ class RouteMonitor(object): LOG.debug('Adding route to %d via %r', target_id, stream) self._routes_by_stream[stream].add(target_id) self.router.add_route(target_id, stream) - self.propagate(mitogen.core.ADD_ROUTE, target_id, target_name) + self._propagate(mitogen.core.ADD_ROUTE, target_id, target_name) def _on_del_route(self, msg): if msg.is_dead: @@ -1534,8 +1546,8 @@ class RouteMonitor(object): self.router.del_route(target_id) if stream.remote_id != mitogen.parent_id: - self.propagate(mitogen.core.DEL_ROUTE, target_id) - self.child_propagate(mitogen.core.DEL_ROUTE, target_id) + self._propagate(mitogen.core.DEL_ROUTE, target_id) + self._child_propagate(mitogen.core.DEL_ROUTE, target_id) class Router(mitogen.core.Router): From 58d0a4573817d70c9f00e5553ed135be420771ef Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 24 Oct 2018 13:04:03 +0100 Subject: [PATCH 010/662] issue #76: quieten routing errors. Receiving DEL_ROUTE without a corresponding ADD_ROUTE is now legit behaviour, so don't print an error in this case. Don't print an error for dropped messages if the reply_to indicates the sender doesn't care about a response (dead and no_reply) --- mitogen/core.py | 5 +++-- mitogen/parent.py | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 5d509152..a5270ec7 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1870,8 +1870,9 @@ class Router(object): dead = False if out_stream is None: - LOG.error('%r: no route for %r, my ID is %r', - self, msg, mitogen.context_id) + if msg.reply_to not in (0, IS_DEAD): + LOG.error('%r: no route for %r, my ID is %r', + self, msg, mitogen.context_id) dead = True if in_stream and self.unidirectional and not dead and \ diff --git a/mitogen/parent.py b/mitogen/parent.py index 5e0157d6..9e878e3f 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1598,11 +1598,12 @@ class Router(mitogen.core.Router): def del_route(self, target_id): LOG.debug('%r.del_route(%r)', self, target_id) - try: - del self._stream_by_id[target_id] - except KeyError: - LOG.error('%r: cant delete route to %r: no such stream', - self, target_id) + # DEL_ROUTE may be sent by a parent if it knows this context sent + # messages to a peer that has now disconnected, to let us raise + # 'disconnect' event on the appropriate Context instance. In that case, + # we won't a matching _stream_by_id entry for the disappearing route, + # so don't raise an error for a missing key here. + self._stream_by_id.pop(target_id, None) def get_module_blacklist(self): if mitogen.context_id == 0: From 7647c95f346137ce6d6a1481b1fbf2a5ecddaff0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 25 Oct 2018 13:25:27 +0100 Subject: [PATCH 011/662] issue #76: add one more test for indirect siblings --- tests/parent_test.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/parent_test.py b/tests/parent_test.py index a450ee8a..edeb66b6 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -336,7 +336,7 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): lambda: recv.get()) self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) - def test_sibling_disconnected(self): + def test_near_sibling_disconnected(self): # Hard mode: child notices sibling connected to same parent has # disconnected. c1 = self.router.fork() @@ -354,6 +354,27 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): 'mitogen.core.ChannelError: Channel closed by local end.' )) + def test_far_sibling_disconnected(self): + # God mode: child of child notices child of child of parent has + # disconnected. + c1 = self.router.fork() + c11 = self.router.fork(via=c1) + + c2 = self.router.fork() + c22 = self.router.fork(via=c2) + + # Let c1 call functions in c2. + self.router.stream_by_id(c1.context_id).auth_id = mitogen.context_id + c11.call(mitogen.parent.upgrade_router) + + recv = c11.call_async(call_func_in_sibling, c22) + c22.shutdown(wait=True) + e = self.assertRaises(mitogen.core.CallError, + lambda: recv.get().unpickle()) + self.assertTrue(e.args[0].startswith( + 'mitogen.core.ChannelError: Channel closed by local end.' + )) + if __name__ == '__main__': unittest2.main() From 73055150f3858bbc86ac3b17e6d1df2bf27fcc84 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 09:14:39 +0100 Subject: [PATCH 012/662] tests: move stub tools, into subdir, import docker_test. --- tests/call_function_test.py | 2 +- tests/data/stubs/README.md | 5 ++++ tests/data/stubs/docker.py | 7 +++++ .../data/{fake_lxc.py => stubs/lxc-attach.py} | 0 .../data/{fake_lxc_attach.py => stubs/lxc.py} | 0 tests/data/{fakessh.py => stubs/ssh.py} | 0 tests/docker_test.py | 28 +++++++++++++++++++ tests/lxc_test.py | 4 +-- tests/lxd_test.py | 4 +-- tests/ssh_test.py | 4 +-- 10 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 tests/data/stubs/README.md create mode 100755 tests/data/stubs/docker.py rename tests/data/{fake_lxc.py => stubs/lxc-attach.py} (100%) rename tests/data/{fake_lxc_attach.py => stubs/lxc.py} (100%) rename tests/data/{fakessh.py => stubs/ssh.py} (100%) create mode 100644 tests/docker_test.py diff --git a/tests/call_function_test.py b/tests/call_function_test.py index dc9a2298..ce434b31 100644 --- a/tests/call_function_test.py +++ b/tests/call_function_test.py @@ -119,7 +119,7 @@ class CallFunctionTest(testlib.RouterMixin, testlib.TestCase): lambda: recv.get().unpickle()) -class ChainTest(testlib.RouterMixin, testlib.TestCase): +class CallChainTest(testlib.RouterMixin, testlib.TestCase): # Verify mitogen_chain functionality. klass = mitogen.parent.CallChain diff --git a/tests/data/stubs/README.md b/tests/data/stubs/README.md new file mode 100644 index 00000000..3f9af3c8 --- /dev/null +++ b/tests/data/stubs/README.md @@ -0,0 +1,5 @@ + +# stubs/ + +Dummy implementations of various third party tools that just spawn local Python +interpreters. Used to roughly test the tools' associated Mitogen classes. diff --git a/tests/data/stubs/docker.py b/tests/data/stubs/docker.py new file mode 100755 index 00000000..341cc818 --- /dev/null +++ b/tests/data/stubs/docker.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +import sys +import os + +os.environ['ORIGINAL_ARGV'] = repr(sys.argv) +os.execv(sys.executable, sys.argv[sys.argv.index('-c') - 1:]) diff --git a/tests/data/fake_lxc.py b/tests/data/stubs/lxc-attach.py similarity index 100% rename from tests/data/fake_lxc.py rename to tests/data/stubs/lxc-attach.py diff --git a/tests/data/fake_lxc_attach.py b/tests/data/stubs/lxc.py similarity index 100% rename from tests/data/fake_lxc_attach.py rename to tests/data/stubs/lxc.py diff --git a/tests/data/fakessh.py b/tests/data/stubs/ssh.py similarity index 100% rename from tests/data/fakessh.py rename to tests/data/stubs/ssh.py diff --git a/tests/docker_test.py b/tests/docker_test.py new file mode 100644 index 00000000..33ead10c --- /dev/null +++ b/tests/docker_test.py @@ -0,0 +1,28 @@ +import os + +import mitogen + +import unittest2 + +import testlib + + +class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): + def test_okay(self): + docker_path = testlib.data_path('stubs/docker.py') + context = self.router.docker( + container='container_name', + docker_path=docker_path, + ) + stream = self.router.stream_by_id(context.context_id) + + argv = eval(context.call(os.getenv, 'ORIGINAL_ARGV')) + self.assertEquals(argv[0], docker_path) + self.assertEquals(argv[1], 'exec') + self.assertEquals(argv[2], '--interactive') + self.assertEquals(argv[3], 'container_name') + self.assertEquals(argv[4], stream.python_path) + + +if __name__ == '__main__': + unittest2.main() diff --git a/tests/lxc_test.py b/tests/lxc_test.py index a30cd186..3168aad2 100644 --- a/tests/lxc_test.py +++ b/tests/lxc_test.py @@ -11,9 +11,9 @@ def has_subseq(seq, subseq): return any(seq[x:x+len(subseq)] == subseq for x in range(0, len(seq))) -class FakeLxcAttachTest(testlib.RouterMixin, unittest2.TestCase): +class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): def test_okay(self): - lxc_attach_path = testlib.data_path('fake_lxc_attach.py') + lxc_attach_path = testlib.data_path('stubs/lxc-attach.py') context = self.router.lxc( container='container_name', lxc_attach_path=lxc_attach_path, diff --git a/tests/lxd_test.py b/tests/lxd_test.py index 9c2397a2..41e9df15 100644 --- a/tests/lxd_test.py +++ b/tests/lxd_test.py @@ -7,9 +7,9 @@ import unittest2 import testlib -class FakeLxcTest(testlib.RouterMixin, unittest2.TestCase): +class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): def test_okay(self): - lxc_path = testlib.data_path('fake_lxc.py') + lxc_path = testlib.data_path('stubs/lxc.py') context = self.router.lxd( container='container_name', lxc_path=lxc_path, diff --git a/tests/ssh_test.py b/tests/ssh_test.py index efca057d..edfe45dc 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -11,12 +11,12 @@ import testlib import plain_old_module -class FakeSshTest(testlib.RouterMixin, unittest2.TestCase): +class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): def test_okay(self): context = self.router.ssh( hostname='hostname', username='mitogen__has_sudo', - ssh_path=testlib.data_path('fakessh.py'), + ssh_path=testlib.data_path('stubs/ssh.py'), ) #context.call(mitogen.utils.log_to_file, '/tmp/log') #context.call(mitogen.utils.disable_site_packages) From 3943634fa6654de45ce830cb9fe272f4cc5a49b5 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 09:32:09 +0100 Subject: [PATCH 013/662] tests: import bench/large_messages.py. --- tests/bench/large_messages.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/bench/large_messages.py diff --git a/tests/bench/large_messages.py b/tests/bench/large_messages.py new file mode 100644 index 00000000..24220023 --- /dev/null +++ b/tests/bench/large_messages.py @@ -0,0 +1,28 @@ + +# Verify _receive_one() quadratic behaviour fixed. + +import subprocess +import time +import socket +import mitogen + + +@mitogen.main() +def main(router): + c = router.fork() + + n = 1048576 * 127 + s = ' ' * n + print('bytes in %.2fMiB string...' % (n/1048576.0),) + + t0 = time.time() + for x in range(10): + tt0 = time.time() + assert n == c.call(len, s) + print('took %dms' % (1000 * (time.time() - tt0),)) + t1 = time.time() + print('total %dms / %dms avg / %.2fMiB/sec' % ( + 1000 * (t1 - t0), + (1000 * (t1 - t0)) / (x + 1), + ((n * (x + 1)) / (t1 - t0)) / 1048576.0, + )) From 36e5ca4115e87a4d496ff441eb6d3906319e7338 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 09:33:28 +0100 Subject: [PATCH 014/662] tests: import missing main_with_no_exec_guard.py. --- tests/data/main_with_no_exec_guard.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/data/main_with_no_exec_guard.py diff --git a/tests/data/main_with_no_exec_guard.py b/tests/data/main_with_no_exec_guard.py new file mode 100644 index 00000000..153e4743 --- /dev/null +++ b/tests/data/main_with_no_exec_guard.py @@ -0,0 +1,12 @@ + +import logging +import mitogen.master + +def foo(): + pass + +logging.basicConfig(level=logging.INFO) +router = mitogen.master.Router() + +l = router.local() +l.call(foo) From 0d70fc132482ad076d875214626eb2473c530900 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 09:34:31 +0100 Subject: [PATCH 015/662] tests: import z hostfile --- tests/ansible/hosts/z | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/ansible/hosts/z diff --git a/tests/ansible/hosts/z b/tests/ansible/hosts/z new file mode 100644 index 00000000..61d27940 --- /dev/null +++ b/tests/ansible/hosts/z @@ -0,0 +1,25 @@ +z + +[z-x10] +z-[01:10] + +[z-x20] +z-[01:20] + +[z-x50] +z-[01:50] + +[z-x100] +z-[001:100] + +[z-x200] +z-[001:200] + +[z-x300] +z-[001:300] + +[z-x400] +z-[001:400] + +[z-x500] +z-[001:500] From 918f709420d4278fbdc47905517746bc5475ac3c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 09:41:25 +0100 Subject: [PATCH 016/662] tests: import a bunch more random unchecked in pieces. --- tests/ansible/bench/loop-100-copies.yml | 25 +++++++++++++++++++++++++ tests/ansible/tests/__init__.py | 0 tests/bench/README.md | 5 +++++ tests/bench/roundtrip.py | 2 +- tests/bench/service.py | 23 +++++++++++++++++++++++ tests/data/stubs/README.md | 2 +- 6 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 tests/ansible/bench/loop-100-copies.yml create mode 100644 tests/ansible/tests/__init__.py create mode 100644 tests/bench/README.md create mode 100644 tests/bench/service.py diff --git a/tests/ansible/bench/loop-100-copies.yml b/tests/ansible/bench/loop-100-copies.yml new file mode 100644 index 00000000..231bf4a1 --- /dev/null +++ b/tests/ansible/bench/loop-100-copies.yml @@ -0,0 +1,25 @@ + +- hosts: all + any_errors_fatal: true + tasks: + + - name: Create file tree + connection: local + shell: > + mkdir -p /tmp/filetree.in; + for i in `seq -f /tmp/filetree.in/%g 1 100`; do echo $RANDOM > $i; done; + + - name: Delete remote file tree + file: path=/tmp/filetree.out state=absent + when: 0 + + - file: + state: directory + path: /tmp/filetree.out + + - name: Trigger nasty process pileup + copy: + src: "{{item.src}}" + dest: "/tmp/filetree.out/{{item.path}}" + with_filetree: /tmp/filetree.in + when: item.state == 'file' diff --git a/tests/ansible/tests/__init__.py b/tests/ansible/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/bench/README.md b/tests/bench/README.md new file mode 100644 index 00000000..0ef27df3 --- /dev/null +++ b/tests/bench/README.md @@ -0,0 +1,5 @@ + +# tests/bench/ + +Various manually executed scripts to aid benchmarking, or trigger old +performance problems. diff --git a/tests/bench/roundtrip.py b/tests/bench/roundtrip.py index 13b9413d..7c5a9252 100644 --- a/tests/bench/roundtrip.py +++ b/tests/bench/roundtrip.py @@ -12,6 +12,6 @@ def do_nothing(): def main(router): f = router.fork() t0 = time.time() - for x in xrange(10000): + for x in range(1000): f.call(do_nothing) print '++', int(1e6 * ((time.time() - t0) / (1.0+x))), 'usec' diff --git a/tests/bench/service.py b/tests/bench/service.py new file mode 100644 index 00000000..6d866b5c --- /dev/null +++ b/tests/bench/service.py @@ -0,0 +1,23 @@ +""" +Measure latency of local service RPC. +""" + +import time + +import mitogen.service +import mitogen + + +class MyService(mitogen.service.Service): + @mitogen.service.expose(policy=mitogen.service.AllowParents()) + def ping(self): + return 'pong' + + +@mitogen.main() +def main(router): + f = router.fork() + t0 = time.time() + for x in range(1000): + f.call_service(service_name=MyService, method_name='ping') + print('++', int(1e6 * ((time.time() - t0) / (1.0+x))), 'usec') diff --git a/tests/data/stubs/README.md b/tests/data/stubs/README.md index 3f9af3c8..02de6456 100644 --- a/tests/data/stubs/README.md +++ b/tests/data/stubs/README.md @@ -1,5 +1,5 @@ -# stubs/ +# tests/data/stubs/ Dummy implementations of various third party tools that just spawn local Python interpreters. Used to roughly test the tools' associated Mitogen classes. From 4e3830d75eb1c8b2336a552a27c18ced3a86078a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 10:08:06 +0100 Subject: [PATCH 017/662] tests: add basic unix_test.py. --- mitogen/unix.py | 2 +- tests/unix_test.py | 117 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/unix_test.py diff --git a/mitogen/unix.py b/mitogen/unix.py index 4a4dfb65..056c604d 100644 --- a/mitogen/unix.py +++ b/mitogen/unix.py @@ -82,7 +82,7 @@ class Listener(mitogen.core.BasicStream): sock.setblocking(True) try: pid, = struct.unpack('>L', sock.recv(4)) - except socket.error: + except (struct.error, socket.error): LOG.error('%r: failed to read remote identity: %s', self, sys.exc_info()[1]) return diff --git a/tests/unix_test.py b/tests/unix_test.py new file mode 100644 index 00000000..6225de7d --- /dev/null +++ b/tests/unix_test.py @@ -0,0 +1,117 @@ + +import os +import socket +import sys +import time + +import unittest2 + +import mitogen +import mitogen.master +import mitogen.service +import mitogen.unix + +import testlib + + +class MyService(mitogen.service.Service): + def __init__(self, latch, **kwargs): + super(MyService, self).__init__(**kwargs) + # used to wake up main thread once client has made its request + self.latch = latch + + @mitogen.service.expose(policy=mitogen.service.AllowParents()) + def ping(self, msg): + self.latch.put(None) + return { + 'src_id': msg.src_id, + 'auth_id': msg.auth_id, + } + + +class IsPathDeadTest(unittest2.TestCase): + func = staticmethod(mitogen.unix.is_path_dead) + path = '/tmp/stale-socket' + + def test_does_not_exist(self): + self.assertTrue(self.func('/tmp/does-not-exist')) + + def make_socket(self): + if os.path.exists(self.path): + os.unlink(self.path) + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.bind(self.path) + return s + + def test_conn_refused(self): + s = self.make_socket() + s.close() + self.assertTrue(self.func(self.path)) + + def test_is_alive(self): + s = self.make_socket() + s.listen(5) + self.assertFalse(self.func(self.path)) + s.close() + os.unlink(self.path) + + +class ListenerTest(testlib.RouterMixin, unittest2.TestCase): + klass = mitogen.unix.Listener + + def test_constructor_basic(self): + listener = self.klass(router=self.router) + self.assertFalse(mitogen.unix.is_path_dead(listener.path)) + os.unlink(listener.path) + + +class ClientTest(unittest2.TestCase): + klass = mitogen.unix.Listener + + def _try_connect(self, path): + # give server a chance to setup listener + for x in range(10): + try: + return mitogen.unix.connect(path) + except socket.error: + if x == 9: + raise + time.sleep(0.1) + + def _test_simple_client(self, path): + router, context = self._try_connect(path) + self.assertEquals(0, context.context_id) + self.assertEquals(1, mitogen.context_id) + self.assertEquals(0, mitogen.parent_id) + resp = context.call_service(service_name=MyService, method_name='ping') + self.assertEquals(mitogen.context_id, resp['src_id']) + self.assertEquals(0, resp['auth_id']) + + def _test_simple_server(self, path): + router = mitogen.master.Router() + latch = mitogen.core.Latch() + try: + try: + listener = self.klass(path=path, router=router) + pool = mitogen.service.Pool(router=router, services=[ + MyService(latch=latch, router=router), + ]) + latch.get() + # give broker a chance to deliver service resopnse + time.sleep(0.1) + finally: + pool.shutdown() + router.broker.shutdown() + finally: + os._exit(0) + + def test_simple(self): + path = mitogen.unix.make_socket_path() + if os.fork(): + self._test_simple_client(path) + else: + self._test_simple_server(path) + + +if __name__ == '__main__': + unittest2.main() From 8891fda48a37943022e7e4fbed2dde52067f6ba2 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 09:09:54 +0000 Subject: [PATCH 018/662] docs: getting_started typo --- docs/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 1c92a559..20d1c2b3 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -246,7 +246,7 @@ Running User Functions ---------------------- So far we have used the interactive interpreter to call some standard library -functions, but if since source code typed at the interpreter cannot be +functions, but since the source code typed at the interpreter cannot be recovered, Mitogen is unable to execute functions defined in this way. We must therefore continue by writing our code as a script:: From 0d04e940b7d809272054b8bf0c47e7b03a239a96 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 09:12:00 +0000 Subject: [PATCH 019/662] master: docstring fixes. --- mitogen/master.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mitogen/master.py b/mitogen/master.py index d4ee607a..73302910 100644 --- a/mitogen/master.py +++ b/mitogen/master.py @@ -335,6 +335,11 @@ class LogForwarder(object): class ModuleFinder(object): + """ + Given the name of a loaded module, make a best-effort attempt at finding + related modules likely needed by a child context requesting the original + module. + """ def __init__(self): #: Import machinery is expensive, keep :py:meth`:get_module_source` #: results around. @@ -477,7 +482,8 @@ class ModuleFinder(object): def resolve_relpath(self, fullname, level): """Given an ImportFrom AST node, guess the prefix that should be tacked on to an alias name to produce a canonical name. `fullname` is the name - of the module in which the ImportFrom appears.""" + of the module in which the ImportFrom appears. + """ mod = sys.modules.get(fullname, None) if hasattr(mod, '__path__'): fullname += '.__init__' @@ -499,7 +505,7 @@ class ModuleFinder(object): def find_related_imports(self, fullname): """ - Return a list of non-stdlb modules that are directly imported by + Return a list of non-stdlib modules that are directly imported by `fullname`, plus their parents. The list is determined by retrieving the source code of @@ -550,8 +556,8 @@ class ModuleFinder(object): Return a list of non-stdlib modules that are imported directly or indirectly by `fullname`, plus their parents. - This method is like :py:meth:`on_disconect`, but it also recursively - searches any modules which are imported by `fullname`. + This method is like :py:meth:`find_related_imports`, but also + recursively searches any modules which are imported by `fullname`. :param fullname: Fully qualified name of an _already imported_ module for which source code can be retrieved From a7d635dff8f6ce90b420082052179fc4a8a5ac4a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 10:16:34 +0100 Subject: [PATCH 020/662] tests: import ara_env helper script. --- tests/ansible/ara_env.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100755 tests/ansible/ara_env.py diff --git a/tests/ansible/ara_env.py b/tests/ansible/ara_env.py new file mode 100755 index 00000000..ab2b726e --- /dev/null +++ b/tests/ansible/ara_env.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Print shell environment exports adding ARA plugins to the list of plugins +from ansible.cfg in the CWD. +""" + +import os + +import ara.setup +import ansible.constants as C + +os.chdir(os.path.dirname(__file__)) + +print('export ANSIBLE_ACTION_PLUGINS=%s:%s' % ( + ':'.join(C.DEFAULT_ACTION_PLUGIN_PATH), + ara.setup.action_plugins, +)) + +print('export ANSIBLE_CALLBACK_PLUGINS=%s:%s' % ( + ':'.join(C.DEFAULT_CALLBACK_PLUGIN_PATH), + ara.setup.callback_plugins, +)) + +print('export ANSIBLE_LIBRARY=%s:%s' % ( + ':'.join(C.DEFAULT_MODULE_PATH), + ara.setup.library, +)) From 6451117d00ea600a071016fc2cefbb4a219fb94a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 10:23:34 +0100 Subject: [PATCH 021/662] Add venvs/ to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6092d04e..e244ca12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .coverage .tox .venv +venvs/** **/.DS_Store *.pyc *.pyd From 778892eaaa095bce4923854fbed783f63437220d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 10:54:22 +0100 Subject: [PATCH 022/662] issue #76: call_function_test fix. --- tests/call_function_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/call_function_test.py b/tests/call_function_test.py index ce434b31..72991d62 100644 --- a/tests/call_function_test.py +++ b/tests/call_function_test.py @@ -103,7 +103,8 @@ class CallFunctionTest(testlib.RouterMixin, testlib.TestCase): def test_accepts_returns_context(self): context = self.local.call(func_returns_arg, self.local) - self.assertIsNot(context, self.local) + # Unpickling now deduplicates Context instances. + self.assertIs(context, self.local) self.assertEqual(context.context_id, self.local.context_id) self.assertEqual(context.name, self.local.name) @@ -124,7 +125,7 @@ class CallChainTest(testlib.RouterMixin, testlib.TestCase): klass = mitogen.parent.CallChain def setUp(self): - super(ChainTest, self).setUp() + super(CallChainTest, self).setUp() self.local = self.router.fork() def test_subsequent_calls_produce_same_error(self): From 0cf6019bac59cab9c3e34dcfe7aa7d366b37ab2a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 10:55:54 +0100 Subject: [PATCH 023/662] tests: rename one more stubs/ssh.py reference. --- tests/ssh_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ssh_test.py b/tests/ssh_test.py index edfe45dc..179b543d 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -133,7 +133,7 @@ class RequirePtyTest(testlib.DockerMixin, testlib.TestCase): return self.router.ssh( hostname='hostname', username='mitogen__has_sudo', - ssh_path=testlib.data_path('fakessh.py'), + ssh_path=testlib.data_path('stubs/ssh.py'), **kwargs ) finally: From c1c7e5171dc8baed7c1471955769a966529d81ec Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 11:08:12 +0100 Subject: [PATCH 024/662] tests: fix fork FD sharing in unix_test. --- tests/unix_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unix_test.py b/tests/unix_test.py index 6225de7d..67265c81 100644 --- a/tests/unix_test.py +++ b/tests/unix_test.py @@ -7,6 +7,7 @@ import time import unittest2 import mitogen +import mitogen.fork import mitogen.master import mitogen.service import mitogen.unix @@ -110,6 +111,7 @@ class ClientTest(unittest2.TestCase): if os.fork(): self._test_simple_client(path) else: + mitogen.fork.on_fork() self._test_simple_server(path) From 7e04ee8af9fcd471a5affa307da148e4043ad217 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 11:11:25 +0100 Subject: [PATCH 025/662] ansible: fix is_good_temp_dir() log format --- ansible_mitogen/target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_mitogen/target.py b/ansible_mitogen/target.py index ff6ed083..69a082af 100644 --- a/ansible_mitogen/target.py +++ b/ansible_mitogen/target.py @@ -250,7 +250,7 @@ def is_good_temp_dir(path): if not os.access(tmp.name, os.X_OK): raise OSError('filesystem appears to be mounted noexec') except OSError as e: - LOG.debug('temp dir %r unusable: %s: %s', path, e) + LOG.debug('temp dir %r unusable: %s', path, e) return False finally: tmp.close() From 08641555920db9f0653b94f84779985b13d61ede Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 11:15:41 +0100 Subject: [PATCH 026/662] tests: pin pycparser to last 2.6-compatible version --- dev_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_requirements.txt b/dev_requirements.txt index 68f0422a..972dbcb3 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -5,6 +5,7 @@ Django==1.6.11 # Last version supporting 2.6. mock==2.0.0 pytz==2018.5 paramiko==2.3.2 # Last 2.6-compat version. +pycparser==2.18 # Last version supporting 2.6. pytest-catchlog==1.2.2 pytest==3.1.2 PyYAML==3.11; python_version < '2.7' From f343bbba3a5338d75e50f2455820a6d9f2ae0087 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 11:20:18 +0100 Subject: [PATCH 027/662] unix: fix exception catch on 3.x. --- mitogen/unix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitogen/unix.py b/mitogen/unix.py index 056c604d..417842bc 100644 --- a/mitogen/unix.py +++ b/mitogen/unix.py @@ -52,7 +52,7 @@ def is_path_dead(path): s.connect(path) except socket.error: e = sys.exc_info()[1] - return e[0] in (errno.ECONNREFUSED, errno.ENOENT) + return e.args[0] in (errno.ECONNREFUSED, errno.ENOENT) return False From b70c57a2cb6862ec8189d2d174200817cfb6f257 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 11:22:33 +0100 Subject: [PATCH 028/662] tests: fix wstatus_to_str() test on 3.x Now they use enums. --- tests/parent_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parent_test.py b/tests/parent_test.py index edeb66b6..aaf335b8 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -79,7 +79,7 @@ class WstatusToStrTest(testlib.TestCase): (pid, status), _ = mitogen.core.io_op(os.waitpid, pid, 0) self.assertEquals( self.func(status), - 'exited due to signal %s (SIGKILL)' % (signal.SIGKILL,) + 'exited due to signal %s (SIGKILL)' % (int(signal.SIGKILL),) ) # can't test SIGSTOP without POSIX sessions rabbithole From 0dc3f8accfe4e7c90705fdd40bc99718ec08bc37 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 11:26:15 +0100 Subject: [PATCH 029/662] ansible: fix another target.py format string. --- ansible_mitogen/target.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ansible_mitogen/target.py b/ansible_mitogen/target.py index 69a082af..0e74f960 100644 --- a/ansible_mitogen/target.py +++ b/ansible_mitogen/target.py @@ -241,8 +241,7 @@ def is_good_temp_dir(path): try: os.chmod(tmp.name, int('0700', 8)) except OSError as e: - LOG.debug('temp dir %r unusable: %s: chmod failed: %s', - path, e) + LOG.debug('temp dir %r unusable: chmod failed: %s', path, e) return False try: From 905ab890fb5f93c3c7dbabbe074ab8cb9b13b92a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 11:36:49 +0100 Subject: [PATCH 030/662] tests: stop idiotic Travis TTY/pip progress bar spam --- .travis.yml | 3 ++- .travis/ci_lib.py | 11 +++++++++++ .travis/debops_common_tests.sh | 2 +- dev_requirements.txt | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5dfdae00..95d9c64c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,8 @@ cache: - /home/travis/virtualenv install: -- pip install -r dev_requirements.txt +# |cat to disable progress bar. +- pip install -r dev_requirements.txt |cat script: - | diff --git a/.travis/ci_lib.py b/.travis/ci_lib.py index eb130a14..e92564b6 100644 --- a/.travis/ci_lib.py +++ b/.travis/ci_lib.py @@ -32,6 +32,17 @@ def subprocess__check_output(*popenargs, **kwargs): if not hasattr(subprocess, 'check_output'): subprocess.check_output = subprocess__check_output + +# ----------------- + +# Force stdout FD 1 to be a pipe, so tools like pip don't spam progress bars. + +sys.stdout = os.popen('stdbuf -oL cat', 'w', 1) +os.dup2(sys.stdout.fileno(), 1) + +sys.stderr = sys.stdout +os.dup2(sys.stderr.fileno(), 2) + # ----------------- def _argv(s, *args): diff --git a/.travis/debops_common_tests.sh b/.travis/debops_common_tests.sh index 50e67ada..753d1c11 100755 --- a/.travis/debops_common_tests.sh +++ b/.travis/debops_common_tests.sh @@ -27,7 +27,7 @@ mkdir "$TMPDIR" echo travis_fold:start:job_setup -pip install -qqqU debops==0.7.2 ansible==${ANSIBLE_VERSION} +pip install -qqqU debops==0.7.2 ansible==${ANSIBLE_VERSION} |cat debops-init "$TMPDIR/project" cd "$TMPDIR/project" diff --git a/dev_requirements.txt b/dev_requirements.txt index 972dbcb3..a6807488 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -5,6 +5,7 @@ Django==1.6.11 # Last version supporting 2.6. mock==2.0.0 pytz==2018.5 paramiko==2.3.2 # Last 2.6-compat version. +cffi==1.11.2 # Random pin to try and fix pyparser==2.18 not having effect pycparser==2.18 # Last version supporting 2.6. pytest-catchlog==1.2.2 pytest==3.1.2 From 3585ee74f70afeacd37374f2206720cc718c01a3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 12:47:30 +0100 Subject: [PATCH 031/662] tests: split out ansible_tests requirements Also remove hard-coded Ansible version, the tests don't need it, nor does local testing most of the time --- .travis/ansible_tests.py | 1 + dev_requirements.txt | 3 --- tests/ansible/requirements.txt | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 tests/ansible/requirements.txt diff --git a/.travis/ansible_tests.py b/.travis/ansible_tests.py index 3b5e40db..efaca9fe 100755 --- a/.travis/ansible_tests.py +++ b/.travis/ansible_tests.py @@ -34,6 +34,7 @@ with ci_lib.Fold('job_setup'): os.chdir(TESTS_DIR) os.chmod('../data/docker/mitogen__has_sudo_pubkey.key', int('0600', 7)) + run("pip install -qr requirements.txt") # tests/ansible/requirements # Don't set -U as that will upgrade Paramiko to a non-2.6 compatible version. run("pip install -q ansible==%s", ci_lib.ANSIBLE_VERSION) diff --git a/dev_requirements.txt b/dev_requirements.txt index a6807488..0b2846ac 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,10 +1,8 @@ -r docs/docs-requirements.txt -ansible==2.6.1 coverage==4.5.1 Django==1.6.11 # Last version supporting 2.6. mock==2.0.0 pytz==2018.5 -paramiko==2.3.2 # Last 2.6-compat version. cffi==1.11.2 # Random pin to try and fix pyparser==2.18 not having effect pycparser==2.18 # Last version supporting 2.6. pytest-catchlog==1.2.2 @@ -16,4 +14,3 @@ unittest2==1.1.0 # Fix InsecurePlatformWarning while creating py26 tox environment # https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings urllib3[secure]; python_version < '2.7.9' -google-api-python-client==1.6.5 diff --git a/tests/ansible/requirements.txt b/tests/ansible/requirements.txt new file mode 100644 index 00000000..fdabb0f6 --- /dev/null +++ b/tests/ansible/requirements.txt @@ -0,0 +1,2 @@ +paramiko==2.3.2 # Last 2.6-compat version. +google-api-python-client==1.6.5 From 3429e57825a2323285a76baa6207bf6a1e6a8f5c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 13:04:00 +0100 Subject: [PATCH 032/662] tests: fix target_test 3.x compat. --- tests/ansible/tests/target_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ansible/tests/target_test.py b/tests/ansible/tests/target_test.py index e3d59433..7d6c0b46 100644 --- a/tests/ansible/tests/target_test.py +++ b/tests/ansible/tests/target_test.py @@ -28,10 +28,10 @@ class ApplyModeSpecTest(unittest2.TestCase): def test_simple(self): spec = 'u+rwx,go=x' - self.assertEquals(0711, self.func(spec, 0)) + self.assertEquals(int('0711', 8), self.func(spec, 0)) spec = 'g-rw' - self.assertEquals(0717, self.func(spec, 0777)) + self.assertEquals(int('0717', 8), self.func(spec, int('0777', 8))) class IsGoodTempDirTest(unittest2.TestCase): From 592d6fc8d3a4a0a53d922486e11af1516d4f9015 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 13:22:33 +0100 Subject: [PATCH 033/662] tests: fix CaptureStreamHandler on 2.6. --- tests/testlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testlib.py b/tests/testlib.py index 63d96233..f2bad491 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -160,12 +160,12 @@ def sync_with_broker(broker, timeout=10.0): class CaptureStreamHandler(logging.StreamHandler): def __init__(self, *args, **kwargs): - super(CaptureStreamHandler, self).__init__(*args, **kwargs) + logging.StreamHandler.__init__(self, *args, **kwargs) self.msgs = [] def emit(self, msg): self.msgs.append(msg) - return super(CaptureStreamHandler, self).emit(msg) + logging.StreamHandler.emit(self, msg) class LogCapturer(object): From 9ec360c26db00669ccfe77e8eaefc735add52abc Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 13:55:25 +0100 Subject: [PATCH 034/662] core: split out & extend Broker.sync_call() --- docs/api.rst | 8 ++++++++ mitogen/core.py | 26 ++++++++++++++++++++------ tests/broker_test.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 tests/broker_test.py diff --git a/docs/api.rst b/docs/api.rst index 72a6b4db..52d5dcec 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1227,6 +1227,14 @@ Broker Class thread, or immediately if the current thread is the broker thread. Safe to call from any thread. + .. method:: defer_sync (func) + + Arrange for `func()` to execute on the broker thread, blocking the + current thread until a result or exception is available. + + :returns: + Call result. + .. method:: start_receive (stream) Mark the :attr:`receive_side ` on `stream` as diff --git a/mitogen/core.py b/mitogen/core.py index a5270ec7..01be1cfe 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1940,6 +1940,25 @@ class Broker(object): it = (side.keep_alive for (_, (side, _)) in self.poller.readers) return sum(it, 0) + def defer_sync(self, func): + """ + Block the calling thread while `func` runs on a broker thread. + + :returns: + Return value of `func()`. + """ + latch = Latch() + def wrapper(): + try: + latch.put(func()) + except Exception: + latch.put(sys.exc_info()[1]) + self.defer(wrapper) + res = latch.get() + if isinstance(res, Exception): + raise res + return res + def _call(self, stream, func): try: func(self) @@ -2100,11 +2119,6 @@ class ExternalContext(object): _v and LOG.debug('%r: parent stream is gone, dying.', self) self.broker.shutdown() - def _sync(self, func): - latch = Latch() - self.broker.defer(lambda: latch.put(func())) - return latch.get() - def detach(self): self.detached = True stream = self.router.stream_by_id(mitogen.parent_id) @@ -2113,7 +2127,7 @@ class ExternalContext(object): self.parent.send_await(Message(handle=DETACHING)) LOG.info('Detaching from %r; parent is %s', stream, self.parent) for x in range(20): - pending = self._sync(lambda: stream.pending_bytes()) + pending = self.broker.defer_sync(lambda: stream.pending_bytes()) if not pending: break time.sleep(0.05) diff --git a/tests/broker_test.py b/tests/broker_test.py new file mode 100644 index 00000000..7d070e3d --- /dev/null +++ b/tests/broker_test.py @@ -0,0 +1,32 @@ + +import threading + +import unittest2 + +import testlib + +import mitogen.core + + +class DeferSyncTest(testlib.TestCase): + klass = mitogen.core.Broker + + def test_okay(self): + broker = self.klass() + try: + th = broker.defer_sync(lambda: threading.currentThread()) + self.assertEquals(th, broker._thread) + finally: + broker.shutdown() + + def test_exception(self): + broker = self.klass() + try: + self.assertRaises(ValueError, + broker.defer_sync, lambda: int('dave')) + finally: + broker.shutdown() + + +if __name__ == '__main__': + unittest2.main() From efed9da474f2f46d9acbfd604facd20e12976825 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 26 Oct 2018 13:59:06 +0100 Subject: [PATCH 035/662] docs: update Changelog. --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index f0ad3f9f..ac9f37be 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -42,6 +42,8 @@ Core Library signal the connection has broken, even when one participant is not a parent of the other. +* `9ec360c2 `_: a new + :meth:`mitogen.core.Broker.defer_sync` utility function is provided. v0.2.3 (2018-10-23) From 53d882dcbda80d35ed24d59459abedc3539bf006 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 15:23:57 +0000 Subject: [PATCH 036/662] tests: activate faulthandler if available --- dev_requirements.txt | 1 + tests/testlib.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/dev_requirements.txt b/dev_requirements.txt index 0b2846ac..f48006e5 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -5,6 +5,7 @@ mock==2.0.0 pytz==2018.5 cffi==1.11.2 # Random pin to try and fix pyparser==2.18 not having effect pycparser==2.18 # Last version supporting 2.6. +faulthandler==3.1; python_version < '3.3' # used by testlib pytest-catchlog==1.2.2 pytest==3.1.2 PyYAML==3.11; python_version < '2.7' diff --git a/tests/testlib.py b/tests/testlib.py index f2bad491..8f11337d 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -14,6 +14,11 @@ import mitogen.core import mitogen.master import mitogen.utils +try: + import faulthandler +except ImportError: + pass + try: import urlparse except ImportError: @@ -32,6 +37,9 @@ sys.path.append(DATA_DIR) if mitogen.is_master: mitogen.utils.log_to_file() +if faulthandler is not None: + faulthandler.enable() + def data_path(suffix): path = os.path.join(DATA_DIR, suffix) From d0f5671887d3250bdacc23d814b116ec18174792 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 15:27:20 +0000 Subject: [PATCH 037/662] ansible: split key_from_dict() out into free function. --- ansible_mitogen/services.py | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index eae8fc68..f9a1a3df 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -89,6 +89,24 @@ def _get_candidate_temp_dirs(): return mitogen.utils.cast(dirs) +def key_from_dict(**kwargs): + """ + Return a unique string representation of a dict as quickly as possible. + Used to generated deduplication keys from a request. + """ + out = [] + stack = [kwargs] + while stack: + obj = stack.pop() + if isinstance(obj, dict): + stack.extend(sorted(obj.items())) + elif isinstance(obj, (list, tuple)): + stack.extend(obj) + else: + out.append(str(obj)) + return ''.join(out) + + class Error(Exception): pass @@ -113,7 +131,7 @@ class ContextService(mitogen.service.Service): super(ContextService, self).__init__(*args, **kwargs) self._lock = threading.Lock() #: Records the :meth:`get` result dict for successful calls, returned - #: for identical subsequent calls. Keyed by :meth:`key_from_kwargs`. + #: for identical subsequent calls. Keyed by :meth:`key_from_dict`. self._response_by_key = {} #: List of :class:`mitogen.core.Latch` awaiting the result for a #: particular key. @@ -126,7 +144,7 @@ class ContextService(mitogen.service.Service): #: :attr:`max_interpreters` is reached, the most recently used context #: is destroyed to make room for any additional context. self._lru_by_via = {} - #: :meth:`key_from_kwargs` result by Context. + #: :func:`key_from_dict` result by Context. self._key_by_context = {} @mitogen.service.expose(mitogen.service.AllowParents()) @@ -149,29 +167,13 @@ class ContextService(mitogen.service.Service): finally: self._lock.release() - def key_from_kwargs(self, **kwargs): - """ - Generate a deduplication key from the request. - """ - out = [] - stack = [kwargs] - while stack: - obj = stack.pop() - if isinstance(obj, dict): - stack.extend(sorted(obj.items())) - elif isinstance(obj, (list, tuple)): - stack.extend(obj) - else: - out.append(str(obj)) - return ''.join(out) - def _produce_response(self, key, response): """ Reply to every waiting request matching a configuration key with a response dictionary, deleting the list of waiters when done. :param str key: - Result of :meth:`key_from_kwargs` + Result of :meth:`key_from_dict` :param dict response: Response dictionary :returns: @@ -361,7 +363,7 @@ class ContextService(mitogen.service.Service): def _wait_or_start(self, spec, via=None): latch = mitogen.core.Latch() - key = self.key_from_kwargs(via=via, **spec) + key = key_from_dict(via=via, **spec) self._lock.acquire() try: response = self._response_by_key.get(key) From c510e58f9b74279277df4f6e6195847ea678c990 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 15:36:33 +0000 Subject: [PATCH 038/662] issue #352: add test for disconnect message. --- tests/ansible/integration/connection/all.yml | 3 ++- .../connection/disconnect_during_module.yml | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/ansible/integration/connection/disconnect_during_module.yml diff --git a/tests/ansible/integration/connection/all.yml b/tests/ansible/integration/connection/all.yml index 123e11c4..d0ad4c1b 100644 --- a/tests/ansible/integration/connection/all.yml +++ b/tests/ansible/integration/connection/all.yml @@ -1,5 +1,6 @@ --- +- import_playbook: disconnect_during_module.yml - import_playbook: exec_command.yml -- import_playbook: put_small_file.yml - import_playbook: put_large_file.yml +- import_playbook: put_small_file.yml diff --git a/tests/ansible/integration/connection/disconnect_during_module.yml b/tests/ansible/integration/connection/disconnect_during_module.yml new file mode 100644 index 00000000..f2943b44 --- /dev/null +++ b/tests/ansible/integration/connection/disconnect_during_module.yml @@ -0,0 +1,19 @@ +# issue 352: test ability to notice disconnection during a module invocation. +--- + +- name: integration/connection/disconnect_during_module.yml + hosts: test-targets localhost + gather_facts: no + any_errors_fatal: false + tasks: + - run_once: true # don't run against localhost + shell: | + kill -9 $PPID + register: out + ignore_errors: true + + - assert: + that: + - out.msg.startswith('Mitogen was disconnected from the remote environment while a call was in-progress.') + + - meta: clear_host_errors From 89852db163a922f4b4c2cf9bf3519ea19eeee113 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 16:32:34 +0000 Subject: [PATCH 039/662] issue #370: add 'disconnect resets connection' test --- tests/ansible/integration/connection/all.yml | 1 + .../disconnect_resets_connection.yml | 44 +++++++++++++++++++ .../lib/action/mitogen_action_script.py | 28 ++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 tests/ansible/integration/connection/disconnect_resets_connection.yml create mode 100644 tests/ansible/lib/action/mitogen_action_script.py diff --git a/tests/ansible/integration/connection/all.yml b/tests/ansible/integration/connection/all.yml index d0ad4c1b..34e21f61 100644 --- a/tests/ansible/integration/connection/all.yml +++ b/tests/ansible/integration/connection/all.yml @@ -1,6 +1,7 @@ --- - import_playbook: disconnect_during_module.yml +- import_playbook: disconnect_resets_connection.yml - import_playbook: exec_command.yml - import_playbook: put_large_file.yml - import_playbook: put_small_file.yml diff --git a/tests/ansible/integration/connection/disconnect_resets_connection.yml b/tests/ansible/integration/connection/disconnect_resets_connection.yml new file mode 100644 index 00000000..9e186182 --- /dev/null +++ b/tests/ansible/integration/connection/disconnect_resets_connection.yml @@ -0,0 +1,44 @@ +# issue 370: Connection should reset to 'disconnected' state when disconnect +# detected +# +# Previously the 'Mitogen was disconnected' error would fail the first task, +# but the Connection instance would still think it still had a valid +# connection. +# +# See also disconnect_during_module.yml + +--- + +- name: integration/connection/disconnect_resets_connection.yml + hosts: test-targets + gather_facts: no + any_errors_fatal: true + tasks: + - mitogen_action_script: + script: | + import sys + from ansible.errors import AnsibleConnectionFailure + + assert not self._connection.connected, \ + "Connection was not initially disconnected." + + self._low_level_execute_command('echo') + assert self._connection.connected, \ + "Connection was not connected after good command." + + try: + self._low_level_execute_command('kill -9 $PPID') + assert 0, 'AnsibleConnectionFailure was not raised' + except AnsibleConnectionFailure: + e = sys.exc_info()[1] + assert str(e).startswith('Mitogen was disconnected') + + assert not self._connection.connected, \ + "Connection did not reset." + + try: + self._low_level_execute_command('kill -9 $PPID') + assert 0, 'AnsibleConnectionFailure was not raised' + except AnsibleConnectionFailure: + e = sys.exc_info()[1] + assert str(e).startswith('Mitogen was disconnected') diff --git a/tests/ansible/lib/action/mitogen_action_script.py b/tests/ansible/lib/action/mitogen_action_script.py new file mode 100644 index 00000000..e034345c --- /dev/null +++ b/tests/ansible/lib/action/mitogen_action_script.py @@ -0,0 +1,28 @@ +# I am an Ansible action plug-in. I run the script provided in the parameter in +# the context of the action. + +import sys + +from ansible.plugins.action import ActionBase + + +def execute(s, gbls, lcls): + if sys.version_info > (3,): + exec(s, gbls, lcls) + else: + exec('exec s in gbls, lcls') + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + super(ActionModule, self).run(tmp=tmp, task_vars=task_vars) + lcls = { + 'self': self, + 'result': {} + } + execute(self._task.args['script'], globals(), lcls) + return lcls['result'] + + +if __name__ == '__main__': + main() From 519faa3b3ba4e8c84ebf11ed0a2be8e3d6543c14 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 18:52:13 +0000 Subject: [PATCH 040/662] issue #369: add Connection.reset() test. --- tests/ansible/integration/connection/all.yml | 1 + .../ansible/integration/connection/reset.yml | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/ansible/integration/connection/reset.yml diff --git a/tests/ansible/integration/connection/all.yml b/tests/ansible/integration/connection/all.yml index 34e21f61..9c5a2837 100644 --- a/tests/ansible/integration/connection/all.yml +++ b/tests/ansible/integration/connection/all.yml @@ -5,3 +5,4 @@ - import_playbook: exec_command.yml - import_playbook: put_large_file.yml - import_playbook: put_small_file.yml +- import_playbook: reset.yml diff --git a/tests/ansible/integration/connection/reset.yml b/tests/ansible/integration/connection/reset.yml new file mode 100644 index 00000000..56e901b7 --- /dev/null +++ b/tests/ansible/integration/connection/reset.yml @@ -0,0 +1,38 @@ +# issue #369: Connection.reset() should cause destruction of the remote +# interpreter and any children. + +--- + +- name: integration/connection/reset.yml + hosts: test-targets + tasks: + - when: is_mitogen + block: + - custom_python_detect_environment: + register: out + + - custom_python_detect_environment: + become: true + register: out_become + + - meta: reset_connection + + - custom_python_detect_environment: + register: out2 + + - custom_python_detect_environment: + register: out_become2 + + - assert: + that: + # Interpreter PID has changed. + - out.pid != out2.pid + + # SSH PID has changed. + - out.ppid != out2.ppid + + # Interpreter PID has changed. + - out_become.pid != out_become2.pid + + # sudo PID has changed. + - out_become.ppid != out_become2.ppid From 9b7c958e2ef0ff96cc64baa87b63d001bb6b72e2 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 19:30:56 +0000 Subject: [PATCH 041/662] issue #369: refactor ContextService to support reset(). --- ansible_mitogen/services.py | 60 +++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index f9a1a3df..dde44c89 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -146,6 +146,25 @@ class ContextService(mitogen.service.Service): self._lru_by_via = {} #: :func:`key_from_dict` result by Context. self._key_by_context = {} + #: Mapping of Context -> parent Context + self._via_by_context = {} + + @mitogen.service.expose(mitogen.service.AllowParents()) + @mitogen.service.arg_spec({ + 'context': mitogen.core.Context + }) + def reset(self, context): + """ + Return a reference, forcing close and discard of the underlying + connection. Used for 'meta: reset_connection' or when some other error + is detected. + """ + LOG.debug('%r.reset(%r)', self, context) + self._lock.acquire() + try: + self._shutdown_unlocked(context) + finally: + self._lock.release() @mitogen.service.expose(mitogen.service.AllowParents()) @mitogen.service.arg_spec({ @@ -189,6 +208,19 @@ class ContextService(mitogen.service.Service): self._lock.release() return count + def _forget_context_unlocked(self, context): + key = self._key_by_context.get(context) + if key is None: + LOG.debug('%r: attempt to forget unknown %r', self, context) + return + + self._response_by_key.pop(key, None) + self._latches_by_key.pop(key, None) + self._key_by_context.pop(context, None) + self._refs_by_context.pop(context, None) + self._via_by_context.pop(context, None) + self._lru_by_via.pop(context, None) + def _shutdown_unlocked(self, context, lru=None, new_context=None): """ Arrange for `context` to be shut down, and optionally add `new_context` @@ -196,15 +228,15 @@ class ContextService(mitogen.service.Service): """ LOG.info('%r._shutdown_unlocked(): shutting down %r', self, context) context.shutdown() - - key = self._key_by_context[context] - del self._response_by_key[key] - del self._refs_by_context[context] - del self._key_by_context[context] - if lru and context in lru: - lru.remove(context) - if new_context: - lru.append(new_context) + via = self._via_by_context.get(context) + if via: + lru = self._lru_by_via.get(via) + if lru: + if context in lru: + lru.remove(context) + if new_context: + lru.append(new_context) + self._forget_context_unlocked(context) def _update_lru_unlocked(self, new_context, spec, via): """ @@ -225,6 +257,7 @@ class ContextService(mitogen.service.Service): 'but they are all marked as in-use.', via) return + self._via_by_context[new_context] = via self._shutdown_unlocked(context, lru=lru, new_context=new_context) def _update_lru(self, new_context, spec, via): @@ -243,7 +276,6 @@ class ContextService(mitogen.service.Service): try: for context in list(self._key_by_context): self._shutdown_unlocked(context) - self._lru_by_via = {} finally: self._lock.release() @@ -259,15 +291,11 @@ class ContextService(mitogen.service.Service): self._lock.acquire() try: routes = self.router.route_monitor.get_routes(stream) - for context, key in list(self._key_by_context.items()): + for context in list(self._key_by_context): if context.context_id in routes: LOG.info('Dropping %r due to disconnect of %r', context, stream) - self._response_by_key.pop(key, None) - self._latches_by_key.pop(key, None) - self._refs_by_context.pop(context, None) - self._lru_by_via.pop(context, None) - self._refs_by_context.pop(context, None) + self._forget_context_unlocked(context) finally: self._lock.release() From 33412927f5da65fca41af53fc5fa135e1a878db6 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 19:34:50 +0000 Subject: [PATCH 042/662] issue #369: refactor Connection to support reset() Now the tests pass. --- ansible_mitogen/connection.py | 69 +++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 708b6c13..33b551fb 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -550,7 +550,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): self.host_vars = task_vars['hostvars'] self.delegate_to_hostname = delegate_to_hostname self.loader_basedir = loader_basedir - self.close(new_task=True) + self._reset(mode='put') def get_task_var(self, key, default=None): if self._task_vars and key in self._task_vars: @@ -709,6 +709,20 @@ class Connection(ansible.plugins.connection.ConnectionBase): self.get_chain().call_no_reply(os.mkdir, self._shell.tmpdir) return self._shell.tmpdir + def _reset_tmp_path(self): + """ + Called by _reset(); ask the remote context to delete any temporary + directory created for the action. CallChain is not used here to ensure + exception is logged by the context on failure, since the CallChain + itself is about to be destructed. + """ + if getattr(self._shell, 'tmpdir', None) is not None: + self.context.call_no_reply( + ansible_mitogen.target.prune_tree, + self._shell.tmpdir, + ) + self._shell.tmpdir = None + def _connect(self): """ Establish a connection to the master process's UNIX listener socket, @@ -727,38 +741,53 @@ class Connection(ansible.plugins.connection.ConnectionBase): stack = self._build_stack() self._connect_stack(stack) - def close(self, new_task=False): + def _reset(self, mode): """ - Arrange for the mitogen.master.Router running in the worker to - gracefully shut down, and wait for shutdown to complete. Safe to call - multiple times. + Forget everything we know about the connected context. + + :param str mode: + Name of ContextService method to use to discard the context, either + 'put' or 'reset'. """ - if getattr(self._shell, 'tmpdir', None) is not None: - # Avoid CallChain to ensure exception is logged on failure. - self.context.call_no_reply( - ansible_mitogen.target.prune_tree, - self._shell.tmpdir, - ) - self._shell.tmpdir = None + if not self.context: + return - if self.context: - self.chain.reset() - self.parent.call_service( - service_name='ansible_mitogen.services.ContextService', - method_name='put', - context=self.context - ) + self._reset_tmp_path() + self.chain.reset() + self.parent.call_service( + service_name='ansible_mitogen.services.ContextService', + method_name=mode, + context=self.context + ) self.context = None self.login_context = None self.init_child_result = None self.chain = None - if self.broker and not new_task: + + def close(self): + """ + Arrange for the mitogen.master.Router running in the worker to + gracefully shut down, and wait for shutdown to complete. Safe to call + multiple times. + """ + self._reset(mode='put') + if self.broker: self.broker.shutdown() self.broker.join() self.broker = None self.router = None + def reset(self): + """ + Explicitly terminate the connection to the remote host. This discards + any local state we hold for the connection, returns the Connection to + the 'disconnected' state, and informs ContextService the connection is + bad somehow, and should be shut down and discarded. + """ + self._connect() + self._reset(mode='reset') + def get_chain(self, use_login=False, use_fork=False): """ Return the :class:`mitogen.parent.CallChain` to use for executing From 536690760d65d90e194266656c53dae11a82de18 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 19:35:17 +0000 Subject: [PATCH 043/662] issue #369: teach CallChain to reset the connection. --- ansible_mitogen/connection.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 33b551fb..f2725e9d 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -427,16 +427,30 @@ def config_from_hostvars(transport, inventory_name, connection, class CallChain(mitogen.parent.CallChain): + """ + Extend :class:`mitogen.parent.CallChain` to additionally cause the + associated :class:`Connection` to be reset if a ChannelError occurs. + + This only catches failures that occur while a call is pnding, it is a + stop-gap until a more general method is available to notice connection in + every situation. + """ call_aborted_msg = ( 'Mitogen was disconnected from the remote environment while a call ' 'was in-progress. If you feel this is in error, please file a bug. ' 'Original error was: %s' ) + def __init__(self, connection, context, pipelined=False): + super(CallChain, self).__init__(context, pipelined) + #: The connection to reset on CallError. + self._connection = connection + def _rethrow(self, recv): try: return recv.get().unpickle() except mitogen.core.ChannelError as e: + self._connection.reset() raise ansible.errors.AnsibleConnectionFailure( self.call_aborted_msg % (e,) ) @@ -682,7 +696,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): raise ansible.errors.AnsibleConnectionFailure(dct['msg']) self.context = dct['context'] - self.chain = CallChain(self.context, pipelined=True) + self.chain = CallChain(self, self.context, pipelined=True) if self._play_context.become: self.login_context = dct['via'] else: From b527ff0b6618959a820a050c5b2ed46b6b1d2c03 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 19:45:43 +0000 Subject: [PATCH 044/662] docs: update Changelog; closes #369. --- docs/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index ac9f37be..38e25272 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,15 @@ Mitogen for Ansible Enhancements ^^^^^^^^^^^^ +* `#369 `_: :meth:`Connection.reset` + is implemented, allowing `meta: reset_connection + `_ to shut + down the remote interpreter as expected, and improving support for the + `reboot + `_ + module. + + Fixes ^^^^^ From 16ca111ebdb08b901572d0bd446a7ab1a183944a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 10:17:40 +0000 Subject: [PATCH 045/662] ssh: better OpenSSH 7.5+ permission denied handling The user@host prefix in new-style OpenSSH messages unfortunately takes the host part from ~/.ssh/config and friends. There is no way to know which hostname will appear in this string without parsing the OpenSSH config, nor which username will appear. Instead just regex it. Add SSH stub modes to print the new/old errors and add some simple tests. This extends the work done in b9112a9cbb10da3200d07dcc5acc16b2a01b4af9 --- mitogen/ssh.py | 15 +++++++++------ tests/data/stubs/ssh.py | 18 ++++++++++++++++-- tests/ssh_test.py | 21 ++++++++++++++++++--- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/mitogen/ssh.py b/mitogen/ssh.py index ee97425b..1a545964 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -31,6 +31,7 @@ Functionality to allow establishing new slave contexts over an SSH connection. """ import logging +import re import time try: @@ -46,10 +47,16 @@ LOG = logging.getLogger('mitogen') # sshpass uses 'assword' because it doesn't lowercase the input. PASSWORD_PROMPT = b('password') -PERMDENIED_PROMPT = b('permission denied') HOSTKEY_REQ_PROMPT = b('are you sure you want to continue connecting (yes/no)?') HOSTKEY_FAIL = b('host key verification failed.') +# [user@host: ] permission denied +PERMDENIED_RE = re.compile( + ('(?:[^@]+@[^:]+: )?' # Absent in OpenSSH <7.5 + 'Permission denied').encode(), + re.I +) + DEBUG_PREFIXES = (b('debug1:'), b('debug2:'), b('debug3:')) @@ -289,11 +296,7 @@ class Stream(mitogen.parent.Stream): self._host_key_prompt() elif HOSTKEY_FAIL in buf.lower(): raise HostKeyError(self.hostkey_failed_msg) - elif buf.lower().startswith(( - PERMDENIED_PROMPT, - b("%s@%s: " % (self.username, self.hostname)) - + PERMDENIED_PROMPT, - )): + elif PERMDENIED_RE.match(buf): # issue #271: work around conflict with user shell reporting # 'permission denied' e.g. during chdir($HOME) by only matching # it at the start of the line. diff --git a/tests/data/stubs/ssh.py b/tests/data/stubs/ssh.py index 8df5aa39..63397479 100755 --- a/tests/data/stubs/ssh.py +++ b/tests/data/stubs/ssh.py @@ -15,6 +15,10 @@ Are you sure you want to continue connecting (yes/no)? HOST_KEY_STRICT_MSG = """Host key verification failed.\n""" +PERMDENIED_CLASSIC_MSG = 'Permission denied (publickey,password)\n' +PERMDENIED_75_MSG = 'chicken@nandos.com: permission denied (publickey,password)\n' + + def tty(msg): fp = open('/dev/tty', 'wb', 0) @@ -37,13 +41,23 @@ def confirm(msg): fp.close() -if os.getenv('FAKESSH_MODE') == 'ask': +mode = os.getenv('FAKESSH_MODE') + +if mode == 'ask': assert 'y\n' == confirm(HOST_KEY_ASK_MSG) -if os.getenv('FAKESSH_MODE') == 'strict': +elif mode == 'strict': stderr(HOST_KEY_STRICT_MSG) sys.exit(255) +elif mode == 'permdenied_classic': + stderr(PERMDENIED_CLASSIC_MSG) + sys.exit(255) + +elif mode == 'permdenied_75': + stderr(PERMDENIED_75_MSG) + sys.exit(255) + # # Set an env var if stderr was a TTY to make ssh_test tests easier to write. diff --git a/tests/ssh_test.py b/tests/ssh_test.py index 179b543d..ca614fa2 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -124,9 +124,10 @@ class BannerTest(testlib.DockerMixin, unittest2.TestCase): self.assertEquals(name, context.name) -class RequirePtyTest(testlib.DockerMixin, testlib.TestCase): - stream_class = mitogen.ssh.Stream - +class FakeSshMixin(testlib.RouterMixin): + """ + Mix-in that provides :meth:`fake_ssh` executing the stub 'ssh.py'. + """ def fake_ssh(self, FAKESSH_MODE=None, **kwargs): os.environ['FAKESSH_MODE'] = str(FAKESSH_MODE) try: @@ -139,6 +140,20 @@ class RequirePtyTest(testlib.DockerMixin, testlib.TestCase): finally: del os.environ['FAKESSH_MODE'] + +class PermissionDeniedTest(FakeSshMixin, testlib.TestCase): + def test_classic_prompt(self): + self.assertRaises(mitogen.ssh.PasswordError, + lambda: self.fake_ssh(FAKESSH_MODE='permdenied_classic')) + + def test_openssh_75_prompt(self): + self.assertRaises(mitogen.ssh.PasswordError, + lambda: self.fake_ssh(FAKESSH_MODE='permdenied_75')) + + +class RequirePtyTest(FakeSshMixin, testlib.TestCase): + stream_class = mitogen.ssh.Stream + def test_check_host_keys_accept(self): # required=true, host_key_checking=accept context = self.fake_ssh(FAKESSH_MODE='ask', check_host_keys='accept') From cf50b572f65b91ff73dcf5cdb3e6fc66f9eec26e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 10:24:53 +0000 Subject: [PATCH 046/662] docs: update ChangeLog. --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 38e25272..3d7cc21c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -51,6 +51,9 @@ Core Library signal the connection has broken, even when one participant is not a parent of the other. +* `16ca111e `_: handle OpenSSH + 7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present. + * `9ec360c2 `_: a new :meth:`mitogen.core.Broker.defer_sync` utility function is provided. From 1eae594e3239707ee5f5e69adde9574426a810c0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 29 Oct 2018 20:01:27 +0000 Subject: [PATCH 047/662] ssh: fix check_host_keys="accept" and test; closes #411 Add real accept/enforce tests. --- docs/changelog.rst | 5 +++ mitogen/ssh.py | 2 +- tests/data/stubs/ssh.py | 5 +-- tests/ssh_test.py | 94 +++++++++++++++++++++++++++++------------ 4 files changed, 76 insertions(+), 30 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3d7cc21c..a1bbbdf0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -51,6 +51,11 @@ Core Library signal the connection has broken, even when one participant is not a parent of the other. +* `#411 `_: the SSH method typed + "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` + was configured. This would lead to connection timeouts due to the hung + response. + * `16ca111e `_: handle OpenSSH 7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present. diff --git a/mitogen/ssh.py b/mitogen/ssh.py index 1a545964..fba6e8f2 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -265,7 +265,7 @@ class Stream(mitogen.parent.Stream): def _host_key_prompt(self): if self.check_host_keys == 'accept': LOG.debug('%r: accepting host key', self) - self.tty_stream.transmit_side.write(b('y\n')) + self.tty_stream.transmit_side.write(b('yes\n')) return # _host_key_prompt() should never be reached with ignore or enforce diff --git a/tests/data/stubs/ssh.py b/tests/data/stubs/ssh.py index 63397479..80c02835 100755 --- a/tests/data/stubs/ssh.py +++ b/tests/data/stubs/ssh.py @@ -19,7 +19,6 @@ PERMDENIED_CLASSIC_MSG = 'Permission denied (publickey,password)\n' PERMDENIED_75_MSG = 'chicken@nandos.com: permission denied (publickey,password)\n' - def tty(msg): fp = open('/dev/tty', 'wb', 0) fp.write(msg.encode()) @@ -41,10 +40,10 @@ def confirm(msg): fp.close() -mode = os.getenv('FAKESSH_MODE') +mode = os.getenv('STUBSSH_MODE') if mode == 'ask': - assert 'y\n' == confirm(HOST_KEY_ASK_MSG) + assert 'yes\n' == confirm(HOST_KEY_ASK_MSG) elif mode == 'strict': stderr(HOST_KEY_STRICT_MSG) diff --git a/tests/ssh_test.py b/tests/ssh_test.py index ca614fa2..bdee30dd 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -1,5 +1,6 @@ import os import sys +import tempfile import mitogen import mitogen.ssh @@ -11,6 +12,23 @@ import testlib import plain_old_module +class StubSshMixin(testlib.RouterMixin): + """ + Mix-in that provides :meth:`stub_ssh` executing the stub 'ssh.py'. + """ + def stub_ssh(self, STUBSSH_MODE=None, **kwargs): + os.environ['STUBSSH_MODE'] = str(STUBSSH_MODE) + try: + return self.router.ssh( + hostname='hostname', + username='mitogen__has_sudo', + ssh_path=testlib.data_path('stubs/ssh.py'), + **kwargs + ) + finally: + del os.environ['STUBSSH_MODE'] + + class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): def test_okay(self): context = self.router.ssh( @@ -23,7 +41,7 @@ class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): self.assertEquals(3, context.call(plain_old_module.add, 1, 2)) -class SshTest(testlib.DockerMixin, unittest2.TestCase): +class SshTest(testlib.DockerMixin, testlib.TestCase): stream_class = mitogen.ssh.Stream def test_stream_name(self): @@ -105,6 +123,47 @@ class SshTest(testlib.DockerMixin, unittest2.TestCase): context.call(plain_old_module.get_sentinel_value), ) + def test_enforce_unknown_host_key(self): + fp = tempfile.NamedTemporaryFile() + try: + e = self.assertRaises(mitogen.ssh.HostKeyError, + lambda: self.docker_ssh( + username='mitogen__has_sudo_pubkey', + password='has_sudo_password', + ssh_args=['-o', 'UserKnownHostsFile ' + fp.name], + check_host_keys='enforce', + ) + ) + self.assertEquals(e.args[0], mitogen.ssh.Stream.hostkey_failed_msg) + finally: + fp.close() + + def test_accept_enforce_host_keys(self): + fp = tempfile.NamedTemporaryFile() + try: + context = self.docker_ssh( + username='mitogen__has_sudo', + password='has_sudo_password', + ssh_args=['-o', 'UserKnownHostsFile ' + fp.name], + check_host_keys='accept', + ) + context.shutdown(wait=True) + + fp.seek(0) + # Lame test, but we're about to use enforce mode anyway, which + # verifies the file contents. + self.assertTrue(len(fp.read()) > 0) + + context = self.docker_ssh( + username='mitogen__has_sudo', + password='has_sudo_password', + ssh_args=['-o', 'UserKnownHostsFile ' + fp.name], + check_host_keys='enforce', + ) + context.shutdown(wait=True) + finally: + fp.close() + class BannerTest(testlib.DockerMixin, unittest2.TestCase): # Verify the ability to disambiguate random spam appearing in the SSHd's @@ -124,54 +183,37 @@ class BannerTest(testlib.DockerMixin, unittest2.TestCase): self.assertEquals(name, context.name) -class FakeSshMixin(testlib.RouterMixin): - """ - Mix-in that provides :meth:`fake_ssh` executing the stub 'ssh.py'. - """ - def fake_ssh(self, FAKESSH_MODE=None, **kwargs): - os.environ['FAKESSH_MODE'] = str(FAKESSH_MODE) - try: - return self.router.ssh( - hostname='hostname', - username='mitogen__has_sudo', - ssh_path=testlib.data_path('stubs/ssh.py'), - **kwargs - ) - finally: - del os.environ['FAKESSH_MODE'] - - -class PermissionDeniedTest(FakeSshMixin, testlib.TestCase): +class StubPermissionDeniedTest(StubSshMixin, testlib.TestCase): def test_classic_prompt(self): self.assertRaises(mitogen.ssh.PasswordError, - lambda: self.fake_ssh(FAKESSH_MODE='permdenied_classic')) + lambda: self.stub_ssh(STUBSSH_MODE='permdenied_classic')) def test_openssh_75_prompt(self): self.assertRaises(mitogen.ssh.PasswordError, - lambda: self.fake_ssh(FAKESSH_MODE='permdenied_75')) + lambda: self.stub_ssh(STUBSSH_MODE='permdenied_75')) -class RequirePtyTest(FakeSshMixin, testlib.TestCase): +class StubCheckHostKeysTest(StubSshMixin, testlib.TestCase): stream_class = mitogen.ssh.Stream def test_check_host_keys_accept(self): # required=true, host_key_checking=accept - context = self.fake_ssh(FAKESSH_MODE='ask', check_host_keys='accept') + context = self.stub_ssh(STUBSSH_MODE='ask', check_host_keys='accept') self.assertEquals('1', context.call(os.getenv, 'STDERR_WAS_TTY')) def test_check_host_keys_enforce(self): # required=false, host_key_checking=enforce - context = self.fake_ssh(check_host_keys='enforce') + context = self.stub_ssh(check_host_keys='enforce') self.assertEquals(None, context.call(os.getenv, 'STDERR_WAS_TTY')) def test_check_host_keys_ignore(self): # required=false, host_key_checking=ignore - context = self.fake_ssh(check_host_keys='ignore') + context = self.stub_ssh(check_host_keys='ignore') self.assertEquals(None, context.call(os.getenv, 'STDERR_WAS_TTY')) def test_password_present(self): # required=true, password is not None - context = self.fake_ssh(check_host_keys='ignore', password='willick') + context = self.stub_ssh(check_host_keys='ignore', password='willick') self.assertEquals('1', context.call(os.getenv, 'STDERR_WAS_TTY')) From 1cbff1011e9353b8a1b125efbfebe210426921b1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 10:53:35 +0000 Subject: [PATCH 048/662] core: send dead message if max message size exceeded; closes #405 --- mitogen/core.py | 20 +++++++++++--------- tests/router_test.py | 26 +++++++++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 01be1cfe..642540a5 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1832,11 +1832,17 @@ class Router(object): except Exception: LOG.exception('%r._invoke(%r): %r crashed', self, msg, fn) + def _maybe_send_dead(self, msg): + if msg.reply_to and not msg.is_dead: + msg.reply(Message.dead(), router=self) + def _async_route(self, msg, in_stream=None): _vv and IOLOG.debug('%r._async_route(%r, %r)', self, msg, in_stream) + if len(msg.data) > self.max_message_size: LOG.error('message too large (max %d bytes): %r', self.max_message_size, msg) + self._maybe_send_dead(msg) return # Perform source verification. @@ -1868,22 +1874,18 @@ class Router(object): if out_stream is None: out_stream = self._stream_by_id.get(mitogen.parent_id) - dead = False if out_stream is None: if msg.reply_to not in (0, IS_DEAD): LOG.error('%r: no route for %r, my ID is %r', self, msg, mitogen.context_id) - dead = True + self._maybe_send_dead(msg) + return - if in_stream and self.unidirectional and not dead and \ - not (in_stream.is_privileged or out_stream.is_privileged): + if in_stream and self.unidirectional and not \ + (in_stream.is_privileged or out_stream.is_privileged): LOG.error('routing mode prevents forward of %r from %r -> %r', msg, in_stream, out_stream) - dead = True - - if dead: - if msg.reply_to and not msg.is_dead: - msg.reply(Message.dead(), router=self) + self._maybe_send_dead(msg) return out_stream._send(msg) diff --git a/tests/router_test.py b/tests/router_test.py index d0e4f539..7b7e2896 100644 --- a/tests/router_test.py +++ b/tests/router_test.py @@ -1,6 +1,7 @@ import logging import subprocess import time +import zlib import unittest2 @@ -189,20 +190,35 @@ class AddHandlerTest(unittest2.TestCase): self.assertTrue(queue.get(timeout=5).is_dead) -class MessageSizeTest(testlib.BrokerMixin, unittest2.TestCase): +class MessageSizeTest(testlib.BrokerMixin, testlib.TestCase): klass = mitogen.master.Router def test_local_exceeded(self): router = self.klass(broker=self.broker, max_message_size=4096) - recv = mitogen.core.Receiver(router) logs = testlib.LogCapturer() logs.start() - sem = mitogen.core.Latch() + # Send message and block for one IO loop, so _async_route can run. router.route(mitogen.core.Message.pickled(' '*8192)) - router.broker.defer(sem.put, ' ') # wlil always run after _async_route - sem.get() + router.broker.defer_sync(lambda: None) + + expect = 'message too large (max 4096 bytes)' + self.assertTrue(expect in logs.stop()) + + def test_local_dead_message(self): + # Local router should generate dead message when reply_to is set. + router = self.klass(broker=self.broker, max_message_size=4096) + + logs = testlib.LogCapturer() + logs.start() + + # Try function call. Receiver should be woken by a dead message sent by + # router due to message size exceeded. + child = router.fork() + e = self.assertRaises(mitogen.core.ChannelError, + lambda: child.call(zlib.crc32, ' '*8192)) + self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) expect = 'message too large (max 4096 bytes)' self.assertTrue(expect in logs.stop()) From d81698c43a0626b0209b1409bd682de234c36b9b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 10:55:26 +0000 Subject: [PATCH 049/662] docs: update Changelog. --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a1bbbdf0..0d7d9766 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -51,6 +51,11 @@ Core Library signal the connection has broken, even when one participant is not a parent of the other. +* `#405 `_: if a message is rejected + due to being too large, and it has a ``reply_to`` set, a dead message is + returned to the sender. This ensures function calls exceeding the configured + maximum size crash rather than hang. + * `#411 `_: the SSH method typed "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` was configured. This would lead to connection timeouts due to the hung From 9aa76cf9ceab6e5f0e19ae4009728870f92260af Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 11:20:32 +0000 Subject: [PATCH 050/662] tests: better Docker test key comment. --- tests/data/docker/mitogen__has_sudo_pubkey.key.pub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/docker/mitogen__has_sudo_pubkey.key.pub b/tests/data/docker/mitogen__has_sudo_pubkey.key.pub index 245ce379..b132d993 100644 --- a/tests/data/docker/mitogen__has_sudo_pubkey.key.pub +++ b/tests/data/docker/mitogen__has_sudo_pubkey.key.pub @@ -1 +1 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkMz7vE4piReKXBNarhGhzfMr6g7capaUHllxThmtm4ndlM3kbiEFvxI9P7s17T50CycfesJf5/1bmLxACROtdMGrgBrCAAGwEy2qnCNPhqrLpd2amoLUkBcthmiaTVmU+eMMHm8ubxh0qEauXOaaVqXTGcK1bGMsufLYGr0lv5RE2AErg9jPYkh6qT0CpxGtRmfbYubFAIunP5gxHgiOQrD7Yzs2NFDqPq9rRuvRMGX/XLpDurFm9x16LTx1fDSU1aqmu88QMJtXoMyPlHCqd5x/FdZ1KorR79LB+H/cptB1/ND1geZv5OAD8ydCc3nNGi8hiyPobb6jOX68agXyX dmw@Eldil.local +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkMz7vE4piReKXBNarhGhzfMr6g7capaUHllxThmtm4ndlM3kbiEFvxI9P7s17T50CycfesJf5/1bmLxACROtdMGrgBrCAAGwEy2qnCNPhqrLpd2amoLUkBcthmiaTVmU+eMMHm8ubxh0qEauXOaaVqXTGcK1bGMsufLYGr0lv5RE2AErg9jPYkh6qT0CpxGtRmfbYubFAIunP5gxHgiOQrD7Yzs2NFDqPq9rRuvRMGX/XLpDurFm9x16LTx1fDSU1aqmu88QMJtXoMyPlHCqd5x/FdZ1KorR79LB+H/cptB1/ND1geZv5OAD8ydCc3nNGi8hiyPobb6jOX68agXyX mitogen__has_sudo_pubkey@testdata From 96f000c5ea1fa3e2330106dbd21fcbf3d6a64a2b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 14:58:35 +0000 Subject: [PATCH 051/662] ansible: tilde-expand SSH key before passing to SSH; closes #334. --- ansible_mitogen/connection.py | 8 +++++++- tests/ansible/integration/ssh/all.yml | 1 + tests/ansible/integration/ssh/config.yml | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/ansible/integration/ssh/config.yml diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index f2725e9d..df10884a 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -92,6 +92,12 @@ def _connect_ssh(spec): else: check_host_keys = 'ignore' + # #334: tilde-expand private_key_file to avoid implementation difference + # between Python and OpenSSH. + private_key_file = spec['private_key_file'] + if private_key_file is not None: + private_key_file = os.path.expanduser(private_key_file) + return { 'method': 'ssh', 'kwargs': { @@ -101,7 +107,7 @@ def _connect_ssh(spec): 'password': optional_secret(spec['password']), 'port': spec['port'], 'python_path': spec['python_path'], - 'identity_file': spec['private_key_file'], + 'identity_file': private_key_file, 'identities_only': False, 'ssh_path': spec['ssh_executable'], 'connect_timeout': spec['ansible_ssh_timeout'], diff --git a/tests/ansible/integration/ssh/all.yml b/tests/ansible/integration/ssh/all.yml index 2425943a..a8335ab7 100644 --- a/tests/ansible/integration/ssh/all.yml +++ b/tests/ansible/integration/ssh/all.yml @@ -1,2 +1,3 @@ +- import_playbook: config.yml - import_playbook: timeouts.yml - import_playbook: variables.yml diff --git a/tests/ansible/integration/ssh/config.yml b/tests/ansible/integration/ssh/config.yml new file mode 100644 index 00000000..07ad1c21 --- /dev/null +++ b/tests/ansible/integration/ssh/config.yml @@ -0,0 +1,19 @@ +# issue #334: test expanduser() on key file during config generation. + +- name: integration/ssh/config.yml + hosts: test-targets + connection: ssh + vars: + ansible_private_key_file: ~/fakekey + tasks: + - meta: end_play + when: not is_mitogen + + - mitogen_get_stack: + register: out + + - assert: + that: | + out.result[0].kwargs.identity_file == ( + lookup('env', 'HOME') + '/fakekey' + ) From 766dce9a5952fb0879903f49ec9e343b38835161 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 15:02:46 +0000 Subject: [PATCH 052/662] docs: update Changelog --- docs/changelog.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0d7d9766..c1fe401e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -36,6 +36,17 @@ Enhancements Fixes ^^^^^ +* `#334 `_: the SSH method + tilde-expands private key paths using Ansible's logic. Previously Mitogen + passed the path unmodified to SSH, which would expand it using + :func:`os.getpwent`. + + This differs from :func:`os.path.expanduser`, which prefers the ``HOME`` + environment variable if it is set, causing behaviour to diverge when Ansible + was invoked using sudo without appropriate flags to cause the ``HOME`` + environment variable to be reset to match the target account. + + Core Library ~~~~~~~~~~~~ @@ -68,6 +79,14 @@ Core Library :meth:`mitogen.core.Broker.defer_sync` utility function is provided. +Thanks! +~~~~~~~ + +Mitogen would not be possible without the support of users. A huge thanks for +bug reports, features and fixes in this release contributed by +`Guy Knights `_. + + v0.2.3 (2018-10-23) ------------------- From 1198164ce6e7e21c7d82d8b60e6f66918f48c67a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 15:24:50 +0000 Subject: [PATCH 053/662] docs: Changelog typos. --- docs/changelog.rst | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c1fe401e..96a4fedc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -37,14 +37,14 @@ Fixes ^^^^^ * `#334 `_: the SSH method - tilde-expands private key paths using Ansible's logic. Previously Mitogen - passed the path unmodified to SSH, which would expand it using - :func:`os.getpwent`. + tilde-expands private key paths using Ansible's logic. Previously Mitogen + passed the path unmodified to SSH, which would expand it using + :func:`os.getpwent`. - This differs from :func:`os.path.expanduser`, which prefers the ``HOME`` - environment variable if it is set, causing behaviour to diverge when Ansible - was invoked using sudo without appropriate flags to cause the ``HOME`` - environment variable to be reset to match the target account. + This differs from :func:`os.path.expanduser`, which prefers the ``HOME`` + environment variable if it is set, causing behaviour to diverge when Ansible + was invoked using sudo without appropriate flags to cause the ``HOME`` + environment variable to be reset to match the target account. Core Library @@ -56,11 +56,10 @@ Core Library every stream that ever communicated with a disappearing peer, rather than simply toward parents. - Conversations between nodes in any level of the connection tree should - correctly receive ``DEL_ROUTE`` messages when a participant disconnects, - allowing receivers to be woken with :class:`mitogen.core.ChannelError` to - signal the connection has broken, even when one participant is not a parent - of the other. + Conversations between nodes in any level of the tree receive ``DEL_ROUTE`` + messages when a participant disconnects, allowing receivers to be woken with + :class:`mitogen.core.ChannelError` to signal the connection has broken, even + when one participant is not a parent of the other. * `#405 `_: if a message is rejected due to being too large, and it has a ``reply_to`` set, a dead message is @@ -68,9 +67,9 @@ Core Library maximum size crash rather than hang. * `#411 `_: the SSH method typed - "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` - was configured. This would lead to connection timeouts due to the hung - response. + "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` + was configured. This would lead to connection timeouts due to the hung + response. * `16ca111e `_: handle OpenSSH 7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present. From 73cda2994f81567ecad059e969b15642dbf9a4b5 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 21:20:33 +0000 Subject: [PATCH 054/662] issue #333: add versioning, initial batch of poller tests Now poller is start enough to know a start_receive() during an iteration does not cause events yielded by that iteration to associate with the wrong descriptor. These changes are tangentially related to the associated ticket, but event versioning is still the underlying issue. --- mitogen/core.py | 27 ++-- mitogen/parent.py | 28 ++-- tests/poller_test.py | 332 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 363 insertions(+), 24 deletions(-) create mode 100644 tests/poller_test.py diff --git a/mitogen/core.py b/mitogen/core.py index 642540a5..d3909773 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1286,17 +1286,20 @@ def _unpickle_context(router, context_id, name): class Poller(object): + #: Increments on every poll(). Used to version _rfds and _wfds. + _generation = 1 + def __init__(self): self._rfds = {} self._wfds = {} @property def readers(self): - return list(self._rfds.items()) + return list((fd, data) for fd, (data, gen) in self._rfds.items()) @property def writers(self): - return list(self._wfds.items()) + return list((fd, data) for fd, (data, gen) in self._wfds.items()) def __repr__(self): return '%s(%#x)' % (type(self).__name__, id(self)) @@ -1305,19 +1308,18 @@ class Poller(object): pass def start_receive(self, fd, data=None): - self._rfds[fd] = data or fd + self._rfds[fd] = (data or fd, self._generation) def stop_receive(self, fd): self._rfds.pop(fd, None) def start_transmit(self, fd, data=None): - self._wfds[fd] = data or fd + self._wfds[fd] = (data or fd, self._generation) def stop_transmit(self, fd): self._wfds.pop(fd, None) - def poll(self, timeout=None): - _vv and IOLOG.debug('%r.poll(%r)', self, timeout) + def _poll(self, timeout): (rfds, wfds, _), _ = io_op(select.select, self._rfds, self._wfds, @@ -1326,11 +1328,20 @@ class Poller(object): for fd in rfds: _vv and IOLOG.debug('%r: POLLIN for %r', self, fd) - yield self._rfds[fd] + data, gen = self._rfds.get(fd, (None, None)) + if gen and gen < self._generation: + yield data for fd in wfds: _vv and IOLOG.debug('%r: POLLOUT for %r', self, fd) - yield self._wfds[fd] + data, gen = self._wfds.get(fd, (None, None)) + if gen and gen < self._generation: + yield data + + def poll(self, timeout=None): + _vv and IOLOG.debug('%r.poll(%r)', self, timeout) + self._generation += 1 + return self._poll(timeout) class Latch(object): diff --git a/mitogen/parent.py b/mitogen/parent.py index 9e878e3f..af9dd322 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -681,14 +681,6 @@ class KqueuePoller(mitogen.core.Poller): def close(self): self._kqueue.close() - @property - def readers(self): - return list(self._rfds.items()) - - @property - def writers(self): - return list(self._wfds.items()) - def _control(self, fd, filters, flags): mitogen.core._vv and IOLOG.debug( '%r._control(%r, %r, %r)', self, fd, filters, flags) @@ -707,7 +699,7 @@ class KqueuePoller(mitogen.core.Poller): self, fd, data) if fd not in self._rfds: self._control(fd, select.KQ_FILTER_READ, select.KQ_EV_ADD) - self._rfds[fd] = data or fd + self._rfds[fd] = (data or fd, self._generation) def stop_receive(self, fd): mitogen.core._vv and IOLOG.debug('%r.stop_receive(%r)', self, fd) @@ -720,7 +712,7 @@ class KqueuePoller(mitogen.core.Poller): self, fd, data) if fd not in self._wfds: self._control(fd, select.KQ_FILTER_WRITE, select.KQ_EV_ADD) - self._wfds[fd] = data or fd + self._wfds[fd] = (data or fd, self._generation) def stop_transmit(self, fd): mitogen.core._vv and IOLOG.debug('%r.stop_transmit(%r)', self, fd) @@ -728,7 +720,7 @@ class KqueuePoller(mitogen.core.Poller): self._control(fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE) del self._wfds[fd] - def poll(self, timeout=None): + def _poll(self, timeout): changelist = self._changelist self._changelist = [] events, _ = mitogen.core.io_op(self._kqueue.control, @@ -738,13 +730,17 @@ class KqueuePoller(mitogen.core.Poller): if event.flags & select.KQ_EV_ERROR: LOG.debug('ignoring stale event for fd %r: errno=%d: %s', fd, event.data, errno.errorcode.get(event.data)) - elif event.filter == select.KQ_FILTER_READ and fd in self._rfds: + elif event.filter == select.KQ_FILTER_READ: + data, gen = self._rfds.get(fd, (None, None)) # Events can still be read for an already-discarded fd. - mitogen.core._vv and IOLOG.debug('%r: POLLIN: %r', self, fd) - yield self._rfds[fd] + if gen and gen < self._generation: + mitogen.core._vv and IOLOG.debug('%r: POLLIN: %r', self, fd) + yield data elif event.filter == select.KQ_FILTER_WRITE and fd in self._wfds: - mitogen.core._vv and IOLOG.debug('%r: POLLOUT: %r', self, fd) - yield self._wfds[fd] + data, gen = self._wfds.get(fd, (None, None)) + if gen and gen < self._generation: + mitogen.core._vv and IOLOG.debug('%r: POLLOUT: %r', self, fd) + yield data class EpollPoller(mitogen.core.Poller): diff --git a/tests/poller_test.py b/tests/poller_test.py new file mode 100644 index 00000000..d1d7c6ea --- /dev/null +++ b/tests/poller_test.py @@ -0,0 +1,332 @@ + +import errno +import os +import select +import socket +import sys +import time + +import unittest2 + +import mitogen.core +import mitogen.parent + +import testlib + + +class SockMixin(object): + def tearDown(self): + self._teardown_socks() + super(SockMixin, self).tearDown() + + def setUp(self): + super(SockMixin, self).setUp() + self._setup_socks() + + def _setup_socks(self): + # "left" and "right" side of two socket pairs. We use sockets instead + # of pipes since the same process can manipulate transmit/receive + # buffers on both sides (bidirectional IO), making it easier to test + # combinations of readability/writeability on the one side of a single + # file object. + self.l1_sock, self.r1_sock = socket.socketpair() + self.l1 = self.l1_sock.fileno() + self.r1 = self.r1_sock.fileno() + + self.l2_sock, self.r2_sock = socket.socketpair() + self.l2 = self.l2_sock.fileno() + self.r2 = self.r2_sock.fileno() + for fd in self.l1, self.r1, self.l2, self.r2: + mitogen.core.set_nonblock(fd) + + def fill(self, fd): + """Make `fd` unwriteable.""" + while True: + try: + os.write(fd, 'x'*4096) + except OSError: + e = sys.exc_info()[1] + if e.args[0] == errno.EAGAIN: + return + raise + + def drain(self, fd): + """Make `fd` unreadable.""" + while True: + try: + if not os.read(fd, 4096): + return + except OSError: + e = sys.exc_info()[1] + if e.args[0] == errno.EAGAIN: + return + raise + + def _teardown_socks(self): + for sock in self.l1_sock, self.r1_sock, self.l2_sock, self.r2_sock: + sock.close() + + +class PollerMixin(object): + klass = None + + def setUp(self): + super(PollerMixin, self).setUp() + self.p = self.klass() + + def tearDown(self): + self.p.close() + super(PollerMixin, self).tearDown() + + +class ReceiveStateMixin(PollerMixin, SockMixin): + def test_start_receive_adds_reader(self): + self.p.start_receive(self.l1) + self.assertEquals([(self.l1, self.l1)], self.p.readers) + self.assertEquals([], self.p.writers) + + def test_start_receive_adds_reader_data(self): + data = object() + self.p.start_receive(self.l1, data=data) + self.assertEquals([(self.l1, data)], self.p.readers) + self.assertEquals([], self.p.writers) + + def test_stop_receive(self): + self.p.start_receive(self.l1) + self.p.stop_receive(self.l1) + self.assertEquals([], self.p.readers) + self.assertEquals([], self.p.writers) + + def test_stop_receive_dup(self): + self.p.start_receive(self.l1) + self.p.stop_receive(self.l1) + self.assertEquals([], self.p.readers) + self.assertEquals([], self.p.writers) + self.p.stop_receive(self.l1) + self.assertEquals([], self.p.readers) + self.assertEquals([], self.p.writers) + + def test_stop_receive_noexist(self): + p = self.klass() + p.stop_receive(123) # should not fail + self.assertEquals([], p.readers) + self.assertEquals([], self.p.writers) + + +class TransmitStateMixin(PollerMixin, SockMixin): + def test_start_transmit_adds_writer(self): + self.p.start_transmit(self.r1) + self.assertEquals([], self.p.readers) + self.assertEquals([(self.r1, self.r1)], self.p.writers) + + def test_start_transmit_adds_writer_data(self): + data = object() + self.p.start_transmit(self.r1, data=data) + self.assertEquals([], self.p.readers) + self.assertEquals([(self.r1, data)], self.p.writers) + + def test_stop_transmit(self): + self.p.start_transmit(self.r1) + self.p.stop_transmit(self.r1) + self.assertEquals([], self.p.readers) + self.assertEquals([], self.p.writers) + + def test_stop_transmit_dup(self): + self.p.start_transmit(self.r1) + self.p.stop_transmit(self.r1) + self.assertEquals([], self.p.readers) + self.assertEquals([], self.p.writers) + self.p.stop_transmit(self.r1) + self.assertEquals([], self.p.readers) + self.assertEquals([], self.p.writers) + + def test_stop_transmit_noexist(self): + p = self.klass() + p.stop_receive(123) # should not fail + self.assertEquals([], p.readers) + self.assertEquals([], self.p.writers) + + +class CloseMixin(PollerMixin): + def test_single_close(self): + self.p.close() + + def test_double_close(self): + self.p.close() + self.p.close() + + +class PollMixin(PollerMixin): + def test_empty_zero_timeout(self): + t0 = time.time() + self.assertEquals([], list(self.p.poll(0))) + self.assertTrue((time.time() - t0) < .1) # vaguely reasonable + + def test_empty_small_timeout(self): + t0 = time.time() + self.assertEquals([], list(self.p.poll(.2))) + self.assertTrue((time.time() - t0) >= .2) + + +class ReadableMixin(PollerMixin, SockMixin): + def test_unreadable(self): + self.p.start_receive(self.l1) + self.assertEquals([], list(self.p.poll(0))) + + def test_readable_before_add(self): + self.fill(self.r1) + self.p.start_receive(self.l1) + self.assertEquals([self.l1], list(self.p.poll(0))) + + def test_readable_after_add(self): + self.p.start_receive(self.l1) + self.fill(self.r1) + self.assertEquals([self.l1], list(self.p.poll(0))) + + def test_readable_then_unreadable(self): + self.fill(self.r1) + self.p.start_receive(self.l1) + self.assertEquals([self.l1], list(self.p.poll(0))) + self.drain(self.l1) + self.assertEquals([], list(self.p.poll(0))) + + def test_readable_data(self): + data = object() + self.fill(self.r1) + self.p.start_receive(self.l1, data=data) + self.assertEquals([data], list(self.p.poll(0))) + + def test_double_readable_data(self): + data1 = object() + data2 = object() + self.fill(self.r1) + self.p.start_receive(self.l1, data=data1) + self.fill(self.r2) + self.p.start_receive(self.l2, data=data2) + self.assertEquals(set([data1, data2]), set(self.p.poll(0))) + + +class WriteableMixin(PollerMixin, SockMixin): + def test_writeable(self): + self.p.start_transmit(self.r1) + self.assertEquals([self.r1], list(self.p.poll(0))) + + def test_writeable_data(self): + data = object() + self.p.start_transmit(self.r1, data=data) + self.assertEquals([data], list(self.p.poll(0))) + + def test_unwriteable_before_add(self): + self.fill(self.r1) + self.p.start_transmit(self.r1) + self.assertEquals([], list(self.p.poll(0))) + + def test_unwriteable_after_add(self): + self.p.start_transmit(self.r1) + self.fill(self.r1) + self.assertEquals([], list(self.p.poll(0))) + + def test_unwriteable_then_writeable(self): + self.fill(self.r1) + self.p.start_transmit(self.r1) + self.assertEquals([], list(self.p.poll(0))) + self.drain(self.l1) + self.assertEquals([self.r1], list(self.p.poll(0))) + + def test_double_unwriteable_then_Writeable(self): + self.fill(self.r1) + self.p.start_transmit(self.r1) + + self.fill(self.r2) + self.p.start_transmit(self.r2) + + self.assertEquals([], list(self.p.poll(0))) + + self.drain(self.l1) + self.assertEquals([self.r1], list(self.p.poll(0))) + + self.drain(self.l2) + self.assertEquals(set([self.r1, self.r2]), set(self.p.poll(0))) + + +class MutateDuringYieldMixin(PollerMixin, SockMixin): + # verify behaviour when poller contents is modified in the middle of + # poll() output generation. + + def test_one_readable_removed_before_yield(self): + self.fill(self.l1) + self.p.start_receive(self.r1) + p = self.p.poll(0) + self.p.stop_receive(self.r1) + self.assertEquals([], list(p)) + + def test_one_writeable_removed_before_yield(self): + self.p.start_transmit(self.r1) + p = self.p.poll(0) + self.p.stop_transmit(self.r1) + self.assertEquals([], list(p)) + + def test_one_readable_readded_before_yield(self): + # fd removed, closed, another fd opened, gets same fd number, re-added. + # event fires for wrong underlying object. + self.fill(self.l1) + self.p.start_receive(self.r1) + p = self.p.poll(0) + self.p.stop_receive(self.r1) + self.p.start_receive(self.r1) + self.assertEquals([], list(p)) + + def test_one_readable_readded_during_yield(self): + self.fill(self.l1) + self.p.start_receive(self.r1) + + self.fill(self.l2) + self.p.start_receive(self.r2) + + p = self.p.poll(0) + + # figure out which one is consumed and which is still to-read. + consumed = next(p) + ready = (self.r1, self.r2)[consumed == self.r1] + + # now remove and re-add the one that hasn't been read yet. + self.p.stop_receive(ready) + self.p.start_receive(ready) + + # the start_receive() may be for a totally new underlying file object, + # the live loop iteration must not yield any buffered readiness event. + self.assertEquals([], list(p)) + + +class AllMixin(ReceiveStateMixin, + TransmitStateMixin, + ReadableMixin, + WriteableMixin, + MutateDuringYieldMixin, + PollMixin, + CloseMixin): + """ + Helper to avoid cutpasting mixin names below. + """ + + +@unittest2.skipIf(condition=not hasattr(select, 'select'), + reason='select.select() not supported') +class SelectTest(AllMixin, testlib.TestCase): + klass = mitogen.core.Poller + + +@unittest2.skipIf(condition=not hasattr(select, 'kqueue'), + reason='select.kqueue() not supported') +class KqueueTest(AllMixin, testlib.TestCase): + klass = mitogen.parent.KqueuePoller + + +@unittest2.skipIf(condition=not hasattr(select, 'epoll'), + reason='select.epoll() not supported') +class EpollTest(AllMixin, testlib.TestCase): + klass = mitogen.parent.EpollPoller + + +if __name__ == '__main__': + unittest2.main() From 22b4b186d77b236ea1b2fe292771e7e9880d1cdd Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 21:26:54 +0000 Subject: [PATCH 055/662] issue #333: add versioning to EpollPoller too. --- mitogen/parent.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/mitogen/parent.py b/mitogen/parent.py index af9dd322..8f7d68ea 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -755,14 +755,6 @@ class EpollPoller(mitogen.core.Poller): def close(self): self._epoll.close() - @property - def readers(self): - return list(self._rfds.items()) - - @property - def writers(self): - return list(self._wfds.items()) - def _control(self, fd): mitogen.core._vv and IOLOG.debug('%r._control(%r)', self, fd) mask = (((fd in self._rfds) and select.EPOLLIN) | @@ -780,7 +772,7 @@ class EpollPoller(mitogen.core.Poller): def start_receive(self, fd, data=None): mitogen.core._vv and IOLOG.debug('%r.start_receive(%r, %r)', self, fd, data) - self._rfds[fd] = data or fd + self._rfds[fd] = (data or fd, self._generation) self._control(fd) def stop_receive(self, fd): @@ -791,7 +783,7 @@ class EpollPoller(mitogen.core.Poller): def start_transmit(self, fd, data=None): mitogen.core._vv and IOLOG.debug('%r.start_transmit(%r, %r)', self, fd, data) - self._wfds[fd] = data or fd + self._wfds[fd] = (data or fd, self._generation) self._control(fd) def stop_transmit(self, fd): @@ -802,20 +794,24 @@ class EpollPoller(mitogen.core.Poller): _inmask = (getattr(select, 'EPOLLIN', 0) | getattr(select, 'EPOLLHUP', 0)) - def poll(self, timeout=None): + def _poll(self, timeout): the_timeout = -1 if timeout is not None: the_timeout = timeout events, _ = mitogen.core.io_op(self._epoll.poll, the_timeout, 32) for fd, event in events: - if event & self._inmask and fd in self._rfds: - # Events can still be read for an already-discarded fd. - mitogen.core._vv and IOLOG.debug('%r: POLLIN: %r', self, fd) - yield self._rfds[fd] - if event & select.EPOLLOUT and fd in self._wfds: - mitogen.core._vv and IOLOG.debug('%r: POLLOUT: %r', self, fd) - yield self._wfds[fd] + if event & self._inmask: + data, gen = self._rfds.get(fd, (None, None)) + if gen and gen < self._generation: + # Events can still be read for an already-discarded fd. + mitogen.core._vv and IOLOG.debug('%r: POLLIN: %r', self, fd) + yield data + if event & select.EPOLLOUT: + data, gen = self._wfds.get(fd, (None, None)) + if gen and gen < self._generation: + mitogen.core._vv and IOLOG.debug('%r: POLLOUT: %r', self, fd) + yield data POLLER_BY_SYSNAME = { From d5a8293c917605d3af2c3b1713930d02d5d43bd9 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 21:42:26 +0000 Subject: [PATCH 056/662] issue #333: closure & data distinctness tests. --- tests/poller_test.py | 68 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/tests/poller_test.py b/tests/poller_test.py index d1d7c6ea..a6190821 100644 --- a/tests/poller_test.py +++ b/tests/poller_test.py @@ -16,7 +16,7 @@ import testlib class SockMixin(object): def tearDown(self): - self._teardown_socks() + self.close_socks() super(SockMixin, self).tearDown() def setUp(self): @@ -62,7 +62,7 @@ class SockMixin(object): return raise - def _teardown_socks(self): + def close_socks(self): for sock in self.l1_sock, self.r1_sock, self.l2_sock, self.r2_sock: sock.close() @@ -298,11 +298,75 @@ class MutateDuringYieldMixin(PollerMixin, SockMixin): self.assertEquals([], list(p)) +class FileClosedMixin(PollerMixin, SockMixin): + # Verify behaviour when a registered file object is closed in various + # scenarios, without first calling stop_receive()/stop_transmit(). + + def test_writeable_then_closed(self): + self.p.start_transmit(self.r1) + self.assertEquals([self.r1], list(self.p.poll(0))) + self.close_socks() + try: + self.assertEquals([], list(self.p.poll(0))) + except select.error: + # a crash is also reasonable here. + pass + + def test_writeable_closed_before_yield(self): + self.p.start_transmit(self.r1) + p = self.p.poll(0) + self.close_socks() + try: + self.assertEquals([], list(p)) + except select.error: + # a crash is also reasonable here. + pass + + def test_readable_then_closed(self): + self.fill(self.l1) + self.p.start_receive(self.r1) + self.assertEquals([self.r1], list(self.p.poll(0))) + self.close_socks() + try: + self.assertEquals([], list(self.p.poll(0))) + except select.error: + # a crash is also reasonable here. + pass + + def test_readable_closed_before_yield(self): + self.fill(self.l1) + self.p.start_receive(self.r1) + p = self.p.poll(0) + self.close_socks() + try: + self.assertEquals([], list(p)) + except select.error: + # a crash is also reasonable here. + pass + + +class DistinctDataMixin(PollerMixin, SockMixin): + # Verify different data is yielded for the same FD according to the event + # being raised. + + def test_one_distinct(self): + rdata = object() + wdata = object() + self.p.start_receive(self.r1, data=rdata) + self.p.start_transmit(self.r1, data=wdata) + + self.assertEquals([wdata], list(self.p.poll(0))) + self.fill(self.l1) # r1 is now readable and writeable. + self.assertEquals(set([rdata, wdata]), set(self.p.poll(0))) + + class AllMixin(ReceiveStateMixin, TransmitStateMixin, ReadableMixin, WriteableMixin, MutateDuringYieldMixin, + FileClosedMixin, + DistinctDataMixin, PollMixin, CloseMixin): """ From 71f9e84ab3346d6ce3af2672a223a062e3f643b3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 22:39:35 +0000 Subject: [PATCH 057/662] Add EOF error hints for LXC/LXD; closes #373. --- mitogen/lxc.py | 6 ++++++ mitogen/lxd.py | 6 ++++++ mitogen/parent.py | 37 ++++++++++++++++++++++++++++++++----- tests/lxc_test.py | 19 +++++++++++++++---- tests/lxd_test.py | 13 ++++++++++++- 5 files changed, 71 insertions(+), 10 deletions(-) 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() From 026710cb28a291e675f238c390ef9c1663172faa Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 22:41:28 +0000 Subject: [PATCH 058/662] issue #373: update Changelog. --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 96a4fedc..503c797c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -46,6 +46,10 @@ Fixes was invoked using sudo without appropriate flags to cause the ``HOME`` environment variable to be reset to match the target account. +* `#373 `_: the LXC and LXD methods + now print a useful hint when Python fails to start, as no useful error is + normally logged to the console by these tools. + Core Library ~~~~~~~~~~~~ @@ -83,6 +87,7 @@ Thanks! Mitogen would not be possible without the support of users. A huge thanks for bug reports, features and fixes in this release contributed by +`Brian Candler `_, and `Guy Knights `_. From 332d128651519f05ac056e71e8f42482de765b0e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 23:06:32 +0000 Subject: [PATCH 059/662] tests: get rid of some more shell --- tests/data/iter_read_generator.py | 13 +++++++++++++ tests/data/iter_read_generator.sh | 10 ---------- tests/parent_test.py | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100755 tests/data/iter_read_generator.py delete mode 100755 tests/data/iter_read_generator.sh diff --git a/tests/data/iter_read_generator.py b/tests/data/iter_read_generator.py new file mode 100755 index 00000000..3fd3c08c --- /dev/null +++ b/tests/data/iter_read_generator.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# I produce text every 100ms, for testing mitogen.core.iter_read() + +import sys +import time + + +i = 0 +while True: + i += 1 + sys.stdout.write(str(i)) + sys.stdout.flush() + time.sleep(0.1) diff --git a/tests/data/iter_read_generator.sh b/tests/data/iter_read_generator.sh deleted file mode 100755 index 3aa6d6ac..00000000 --- a/tests/data/iter_read_generator.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# I produce text every 100ms, for testing mitogen.core.iter_read() - -i=0 - -while :; do - i=$(($i + 1)) - echo "$i" - sleep 0.1 -done diff --git a/tests/parent_test.py b/tests/parent_test.py index aaf335b8..c0c542f4 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -215,7 +215,7 @@ class IterReadTest(unittest2.TestCase): func = staticmethod(mitogen.parent.iter_read) def make_proc(self): - args = [testlib.data_path('iter_read_generator.sh')] + args = [testlib.data_path('iter_read_generator.py')] proc = subprocess.Popen(args, stdout=subprocess.PIPE) mitogen.core.set_nonblock(proc.stdout.fileno()) return proc From f53e7dd637fdeb09913543c62fa822bfee4b1995 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 23:09:06 +0000 Subject: [PATCH 060/662] tests: Pythonize another shell script. --- ...{python_never_responds.sh => python_never_responds.py} | 8 ++++++-- tests/parent_test.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) rename tests/data/{python_never_responds.sh => python_never_responds.py} (51%) diff --git a/tests/data/python_never_responds.sh b/tests/data/python_never_responds.py similarity index 51% rename from tests/data/python_never_responds.sh rename to tests/data/python_never_responds.py index f1ad5787..449d8565 100755 --- a/tests/data/python_never_responds.sh +++ b/tests/data/python_never_responds.py @@ -1,3 +1,7 @@ -#!/bin/bash +#!/usr/bin/python # I am a Python interpreter that sits idle until the connection times out. -exec -a mitogen-tests-python-never-responds.sh sleep 86400 + +import time + +while True: + time.sleep(86400) diff --git a/tests/parent_test.py b/tests/parent_test.py index c0c542f4..24327024 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -93,7 +93,7 @@ class ReapChildTest(testlib.RouterMixin, testlib.TestCase): remote_id=1234, old_router=self.router, max_message_size=self.router.max_message_size, - python_path=testlib.data_path('python_never_responds.sh'), + python_path=testlib.data_path('python_never_responds.py'), connect_timeout=0.5, ) self.assertRaises(mitogen.core.TimeoutError, From 48e8f1f7aa34dcac485f29da4d5df66de1b089f9 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 23:12:16 +0000 Subject: [PATCH 061/662] tests: Pythonize write_all_consumer.py --- tests/data/write_all_consumer.py | 9 +++++++++ tests/data/write_all_consumer.sh | 7 ------- tests/parent_test.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100755 tests/data/write_all_consumer.py delete mode 100755 tests/data/write_all_consumer.sh diff --git a/tests/data/write_all_consumer.py b/tests/data/write_all_consumer.py new file mode 100755 index 00000000..4013ccdd --- /dev/null +++ b/tests/data/write_all_consumer.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# I consume 65535 bytes every 10ms, for testing mitogen.core.write_all() + +import os +import time + +while True: + os.read(0, 65535) + time.sleep(0.01) diff --git a/tests/data/write_all_consumer.sh b/tests/data/write_all_consumer.sh deleted file mode 100755 index e6aaaf72..00000000 --- a/tests/data/write_all_consumer.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# I consume 65535 bytes every 10ms, for testing mitogen.core.write_all() - -while :; do - read -n 65535 - sleep 0.01 -done diff --git a/tests/parent_test.py b/tests/parent_test.py index 24327024..9d540ccc 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -267,7 +267,7 @@ class WriteAllTest(unittest2.TestCase): func = staticmethod(mitogen.parent.write_all) def make_proc(self): - args = [testlib.data_path('write_all_consumer.sh')] + args = [testlib.data_path('write_all_consumer.py')] proc = subprocess.Popen(args, stdin=subprocess.PIPE) mitogen.core.set_nonblock(proc.stdin.fileno()) return proc From 5b916fc55694c5ebb1b9d8455bee75caa97ecb7e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 30 Oct 2018 23:58:45 +0000 Subject: [PATCH 062/662] issue #409: Pythonize run_ansible_playbook.sh And add git_basedir extra variable. --- .travis/ansible_tests.py | 2 +- tests/ansible/README.md | 4 +-- tests/ansible/debug_run_ansible_playbook.sh | 15 ------------ tests/ansible/mitogen_ansible_playbook.sh | 2 +- tests/ansible/run_ansible_playbook.py | 27 +++++++++++++++++++++ tests/ansible/run_ansible_playbook.sh | 15 ------------ 6 files changed, 31 insertions(+), 34 deletions(-) delete mode 100755 tests/ansible/debug_run_ansible_playbook.sh create mode 100755 tests/ansible/run_ansible_playbook.py delete mode 100755 tests/ansible/run_ansible_playbook.sh diff --git a/.travis/ansible_tests.py b/.travis/ansible_tests.py index efaca9fe..8bb8f6a1 100755 --- a/.travis/ansible_tests.py +++ b/.travis/ansible_tests.py @@ -63,5 +63,5 @@ with ci_lib.Fold('job_setup'): with ci_lib.Fold('ansible'): - run('/usr/bin/time ./run_ansible_playbook.sh all.yml -i "%s" %s', + run('/usr/bin/time ./run_ansible_playbook.py all.yml -i "%s" %s', HOSTS_DIR, ' '.join(sys.argv[1:])) diff --git a/tests/ansible/README.md b/tests/ansible/README.md index 46320951..50e747fe 100644 --- a/tests/ansible/README.md +++ b/tests/ansible/README.md @@ -13,7 +13,7 @@ demonstrator for what does and doesn't work. See `../image_prep/README.md`. -## `run_ansible_playbook.sh` +## `run_ansible_playbook.py` This is necessary to set some environment variables used by future tests, as there appears to be no better way to inject them into the top-level process @@ -22,7 +22,7 @@ environment before the Mitogen connection process forks. ## Running Everything -`ANSIBLE_STRATEGY=mitogen_linear ./run_ansible_playbook.sh all.yml` +`ANSIBLE_STRATEGY=mitogen_linear ./run_ansible_playbook.py all.yml` ## `hosts/` and `common-hosts` diff --git a/tests/ansible/debug_run_ansible_playbook.sh b/tests/ansible/debug_run_ansible_playbook.sh deleted file mode 100755 index ab2c9385..00000000 --- a/tests/ansible/debug_run_ansible_playbook.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Wrap ansible-playbook, setting up some test of the test environment. - -# Used by delegate_to.yml to ensure "sudo -E" preserves environment. -export I_WAS_PRESERVED=1 -export MITOGEN_MAX_INTERPRETERS=3 - -if [ "${ANSIBLE_STRATEGY:0:7}" = "mitogen" ] -then - EXTRA='{"is_mitogen": true}' -else - EXTRA='{"is_mitogen": false}' -fi - -exec ~/src/cpython/venv/bin/ansible-playbook -e "$EXTRA" -e ansible_python_interpreter=/Users/dmw/src/cpython/venv/bin/python2.7 "$@" diff --git a/tests/ansible/mitogen_ansible_playbook.sh b/tests/ansible/mitogen_ansible_playbook.sh index cd5c1e53..462d985b 100755 --- a/tests/ansible/mitogen_ansible_playbook.sh +++ b/tests/ansible/mitogen_ansible_playbook.sh @@ -1,3 +1,3 @@ #!/bin/bash export ANSIBLE_STRATEGY=mitogen_linear -exec ./run_ansible_playbook.sh "$@" +exec ./run_ansible_playbook.py "$@" diff --git a/tests/ansible/run_ansible_playbook.py b/tests/ansible/run_ansible_playbook.py new file mode 100755 index 00000000..c4312cd1 --- /dev/null +++ b/tests/ansible/run_ansible_playbook.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# Wrap ansible-playbook, setting up some test of the test environment. + +import json +import os +import sys + + +# Used by delegate_to.yml to ensure "sudo -E" preserves environment. +os.environ['I_WAS_PRESERVED'] = '1' + +# Used by LRU tests. +os.environ['MITOGEN_MAX_INTERPRETERS'] = '3' + +extra = { + 'is_mitogen': os.environ.get('ANSIBLE_STRATEGY', '').startswith('mitogen'), + 'git_basedir': os.path.dirname( + os.path.abspath( + os.path.join(__file__, '..', '..') + ) + ) +} + +args = ['ansible-playbook'] +args += ['-e', json.dumps(extra)] +args += sys.argv[1:] +os.execvp(args[0], args) diff --git a/tests/ansible/run_ansible_playbook.sh b/tests/ansible/run_ansible_playbook.sh deleted file mode 100755 index 39580e37..00000000 --- a/tests/ansible/run_ansible_playbook.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Wrap ansible-playbook, setting up some test of the test environment. - -# Used by delegate_to.yml to ensure "sudo -E" preserves environment. -export I_WAS_PRESERVED=1 -export MITOGEN_MAX_INTERPRETERS=3 - -if [ "${ANSIBLE_STRATEGY:0:7}" = "mitogen" ] -then - EXTRA='{"is_mitogen": true}' -else - EXTRA='{"is_mitogen": false}' -fi - -exec ansible-playbook -e "$EXTRA" "$@" From 51658fdd66f55dd8bd7f00b798656e895b26c50f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 00:05:21 +0000 Subject: [PATCH 063/662] issue #409: name-prefix stubs so they can be added to PATH Allows us to reference them from playbooks easily. --- tests/data/stubs/{docker.py => stub-docker.py} | 0 tests/data/stubs/{lxc-attach.py => stub-lxc-attach.py} | 0 tests/data/stubs/{lxc.py => stub-lxc.py} | 0 tests/data/stubs/{ssh.py => stub-ssh.py} | 0 tests/docker_test.py | 2 +- tests/lxc_test.py | 2 +- tests/lxd_test.py | 2 +- tests/ssh_test.py | 4 ++-- 8 files changed, 5 insertions(+), 5 deletions(-) rename tests/data/stubs/{docker.py => stub-docker.py} (100%) rename tests/data/stubs/{lxc-attach.py => stub-lxc-attach.py} (100%) rename tests/data/stubs/{lxc.py => stub-lxc.py} (100%) rename tests/data/stubs/{ssh.py => stub-ssh.py} (100%) diff --git a/tests/data/stubs/docker.py b/tests/data/stubs/stub-docker.py similarity index 100% rename from tests/data/stubs/docker.py rename to tests/data/stubs/stub-docker.py diff --git a/tests/data/stubs/lxc-attach.py b/tests/data/stubs/stub-lxc-attach.py similarity index 100% rename from tests/data/stubs/lxc-attach.py rename to tests/data/stubs/stub-lxc-attach.py diff --git a/tests/data/stubs/lxc.py b/tests/data/stubs/stub-lxc.py similarity index 100% rename from tests/data/stubs/lxc.py rename to tests/data/stubs/stub-lxc.py diff --git a/tests/data/stubs/ssh.py b/tests/data/stubs/stub-ssh.py similarity index 100% rename from tests/data/stubs/ssh.py rename to tests/data/stubs/stub-ssh.py diff --git a/tests/docker_test.py b/tests/docker_test.py index 33ead10c..2d45609a 100644 --- a/tests/docker_test.py +++ b/tests/docker_test.py @@ -9,7 +9,7 @@ import testlib class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): def test_okay(self): - docker_path = testlib.data_path('stubs/docker.py') + docker_path = testlib.data_path('stubs/stub-docker.py') context = self.router.docker( container='container_name', docker_path=docker_path, diff --git a/tests/lxc_test.py b/tests/lxc_test.py index 5d8e14d8..bcab8e68 100644 --- a/tests/lxc_test.py +++ b/tests/lxc_test.py @@ -13,7 +13,7 @@ def has_subseq(seq, subseq): class ConstructorTest(testlib.RouterMixin, testlib.TestCase): - lxc_attach_path = testlib.data_path('stubs/lxc-attach.py') + lxc_attach_path = testlib.data_path('stubs/stub-lxc-attach.py') def test_okay(self): context = self.router.lxc( diff --git a/tests/lxd_test.py b/tests/lxd_test.py index cae1172c..e59da43c 100644 --- a/tests/lxd_test.py +++ b/tests/lxd_test.py @@ -11,7 +11,7 @@ import testlib class ConstructorTest(testlib.RouterMixin, testlib.TestCase): def test_okay(self): - lxc_path = testlib.data_path('stubs/lxc.py') + lxc_path = testlib.data_path('stubs/stub-lxc.py') context = self.router.lxd( container='container_name', lxc_path=lxc_path, diff --git a/tests/ssh_test.py b/tests/ssh_test.py index bdee30dd..36359a66 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -22,7 +22,7 @@ class StubSshMixin(testlib.RouterMixin): return self.router.ssh( hostname='hostname', username='mitogen__has_sudo', - ssh_path=testlib.data_path('stubs/ssh.py'), + ssh_path=testlib.data_path('stubs/stub-ssh.py'), **kwargs ) finally: @@ -34,7 +34,7 @@ class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): context = self.router.ssh( hostname='hostname', username='mitogen__has_sudo', - ssh_path=testlib.data_path('stubs/ssh.py'), + ssh_path=testlib.data_path('stubs/stub-ssh.py'), ) #context.call(mitogen.utils.log_to_file, '/tmp/log') #context.call(mitogen.utils.disable_site_packages) From c51b67b8635f1bc43cc6a3c2c1dfeeb88fa9aa57 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 00:08:57 +0000 Subject: [PATCH 064/662] issue #409: add test stubs to the PATH in run_ansible_playbook.py --- tests/ansible/run_ansible_playbook.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/ansible/run_ansible_playbook.py b/tests/ansible/run_ansible_playbook.py index c4312cd1..2f85c8ac 100755 --- a/tests/ansible/run_ansible_playbook.py +++ b/tests/ansible/run_ansible_playbook.py @@ -6,19 +6,29 @@ import os import sys +GIT_BASEDIR = os.path.dirname( + os.path.abspath( + os.path.join(__file__, '..', '..') + ) +) + + # Used by delegate_to.yml to ensure "sudo -E" preserves environment. os.environ['I_WAS_PRESERVED'] = '1' # Used by LRU tests. os.environ['MITOGEN_MAX_INTERPRETERS'] = '3' +# Add test stubs to path. +os.environ['PATH'] = '%s%s%s' % ( + os.path.join(GIT_BASEDIR, 'tests', 'data', 'stubs'), + os.pathsep, + os.environ['PATH'], +) + extra = { 'is_mitogen': os.environ.get('ANSIBLE_STRATEGY', '').startswith('mitogen'), - 'git_basedir': os.path.dirname( - os.path.abspath( - os.path.join(__file__, '..', '..') - ) - ) + 'git_basedir': GIT_BASEDIR, } args = ['ansible-playbook'] From 429832b8f7c7a54aabc21ca0ab3a58372d6bc58b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 00:46:47 +0000 Subject: [PATCH 065/662] issue #409: add kubectl stub and constructor test. --- tests/data/stubs/stub-kubectl.py | 8 ++++++++ tests/kubectl_test.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100755 tests/data/stubs/stub-kubectl.py create mode 100644 tests/kubectl_test.py diff --git a/tests/data/stubs/stub-kubectl.py b/tests/data/stubs/stub-kubectl.py new file mode 100755 index 00000000..16f7e460 --- /dev/null +++ b/tests/data/stubs/stub-kubectl.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +import sys +import os + +os.environ['ORIGINAL_ARGV'] = repr(sys.argv) +os.environ['THIS_IS_STUB_KUBECTL'] = '1' +os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) diff --git a/tests/kubectl_test.py b/tests/kubectl_test.py new file mode 100644 index 00000000..0bac3048 --- /dev/null +++ b/tests/kubectl_test.py @@ -0,0 +1,29 @@ + +import os + +import mitogen +import mitogen.parent + +import unittest2 + +import testlib + + +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): + kubectl_path = testlib.data_path('stubs/stub-kubectl.py') + + def test_okay(self): + context = self.router.kubectl( + pod='pod_name', + kubectl_path=self.kubectl_path + ) + + argv = eval(context.call(os.getenv, 'ORIGINAL_ARGV')) + self.assertEquals(argv[0], self.kubectl_path) + self.assertEquals(argv[1], 'exec') + self.assertEquals(argv[2], '-it') + self.assertEquals(argv[3], 'pod_name') + + +if __name__ == '__main__': + unittest2.main() From f2294c16780ea808a3357c1287dfd4b8a52fede1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 00:47:47 +0000 Subject: [PATCH 066/662] issue #409: add new stub_connections/ test type. --- tests/ansible/integration/all.yml | 1 + .../integration/stub_connections/README.md | 9 ++++++ .../integration/stub_connections/all.yml | 2 ++ .../integration/stub_connections/kubectl.yml | 15 ++++++++++ .../stub_connections/setns_lxc.yml | 30 +++++++++++++++++++ tests/data/stubs/stub-lxc-info.py | 4 +++ 6 files changed, 61 insertions(+) create mode 100644 tests/ansible/integration/stub_connections/README.md create mode 100644 tests/ansible/integration/stub_connections/all.yml create mode 100644 tests/ansible/integration/stub_connections/kubectl.yml create mode 100644 tests/ansible/integration/stub_connections/setns_lxc.yml create mode 100755 tests/data/stubs/stub-lxc-info.py diff --git a/tests/ansible/integration/all.yml b/tests/ansible/integration/all.yml index e9a12ec8..d0d3ebf5 100644 --- a/tests/ansible/integration/all.yml +++ b/tests/ansible/integration/all.yml @@ -7,6 +7,7 @@ - import_playbook: async/all.yml - import_playbook: become/all.yml - import_playbook: connection/all.yml +- import_playbook: stub_connections/all.yml - import_playbook: connection_loader/all.yml - import_playbook: context_service/all.yml - import_playbook: delegation/all.yml diff --git a/tests/ansible/integration/stub_connections/README.md b/tests/ansible/integration/stub_connections/README.md new file mode 100644 index 00000000..e12d5557 --- /dev/null +++ b/tests/ansible/integration/stub_connections/README.md @@ -0,0 +1,9 @@ + +# `stub_connections/` + +The playbooks in this directory use stub implementations of various third party +tools (kubectl etc.) to verify arguments passed by Ansible to Mitogen and +subsequently onward to the tool result in something that looks sane. + +These are bare minimum tests just to ensure sporadically tested connection +methods haven't broken in embarrasingly obvious ways. diff --git a/tests/ansible/integration/stub_connections/all.yml b/tests/ansible/integration/stub_connections/all.yml new file mode 100644 index 00000000..f8d5d169 --- /dev/null +++ b/tests/ansible/integration/stub_connections/all.yml @@ -0,0 +1,2 @@ +- import_playbook: kubectl.yml +- import_playbook: setns_lxc.yml diff --git a/tests/ansible/integration/stub_connections/kubectl.yml b/tests/ansible/integration/stub_connections/kubectl.yml new file mode 100644 index 00000000..13a074e1 --- /dev/null +++ b/tests/ansible/integration/stub_connections/kubectl.yml @@ -0,0 +1,15 @@ + +- name: integration/stub_connections/kubectl.yml + hosts: test-targets + gather_facts: false + any_errors_fatal: true + tasks: + - custom_python_detect_environment: + vars: + ansible_connection: kubectl + mitogen_kubectl_path: stub-kubectl.py + register: out + + - assert: + that: + - out.env.THIS_IS_STUB_KUBECTL == '1' diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml new file mode 100644 index 00000000..bc37aa8e --- /dev/null +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -0,0 +1,30 @@ + +# issue #409. +# setns is hard -- it wants to do superuser syscalls, so we must run it in a +# child Ansible via sudo. But that only works if sudo works. + +- name: integration/stub_connections/setns_lxc.yml + hosts: test-targets + gather_facts: false + any_errors_fatal: true + connection: local + tasks: + - setup: + register: out + + - command: | + sudo -E ansible + -i localhost, + -c setns + -e mitogen_kind=lxc + -e mitogen_lxc_info_path=stub-lxc-info.py + -m shell + -a "echo hi" + localhost + args: + chdir: ../.. + warn: false + + # TODO: we don't know if sudo works on this machine, so hard wire it for + # me and for Travis CI. + when: out.ansible_facts.ansible_user_id in ['dmw', 'travis'] diff --git a/tests/data/stubs/stub-lxc-info.py b/tests/data/stubs/stub-lxc-info.py new file mode 100755 index 00000000..bcbc36a5 --- /dev/null +++ b/tests/data/stubs/stub-lxc-info.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# Mainly for use in stubconnections/kubectl.yml + +print 'PID: 1' From 18af1dfb5178b37d3f8d319969caf8dc651821de Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 00:48:11 +0000 Subject: [PATCH 067/662] ansible: kubectl_path argument appears in wrong connection method Closes #409. --- ansible_mitogen/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index df10884a..a651d163 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -142,6 +142,7 @@ def _connect_kubectl(spec): 'pod': spec['remote_addr'], 'python_path': spec['python_path'], 'connect_timeout': spec['ansible_ssh_timeout'] or spec['timeout'], + 'kubectl_path': spec['mitogen_kubectl_path'], 'kubectl_args': spec['extra_args'], } } @@ -209,7 +210,6 @@ def _connect_setns(spec): 'python_path': spec['python_path'], 'kind': spec['mitogen_kind'], 'docker_path': spec['mitogen_docker_path'], - 'kubectl_path': spec['mitogen_kubectl_path'], 'lxc_info_path': spec['mitogen_lxc_info_path'], 'machinectl_path': spec['mitogen_machinectl_path'], } From 48942a8a30b5138fda2427b1736f17f8246254e3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 00:52:06 +0000 Subject: [PATCH 068/662] issue #409: updat Changelog. --- docs/changelog.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 503c797c..c31dac3e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,6 +50,10 @@ Fixes now print a useful hint when Python fails to start, as no useful error is normally logged to the console by these tools. +* `#409 `_: the setns method was + silently broken due to missing tests. The issue was fixed and basic test + coverage was added. + Core Library ~~~~~~~~~~~~ @@ -87,8 +91,9 @@ Thanks! Mitogen would not be possible without the support of users. A huge thanks for bug reports, features and fixes in this release contributed by -`Brian Candler `_, and -`Guy Knights `_. +`Brian Candler `_, +`Guy Knights `_, and +`Jonathan Rosser `_. v0.2.3 (2018-10-23) From a77f07659e9dcb355901dc5224e18f8f93ef7cf8 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 01:10:41 +0000 Subject: [PATCH 069/662] issue #409: make setns test to work anywhere sudo works. --- .../stub_connections/setns_lxc.yml | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml index bc37aa8e..739047c5 100644 --- a/tests/ansible/integration/stub_connections/setns_lxc.yml +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -6,12 +6,21 @@ - name: integration/stub_connections/setns_lxc.yml hosts: test-targets gather_facts: false - any_errors_fatal: true + any_errors_fatal: false connection: local tasks: - setup: register: out + - debug: msg={{out}} + + - command: sudo whoami + args: + warn: false + ignore_errors: true + register: sudo_available + when: out.ansible_facts.ansible_system == 'Linux' + - command: | sudo -E ansible -i localhost, @@ -24,7 +33,14 @@ args: chdir: ../.. warn: false + when: | + out.ansible_facts.ansible_system == 'Linux' + and sudo_available.rc == 0 + register: result + + - assert: + that: | + (out.ansible_facts.ansible_system != 'Linux') or + (sudo_available != 0) or + (result.rc == 0) - # TODO: we don't know if sudo works on this machine, so hard wire it for - # me and for Travis CI. - when: out.ansible_facts.ansible_user_id in ['dmw', 'travis'] From e832ddec1374e5b6c1baa0e94bf1c5854126f4bc Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 11:54:51 +0000 Subject: [PATCH 070/662] issue #409: mark sudo test noninteractive --- tests/ansible/integration/stub_connections/setns_lxc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml index 739047c5..a979b1b7 100644 --- a/tests/ansible/integration/stub_connections/setns_lxc.yml +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -14,7 +14,7 @@ - debug: msg={{out}} - - command: sudo whoami + - command: sudo -n whoami args: warn: false ignore_errors: true @@ -22,7 +22,7 @@ when: out.ansible_facts.ansible_system == 'Linux' - command: | - sudo -E ansible + sudo -nE ansible -i localhost, -c setns -e mitogen_kind=lxc From 0e8f4511900d12bf988ebbc91e4eab6c74c98492 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 12:12:17 +0000 Subject: [PATCH 071/662] issue #409: add stub LXC test, refactor playbooks. --- .../_end_play_if_not_sudo_linux.yml | 17 +++++++++++ .../integration/stub_connections/all.yml | 1 + .../integration/stub_connections/kubectl.yml | 3 ++ .../stub_connections/setns_lxc.yml | 19 ++---------- .../stub_connections/setns_lxd.yml | 29 +++++++++++++++++++ tests/data/stubs/stub-lxc.py | 5 ++++ 6 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml create mode 100644 tests/ansible/integration/stub_connections/setns_lxd.yml diff --git a/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml b/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml new file mode 100644 index 00000000..55997a72 --- /dev/null +++ b/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml @@ -0,0 +1,17 @@ +# End the play if we're not on Linux and a raw 'sudo' command isn't available. +# Expects connection:local + +- shell: uname -s + register: out + +- meta: end_play + when: out.stdout != 'Linux' + +- command: sudo -n whoami + args: + warn: false + ignore_errors: true + register: sudo_available + +- meta: end_play + when: sudo_available.rc != 0 diff --git a/tests/ansible/integration/stub_connections/all.yml b/tests/ansible/integration/stub_connections/all.yml index f8d5d169..9258c172 100644 --- a/tests/ansible/integration/stub_connections/all.yml +++ b/tests/ansible/integration/stub_connections/all.yml @@ -1,2 +1,3 @@ - import_playbook: kubectl.yml - import_playbook: setns_lxc.yml +- import_playbook: setns_lxd.yml diff --git a/tests/ansible/integration/stub_connections/kubectl.yml b/tests/ansible/integration/stub_connections/kubectl.yml index 13a074e1..c5768a00 100644 --- a/tests/ansible/integration/stub_connections/kubectl.yml +++ b/tests/ansible/integration/stub_connections/kubectl.yml @@ -4,6 +4,9 @@ gather_facts: false any_errors_fatal: true tasks: + - meta: end_play + when: ansible_version.full < '2.5' + - custom_python_detect_environment: vars: ansible_connection: kubectl diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml index a979b1b7..19e2a984 100644 --- a/tests/ansible/integration/stub_connections/setns_lxc.yml +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -1,4 +1,3 @@ - # issue #409. # setns is hard -- it wants to do superuser syscalls, so we must run it in a # child Ansible via sudo. But that only works if sudo works. @@ -9,17 +8,7 @@ any_errors_fatal: false connection: local tasks: - - setup: - register: out - - - debug: msg={{out}} - - - command: sudo -n whoami - args: - warn: false - ignore_errors: true - register: sudo_available - when: out.ansible_facts.ansible_system == 'Linux' + - include_tasks: _end_play_if_not_sudo_linux.yml - command: | sudo -nE ansible @@ -39,8 +28,4 @@ register: result - assert: - that: | - (out.ansible_facts.ansible_system != 'Linux') or - (sudo_available != 0) or - (result.rc == 0) - + that: result.rc == 0 diff --git a/tests/ansible/integration/stub_connections/setns_lxd.yml b/tests/ansible/integration/stub_connections/setns_lxd.yml new file mode 100644 index 00000000..e552a656 --- /dev/null +++ b/tests/ansible/integration/stub_connections/setns_lxd.yml @@ -0,0 +1,29 @@ +# issue #409. +# setns is hard -- it wants to do superuser syscalls, so we must run it in a +# child Ansible via sudo. But that only works if sudo works. + +- name: integration/stub_connections/setns_lxc.yml + hosts: test-targets + gather_facts: false + any_errors_fatal: false + connection: local + tasks: + - include_tasks: _end_play_if_not_sudo_linux.yml + + - command: | + sudo -nE ansible + -i localhost, + -c setns + -e mitogen_kind=lxd + -e mitogen_lxc_info_path=stub-lxc.py + -m shell + -a "echo hi" + localhost + args: + chdir: ../.. + warn: false + register: result + + - assert: + that: result.rc == 0 + diff --git a/tests/data/stubs/stub-lxc.py b/tests/data/stubs/stub-lxc.py index 2fedb961..b1448bec 100755 --- a/tests/data/stubs/stub-lxc.py +++ b/tests/data/stubs/stub-lxc.py @@ -3,5 +3,10 @@ import sys import os +# setns.py fetching leader PID. +if sys.argv[1] == 'info': + print 'Pid: 1' + sys.exit(0) + os.environ['ORIGINAL_ARGV'] = repr(sys.argv) os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) From 05f9fb4dd8b27415c1fd5376ca787829c46ea4c6 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 12:12:54 +0000 Subject: [PATCH 072/662] issue #409: don't run kubectl test in <2.5. --- .../plugins/connection/mitogen_kubectl.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ansible_mitogen/plugins/connection/mitogen_kubectl.py b/ansible_mitogen/plugins/connection/mitogen_kubectl.py index 5ffe3f7b..855b4ec0 100644 --- a/ansible_mitogen/plugins/connection/mitogen_kubectl.py +++ b/ansible_mitogen/plugins/connection/mitogen_kubectl.py @@ -31,7 +31,12 @@ from __future__ import absolute_import import os.path import sys -import ansible.plugins.connection.kubectl +try: + from ansible.plugins.connection import kubectl +except ImportError: + kubectl = None + +from ansible.errors import AnsibleConnectionFailure from ansible.module_utils.six import iteritems try: @@ -47,6 +52,16 @@ import ansible_mitogen.connection class Connection(ansible_mitogen.connection.Connection): transport = 'kubectl' + not_supported_msg = ( + 'The "mitogen_kubectl" plug-in requires a version of Ansible ' + 'that ships with the "kubectl" connection plug-in.' + ) + + def __init__(self, *args, **kwargs): + if kubectl is None: + raise AnsibleConnectionFailure(self.not_supported_msg) + super(Connection, self).__init__(*args, **kwargs) + def get_extra_args(self): parameters = [] for key, option in iteritems(ansible.plugins.connection.kubectl.CONNECTION_OPTIONS): From a68675af8bb6810bf34e5c1d20078c01bba3aeb5 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 12:39:43 +0000 Subject: [PATCH 073/662] issue #409: fix reference error in kubectl.py. --- ansible_mitogen/plugins/connection/mitogen_kubectl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_mitogen/plugins/connection/mitogen_kubectl.py b/ansible_mitogen/plugins/connection/mitogen_kubectl.py index 855b4ec0..2dab131b 100644 --- a/ansible_mitogen/plugins/connection/mitogen_kubectl.py +++ b/ansible_mitogen/plugins/connection/mitogen_kubectl.py @@ -64,7 +64,7 @@ class Connection(ansible_mitogen.connection.Connection): def get_extra_args(self): parameters = [] - for key, option in iteritems(ansible.plugins.connection.kubectl.CONNECTION_OPTIONS): + for key, option in iteritems(kubectl.CONNECTION_OPTIONS): if self.get_task_var('ansible_' + key) is not None: parameters += [ option, self.get_task_var('ansible_' + key) ] From 54445470e2a1a065d9d0693849bea5ab748eeada Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 12:40:08 +0000 Subject: [PATCH 074/662] issue #409: add missing path config variables to severa plugins So every method can be redirected to a stub implementation. --- ansible_mitogen/connection.py | 9 +++++++++ docs/ansible.rst | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index a651d163..140c901b 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -172,6 +172,7 @@ def _connect_lxc(spec): 'kwargs': { 'container': spec['remote_addr'], 'python_path': spec['python_path'], + 'lxc_attach_path': spec['mitogen_lxc_attach_path'], 'connect_timeout': spec['ansible_ssh_timeout'] or spec['timeout'], } } @@ -186,6 +187,7 @@ def _connect_lxd(spec): 'kwargs': { 'container': spec['remote_addr'], 'python_path': spec['python_path'], + 'lxc_path': spec['mitogen_lxc_path'], 'connect_timeout': spec['ansible_ssh_timeout'] or spec['timeout'], } } @@ -210,6 +212,7 @@ def _connect_setns(spec): 'python_path': spec['python_path'], 'kind': spec['mitogen_kind'], 'docker_path': spec['mitogen_docker_path'], + 'lxc_path': spec['mitogen_lxc_path'], 'lxc_info_path': spec['mitogen_lxc_info_path'], 'machinectl_path': spec['mitogen_machinectl_path'], } @@ -392,6 +395,10 @@ def config_from_play_context(transport, inventory_name, connection): connection.get_task_var('mitogen_docker_path'), 'mitogen_kubectl_path': connection.get_task_var('mitogen_kubectl_path'), + 'mitogen_lxc_path': + connection.get_task_var('mitogen_lxc_path'), + 'mitogen_lxc_attach_path': + connection.get_task_var('mitogen_lxc_attach_path'), 'mitogen_lxc_info_path': connection.get_task_var('mitogen_lxc_info_path'), 'mitogen_machinectl_path': @@ -427,6 +434,8 @@ def config_from_hostvars(transport, inventory_name, connection, 'mitogen_kind': hostvars.get('mitogen_kind'), 'mitogen_docker_path': hostvars.get('mitogen_docker_path'), 'mitogen_kubectl_path': hostvars.get('mitogen_kubectl_path'), + 'mitogen_lxc_path': hostvars.get('mitogen_lxc_path'), + 'mitogen_lxc_attach_path': hostvars.get('mitogen_lxc_attach_path'), 'mitogen_lxc_info_path': hostvars.get('mitogen_lxc_info_path'), 'mitogen_machinectl_path': hostvars.get('mitogen_machinctl_path'), }) diff --git a/docs/ansible.rst b/docs/ansible.rst index c6e9d553..0918b2f1 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -768,10 +768,10 @@ Connect to classic LXC containers, like `lxc connection delegation is supported, and ``lxc-attach`` is always used rather than the LXC Python bindings, as is usual with ``lxc``. -The ``lxc-attach`` command must be available on the host machine. - * ``ansible_python_interpreter`` * ``ansible_host``: Name of LXC container (default: inventory hostname). +* ``mitogen_lxc_attach_path``: path to ``lxc-attach`` command if not available + on the system path. .. _method-lxd: @@ -786,6 +786,8 @@ the host machine. * ``ansible_python_interpreter`` * ``ansible_host``: Name of LXC container (default: inventory hostname). +* ``mitogen_lxc_path``: path to ``lxc`` command if not available on the system + path. .. _machinectl: From 144685a327243b3c74a36e08e5e08031eb05ca75 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 12:41:32 +0000 Subject: [PATCH 075/662] issue #409: more lxc/lxd stub tests, let tests run on vanilla. --- .../integration/stub_connections/all.yml | 2 ++ .../integration/stub_connections/kubectl.yml | 3 +++ .../integration/stub_connections/lxc.yml | 18 ++++++++++++++++++ .../integration/stub_connections/lxd.yml | 18 ++++++++++++++++++ .../integration/stub_connections/setns_lxc.yml | 6 +++--- .../integration/stub_connections/setns_lxd.yml | 7 +++++-- tests/data/stubs/stub-lxc-attach.py | 1 + tests/data/stubs/stub-lxc.py | 1 + 8 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 tests/ansible/integration/stub_connections/lxc.yml create mode 100644 tests/ansible/integration/stub_connections/lxd.yml diff --git a/tests/ansible/integration/stub_connections/all.yml b/tests/ansible/integration/stub_connections/all.yml index 9258c172..c845d872 100644 --- a/tests/ansible/integration/stub_connections/all.yml +++ b/tests/ansible/integration/stub_connections/all.yml @@ -1,3 +1,5 @@ - import_playbook: kubectl.yml +- import_playbook: lxc.yml +- import_playbook: lxd.yml - import_playbook: setns_lxc.yml - import_playbook: setns_lxd.yml diff --git a/tests/ansible/integration/stub_connections/kubectl.yml b/tests/ansible/integration/stub_connections/kubectl.yml index c5768a00..ba53d1e0 100644 --- a/tests/ansible/integration/stub_connections/kubectl.yml +++ b/tests/ansible/integration/stub_connections/kubectl.yml @@ -4,6 +4,9 @@ gather_facts: false any_errors_fatal: true tasks: + - meta: end_play + when: not is_mitogen + - meta: end_play when: ansible_version.full < '2.5' diff --git a/tests/ansible/integration/stub_connections/lxc.yml b/tests/ansible/integration/stub_connections/lxc.yml new file mode 100644 index 00000000..7a2cd81c --- /dev/null +++ b/tests/ansible/integration/stub_connections/lxc.yml @@ -0,0 +1,18 @@ + +- name: integration/stub_connections/lxc.yml + hosts: test-targets + gather_facts: false + any_errors_fatal: true + tasks: + - meta: end_play + when: not is_mitogen + + - custom_python_detect_environment: + vars: + ansible_connection: lxc + mitogen_lxc_attach_path: stub-lxc-attach.py + register: out + + - assert: + that: + - out.env.THIS_IS_STUB_LXC_ATTACH == '1' diff --git a/tests/ansible/integration/stub_connections/lxd.yml b/tests/ansible/integration/stub_connections/lxd.yml new file mode 100644 index 00000000..86f4b185 --- /dev/null +++ b/tests/ansible/integration/stub_connections/lxd.yml @@ -0,0 +1,18 @@ + +- name: integration/stub_connections/lxd.yml + hosts: test-targets + gather_facts: false + any_errors_fatal: true + tasks: + - meta: end_play + when: not is_mitogen + + - custom_python_detect_environment: + vars: + ansible_connection: lxd + mitogen_lxc_path: stub-lxc.py + register: out + + - assert: + that: + - out.env.THIS_IS_STUB_LXC == '1' diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml index 19e2a984..a1feb4fe 100644 --- a/tests/ansible/integration/stub_connections/setns_lxc.yml +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -8,6 +8,9 @@ any_errors_fatal: false connection: local tasks: + - meta: end_play + when: not is_mitogen + - include_tasks: _end_play_if_not_sudo_linux.yml - command: | @@ -22,9 +25,6 @@ args: chdir: ../.. warn: false - when: | - out.ansible_facts.ansible_system == 'Linux' - and sudo_available.rc == 0 register: result - assert: diff --git a/tests/ansible/integration/stub_connections/setns_lxd.yml b/tests/ansible/integration/stub_connections/setns_lxd.yml index e552a656..b507f412 100644 --- a/tests/ansible/integration/stub_connections/setns_lxd.yml +++ b/tests/ansible/integration/stub_connections/setns_lxd.yml @@ -2,12 +2,15 @@ # setns is hard -- it wants to do superuser syscalls, so we must run it in a # child Ansible via sudo. But that only works if sudo works. -- name: integration/stub_connections/setns_lxc.yml +- name: integration/stub_connections/setns_lxd.yml hosts: test-targets gather_facts: false any_errors_fatal: false connection: local tasks: + - meta: end_play + when: not is_mitogen + - include_tasks: _end_play_if_not_sudo_linux.yml - command: | @@ -15,7 +18,7 @@ -i localhost, -c setns -e mitogen_kind=lxd - -e mitogen_lxc_info_path=stub-lxc.py + -e mitogen_lxc_path=stub-lxc.py -m shell -a "echo hi" localhost diff --git a/tests/data/stubs/stub-lxc-attach.py b/tests/data/stubs/stub-lxc-attach.py index 2fedb961..5263d362 100755 --- a/tests/data/stubs/stub-lxc-attach.py +++ b/tests/data/stubs/stub-lxc-attach.py @@ -4,4 +4,5 @@ import sys import os os.environ['ORIGINAL_ARGV'] = repr(sys.argv) +os.environ['THIS_IS_STUB_LXC_ATTACH'] = '1' os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) diff --git a/tests/data/stubs/stub-lxc.py b/tests/data/stubs/stub-lxc.py index b1448bec..03572cac 100755 --- a/tests/data/stubs/stub-lxc.py +++ b/tests/data/stubs/stub-lxc.py @@ -9,4 +9,5 @@ if sys.argv[1] == 'info': sys.exit(0) os.environ['ORIGINAL_ARGV'] = repr(sys.argv) +os.environ['THIS_IS_STUB_LXC'] = '1' os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) From 7314b54afde0f9427f309b9190bb19ab9d9091d3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 12:44:25 +0000 Subject: [PATCH 076/662] issue #409: update Changelog. --- docs/changelog.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c31dac3e..8e1be53f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -51,8 +51,12 @@ Fixes normally logged to the console by these tools. * `#409 `_: the setns method was - silently broken due to missing tests. The issue was fixed and basic test - coverage was added. + silently broken due to missing tests. Basic coverage was added to prevent a + recurrence. + +* `#409 `_: the LXC and LXD methods + support ``mitogen_lxc_path`` and ``mitogen_lxc_attach`` variables to control + the location of third pary utilities. Core Library From 1502e90599fdae5ff04d88375d66943371f999e3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 12:56:03 +0000 Subject: [PATCH 077/662] Import PULL_REQUEST_TEMPLATE.md. --- .github/PULL_REQUEST_TEMPLATE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..2f8ca63b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ + +Thanks for creating a PR! Here's a quick checklist to pay attention to: + +* Please add an entry to ``docs/changelog.rst`` as appropriate. + +* Has some new parameter been added or semantics modified somehow? Please + ensure relevant documentation is updated in ``docs/ansible.rst`` and + ``docs/api.rst``. + +* If it's for new functionality, is there at least a basic test in either + ``tests/`` or ``tests/ansible/`` covering it? + +* If it's for a new connection method, please try to stub out the + implementation as in ``tests/data/stubs/``, so that construction can at least + be tested without having a full configruation. + From 95f95ce868ea2c66a4de6fbf9b9ff73137724fd8 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 13:14:30 +0000 Subject: [PATCH 078/662] Update PULL_REQUEST_TEMPLATE.md. --- .github/PULL_REQUEST_TEMPLATE.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2f8ca63b..116b0c79 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,16 @@ Thanks for creating a PR! Here's a quick checklist to pay attention to: -* Please add an entry to ``docs/changelog.rst`` as appropriate. +* Please add an entry to docs/changelog.rst as appropriate. * Has some new parameter been added or semantics modified somehow? Please - ensure relevant documentation is updated in ``docs/ansible.rst`` and - ``docs/api.rst``. + ensure relevant documentation is updated in docs/ansible.rst and + docs/api.rst. * If it's for new functionality, is there at least a basic test in either - ``tests/`` or ``tests/ansible/`` covering it? + tests/ or tests/ansible/ covering it? * If it's for a new connection method, please try to stub out the - implementation as in ``tests/data/stubs/``, so that construction can at least - be tested without having a full configruation. + implementation as in tests/data/stubs/, so that construction can be tested + without having a working configuration. From 0394dac2c72ac2b0a32739b5c08dd659f6590cc4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 15:06:28 +0000 Subject: [PATCH 079/662] docs: document RouteMonitor class. --- docs/internals.rst | 8 +++++ mitogen/parent.py | 88 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 03f12e1e..87794815 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -236,6 +236,14 @@ Responder Class :members: +RouteMonitor Class +------------------ + +.. currentmodule:: mitogen.parent +.. autoclass:: RouteMonitor + :members: + + Forwarder Class --------------- diff --git a/mitogen/parent.py b/mitogen/parent.py index 4549d877..78a62380 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1431,6 +1431,29 @@ class Context(mitogen.core.Context): class RouteMonitor(object): + """ + Generate and respond to :data:`mitogen.core.ADD_ROUTE` and + :data:`mitogen.core.DEL_ROUTE` messages sent to the local context by + maintaining a table of available routes, and propagating messages towards + parents and siblings as appropriate. + + :class:`RouteMonitor` is responsible for generating routing messages for + directly attached children. It learns of new children via + :meth:`notice_stream` called by :class:`Router`, and subscribes to their + ``disconnect`` event to learn when they disappear. + + In children, constructing this class overwrites the stub + :data:`mitogen.core.DEL_ROUTE` handler installed by + :class:`mitogen.core.ExternalContext`, which is expected behaviour when a + child is beging upgraded in preparation to become a parent of children of + its own. + + :param Router router: + Router to install handlers on. + :param Context parent: + :data:`None` in the master process, or reference to the parent context + we should propagate route updates towards. + """ def __init__(self, router, parent=None): self.router = router self.parent = parent @@ -1451,6 +1474,18 @@ class RouteMonitor(object): self._routes_by_stream = {} def _send_one(self, stream, handle, target_id, name): + """ + Compose and send an update message on a stream. + + :param mitogen.core.Stream stream: + Stream to send it on. + :param int handle: + :data:`mitogen.core.ADD_ROUTE` or :data:`mitogen.core.DEL_ROUTE` + :param int target_id: + ID of the connecting or disconnecting context. + :param str name: + Context name or :data:`None`. + """ data = str(target_id) if name: data = '%s:%s' % (target_id, mitogen.core.b(name)) @@ -1462,20 +1497,34 @@ class RouteMonitor(object): ) ) - def _propagate(self, handle, target_id, name=None): - if not self.parent: - # self.parent is None in the master. - return - - stream = self.router.stream_by_id(self.parent.context_id) - self._send_one(stream, handle, target_id, name) + def _propagate_up(self, handle, target_id, name=None): + """ + In a non-master context, propagate an update towards the master. + + :param int handle: + :data:`mitogen.core.ADD_ROUTE` or :data:`mitogen.core.DEL_ROUTE` + :param int target_id: + ID of the connecting or disconnecting context. + :param str name: + For :data:`mitogen.core.ADD_ROUTE`, the name of the new context + assigned by its parent. This is used by parents to assign the + :attr:`mitogen.core.Context.name` attribute. + """ + if self.parent: + stream = self.router.stream_by_id(self.parent.context_id) + self._send_one(stream, handle, target_id, name) - def _child_propagate(self, handle, target_id): + def _propagate_down(self, handle, target_id): """ For DEL_ROUTE, we additionally want to broadcast the message to any stream that has ever communicated with the disconnecting ID, so core.py's :meth:`mitogen.core.Router._on_del_route` can turn the message into a disconnect event. + + :param int handle: + :data:`mitogen.core.ADD_ROUTE` or :data:`mitogen.core.DEL_ROUTE` + :param int target_id: + ID of the connecting or disconnecting context. """ for stream in itervalues(self.router._stream_by_id): if target_id in stream.egress_ids: @@ -1488,7 +1537,7 @@ class RouteMonitor(object): if/when that child disconnects. """ self._routes_by_stream[stream] = set([stream.remote_id]) - self._propagate(mitogen.core.ADD_ROUTE, stream.remote_id, + self._propagate_up(mitogen.core.ADD_ROUTE, stream.remote_id, stream.name) mitogen.core.listen( obj=stream, @@ -1513,14 +1562,19 @@ class RouteMonitor(object): LOG.debug('%r is gone; propagating DEL_ROUTE for %r', stream, routes) for target_id in routes: self.router.del_route(target_id) - self._propagate(mitogen.core.DEL_ROUTE, target_id) - self._child_propagate(mitogen.core.DEL_ROUTE, target_id) + self._propagate_up(mitogen.core.DEL_ROUTE, target_id) + self._propagate_down(mitogen.core.DEL_ROUTE, target_id) context = self.router.context_by_id(target_id, create=False) if context: mitogen.core.fire(context, 'disconnect') def _on_add_route(self, msg): + """ + Respond to :data:`mitogen.core.ADD_ROUTE` by validating the source of + the message, updating the local table, and propagating the message + upwards. + """ if msg.is_dead: return @@ -1539,9 +1593,15 @@ class RouteMonitor(object): LOG.debug('Adding route to %d via %r', target_id, stream) self._routes_by_stream[stream].add(target_id) self.router.add_route(target_id, stream) - self._propagate(mitogen.core.ADD_ROUTE, target_id, target_name) + self._propagate_up(mitogen.core.ADD_ROUTE, target_id, target_name) def _on_del_route(self, msg): + """ + Respond to :data:`mitogen.core.DEL_ROUTE` by validating the source of + the message, updating the local table, propagating the message + upwards, and downwards towards any stream that every had a message + forwarded from it towards the disconnecting context. + """ if msg.is_dead: return @@ -1565,8 +1625,8 @@ class RouteMonitor(object): self.router.del_route(target_id) if stream.remote_id != mitogen.parent_id: - self._propagate(mitogen.core.DEL_ROUTE, target_id) - self._child_propagate(mitogen.core.DEL_ROUTE, target_id) + self._propagate_up(mitogen.core.DEL_ROUTE, target_id) + self._propagate_down(mitogen.core.DEL_ROUTE, target_id) class Router(mitogen.core.Router): From 0d410aef51e611248d0f9930ac2050276d9b7cdd Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 15:08:27 +0000 Subject: [PATCH 080/662] docs: fix internals.rst headings. --- docs/internals.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/internals.rst b/docs/internals.rst index 87794815..9c533952 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -32,7 +32,7 @@ PidfulStreamHandler Class Side Class ----------- +========== .. currentmodule:: mitogen.core @@ -105,7 +105,7 @@ Side Class Stream Classes --------------- +============== .. currentmodule:: mitogen.core @@ -196,7 +196,7 @@ Stream Classes Other Stream Subclasses ------------------------ +======================= .. currentmodule:: mitogen.core @@ -208,7 +208,7 @@ Other Stream Subclasses Poller Class ------------- +============ .. currentmodule:: mitogen.core .. autoclass:: Poller @@ -221,7 +221,7 @@ Poller Class Importer Class --------------- +============== .. currentmodule:: mitogen.core .. autoclass:: Importer @@ -229,7 +229,7 @@ Importer Class Responder Class ---------------- +=============== .. currentmodule:: mitogen.master .. autoclass:: ModuleResponder @@ -237,7 +237,7 @@ Responder Class RouteMonitor Class ------------------- +================== .. currentmodule:: mitogen.parent .. autoclass:: RouteMonitor @@ -245,7 +245,7 @@ RouteMonitor Class Forwarder Class ---------------- +=============== .. currentmodule:: mitogen.parent .. autoclass:: ModuleForwarder @@ -253,7 +253,7 @@ Forwarder Class ExternalContext Class ---------------------- +===================== .. currentmodule:: mitogen.core From fadb9181bcff12152abb2e90057840a3c79bb956 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 15:46:59 +0000 Subject: [PATCH 081/662] issue #410: support sudo --user and SELinux options, add stub test. --- docs/api.rst | 4 ++++ mitogen/sudo.py | 47 +++++++++++++++++++++++------------- tests/sudo_test.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 tests/sudo_test.py diff --git a/docs/api.rst b/docs/api.rst index 52d5dcec..ea20ada7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -717,6 +717,10 @@ Router Class :param bool preserve_env: If :data:`True`, request ``sudo`` to preserve the environment of the parent process. + :param str selinux_type: + If not :data:`None`, the SELinux security context to use. + :param str selinux_role: + If not :data:`None`, the SELinux role to use. :param list sudo_args: Arguments in the style of :data:`sys.argv` that would normally be passed to ``sudo``. The arguments are parsed in-process to set diff --git a/mitogen/sudo.py b/mitogen/sudo.py index c410dac9..84b81ddc 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -55,7 +55,10 @@ SUDO_OPTIONS = [ #(False, 'bool', '--list', '-l') #(False, 'bool', '--preserve-groups', '-P') #(False, 'str', '--prompt', '-p') - #(False, 'str', '--role', '-r') + + # SELinux options. Passed through as-is. + (False, 'str', '--role', '-r'), + (False, 'str', '--type', '-t'), # These options are supplied by default by Ansible, but are ignored, as # sudo always runs under a TTY with Mitogen. @@ -63,9 +66,8 @@ SUDO_OPTIONS = [ (True, 'bool', '--non-interactive', '-n'), #(False, 'str', '--shell', '-s') - #(False, 'str', '--type', '-t') #(False, 'str', '--other-user', '-U') - #(False, 'str', '--user', '-u') + (False, 'str', '--user', '-u'), #(False, 'bool', '--version', '-V') #(False, 'bool', '--validate', '-v') ] @@ -103,6 +105,13 @@ class PasswordError(mitogen.core.StreamError): pass +def option(default, *args): + for arg in args: + if arg is not None: + return arg + return default + + class Stream(mitogen.parent.Stream): create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) child_is_immediate_subprocess = False @@ -118,24 +127,24 @@ class Stream(mitogen.parent.Stream): set_home = False login = False + selinux_role = None + selinux_type = None + def construct(self, username=None, sudo_path=None, password=None, preserve_env=None, set_home=None, sudo_args=None, - login=None, **kwargs): + login=None, selinux_role=None, selinux_type=None, **kwargs): super(Stream, self).construct(**kwargs) opts = parse_sudo_flags(sudo_args or []) - if username is not None: - self.username = username - if sudo_path is not None: - self.sudo_path = sudo_path - if password is not None: - self.password = password - if (preserve_env or opts.preserve_env) is not None: - self.preserve_env = preserve_env or opts.preserve_env - if (set_home or opts.set_home) is not None: - self.set_home = set_home or opts.set_home - if (login or opts.login) is not None: - self.login = True + self.username = option(self.username, username, opts.user) + self.sudo_path = option(self.sudo_path, sudo_path) + self.password = password or None + self.preserve_env = option(self.preserve_env, + preserve_env, opts.preserve_env) + self.set_home = option(self.set_home, set_home, opts.set_home) + self.login = option(self.login, login, opts.login) + self.selinux_role = option(self.selinux_role, selinux_role, opts.role) + self.selinux_type = option(self.selinux_type, selinux_type, opts.type) def connect(self): super(Stream, self).connect() @@ -156,8 +165,12 @@ class Stream(mitogen.parent.Stream): bits += ['-H'] if self.login: bits += ['-i'] + if self.selinux_role: + bits += ['-r', self.selinux_role] + if self.selinux_type: + bits += ['-t', self.selinux_type] - bits = bits + super(Stream, self).get_boot_command() + bits = bits + ['--'] + super(Stream, self).get_boot_command() LOG.debug('sudo command line: %r', bits) return bits diff --git a/tests/sudo_test.py b/tests/sudo_test.py new file mode 100644 index 00000000..87a13cf9 --- /dev/null +++ b/tests/sudo_test.py @@ -0,0 +1,60 @@ + +import os + +import mitogen +import mitogen.lxd +import mitogen.parent + +import unittest2 + +import testlib + + +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): + sudo_path = testlib.data_path('stubs/stub-sudo.py') + + def run_sudo(self, **kwargs): + context = self.router.sudo( + sudo_path=self.sudo_path, + **kwargs + ) + argv = eval(context.call(os.getenv, 'ORIGINAL_ARGV')) + return context, argv + + + def test_basic(self): + context, argv = self.run_sudo() + self.assertEquals(argv[:4], [ + self.sudo_path, + '-u', 'root', + '--' + ]) + + def test_selinux_type_role(self): + context, argv = self.run_sudo( + selinux_type='setype', + selinux_role='serole', + ) + self.assertEquals(argv[:8], [ + self.sudo_path, + '-u', 'root', + '-r', 'serole', + '-t', 'setype', + '--' + ]) + + def test_reparse_args(self): + context, argv = self.run_sudo( + sudo_args=['--type', 'setype', '--role', 'serole', '--user', 'user'] + ) + self.assertEquals(argv[:8], [ + self.sudo_path, + '-u', 'user', + '-r', 'serole', + '-t', 'setype', + '--' + ]) + + +if __name__ == '__main__': + unittest2.main() From c89675112bc41c268a35fd41c0a3e66420e55bc1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 16:05:08 +0000 Subject: [PATCH 082/662] issue #410: update changelog --- docs/changelog.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8e1be53f..7c7609de 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -58,6 +58,9 @@ Fixes support ``mitogen_lxc_path`` and ``mitogen_lxc_attach`` variables to control the location of third pary utilities. +* `#410 `_: the sudo method supports + the SELinux ``--type`` and ``--role`` options. + Core Library ~~~~~~~~~~~~ @@ -96,8 +99,9 @@ Thanks! Mitogen would not be possible without the support of users. A huge thanks for bug reports, features and fixes in this release contributed by `Brian Candler `_, -`Guy Knights `_, and -`Jonathan Rosser `_. +`Guy Knights `_, +`Jonathan Rosser `_, and +`Mehdi `_. v0.2.3 (2018-10-23) From a6dd8bb2d0b3698d967e576c31a611f285ff3a5f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 16:16:28 +0000 Subject: [PATCH 083/662] issue #409: stub test for mitogen_sudo method. --- .../integration/stub_connections/all.yml | 1 + .../integration/stub_connections/sudo.yml | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/ansible/integration/stub_connections/sudo.yml diff --git a/tests/ansible/integration/stub_connections/all.yml b/tests/ansible/integration/stub_connections/all.yml index c845d872..5a3f37cf 100644 --- a/tests/ansible/integration/stub_connections/all.yml +++ b/tests/ansible/integration/stub_connections/all.yml @@ -3,3 +3,4 @@ - import_playbook: lxd.yml - import_playbook: setns_lxc.yml - import_playbook: setns_lxd.yml +- import_playbook: sudo.yml diff --git a/tests/ansible/integration/stub_connections/sudo.yml b/tests/ansible/integration/stub_connections/sudo.yml new file mode 100644 index 00000000..b5e6f263 --- /dev/null +++ b/tests/ansible/integration/stub_connections/sudo.yml @@ -0,0 +1,20 @@ + +- name: integration/stub_connections/sudo.yml + hosts: test-targets + gather_facts: false + any_errors_fatal: true + tasks: + - meta: end_play + when: not is_mitogen + + - custom_python_detect_environment: + vars: + ansible_connection: mitogen_sudo + ansible_become_exe: stub-sudo.py + ansible_become_flags: --type=sometype --role=somerole + register: out + + - assert: + that: + - out.env.THIS_IS_STUB_SUDO == '1' + - (out.env.ORIGINAL_ARGV|from_json)[1:9] == ['-u', 'root', '-H', '-r', 'somerole', '-t', 'sometype', '--'] From fcdf4a0f355fd95c591738aa1ae887518f1f97f3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 16:18:04 +0000 Subject: [PATCH 084/662] Import missing stub-sudo.py. --- tests/data/stubs/stub-sudo.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100755 tests/data/stubs/stub-sudo.py diff --git a/tests/data/stubs/stub-sudo.py b/tests/data/stubs/stub-sudo.py new file mode 100755 index 00000000..ff88cd8e --- /dev/null +++ b/tests/data/stubs/stub-sudo.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python + +import json +import os +import sys + +os.environ['ORIGINAL_ARGV'] = json.dumps(sys.argv) +os.environ['THIS_IS_STUB_SUDO'] = '1' +os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) From d280bba02b978629d4efeccccf16b0ae2e442753 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 16:24:44 +0000 Subject: [PATCH 085/662] issue #369: fix KeyError during new context start. Update _via_by_context earlier; fixes: Traceback (most recent call last): File "/Users/dmw/src/mitogen/mitogen/service.py", line 519, in _on_service_call return invoker.invoke(method_name, kwargs, msg) File "/Users/dmw/src/mitogen/mitogen/service.py", line 253, in invoke response = self._invoke(method_name, kwargs, msg) File "/Users/dmw/src/mitogen/mitogen/service.py", line 239, in _invoke ret = method(**kwargs) File "/Users/dmw/src/mitogen/ansible_mitogen/services.py", line 454, in get reraise(*result) File "/Users/dmw/src/mitogen/ansible_mitogen/services.py", line 412, in _wait_or_start response = self._connect(key, spec, via=via) File "/Users/dmw/src/mitogen/ansible_mitogen/services.py", line 363, in _connect self._update_lru(context, spec, via) File "/Users/dmw/src/mitogen/ansible_mitogen/services.py", line 266, in _update_lru self._update_lru_unlocked(new_context, spec, via) File "/Users/dmw/src/mitogen/ansible_mitogen/services.py", line 253, in _update_lru_unlocked if self._refs_by_context[context] == 0: KeyError: Context(1008, u'ssh.localhost.sudo.mitogen__user3') --- ansible_mitogen/services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index dde44c89..0ce87e09 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -244,6 +244,8 @@ class ContextService(mitogen.service.Service): by `kwargs`, destroying the most recently created context if the list is full. Finally add `new_context` to the list. """ + self._via_by_context[new_context] = via + lru = self._lru_by_via.setdefault(via, []) if len(lru) < self.max_interpreters: lru.append(new_context) @@ -257,7 +259,6 @@ class ContextService(mitogen.service.Service): 'but they are all marked as in-use.', via) return - self._via_by_context[new_context] = via self._shutdown_unlocked(context, lru=lru, new_context=new_context) def _update_lru(self, new_context, spec, via): From 6107ebdc0d817696c8182a834f57c150cf5178ba Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 16:35:25 +0000 Subject: [PATCH 086/662] issue #396: fix compatibility with Connection._reset(). --- ansible_mitogen/connection.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 140c901b..97c56498 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -579,7 +579,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): self.host_vars = task_vars['hostvars'] self.delegate_to_hostname = delegate_to_hostname self.loader_basedir = loader_basedir - self._reset(mode='put') + self._mitogen_reset(mode='put') def get_task_var(self, key, default=None): if self._task_vars and key in self._task_vars: @@ -740,10 +740,10 @@ class Connection(ansible.plugins.connection.ConnectionBase): def _reset_tmp_path(self): """ - Called by _reset(); ask the remote context to delete any temporary - directory created for the action. CallChain is not used here to ensure - exception is logged by the context on failure, since the CallChain - itself is about to be destructed. + Called by _mitogen_reset(); ask the remote context to delete any + temporary directory created for the action. CallChain is not used here + to ensure exception is logged by the context on failure, since the + CallChain itself is about to be destructed. """ if getattr(self._shell, 'tmpdir', None) is not None: self.context.call_no_reply( @@ -770,9 +770,11 @@ class Connection(ansible.plugins.connection.ConnectionBase): stack = self._build_stack() self._connect_stack(stack) - def _reset(self, mode): + def _mitogen_reset(self, mode): """ - Forget everything we know about the connected context. + Forget everything we know about the connected context. This function + cannot be called _reset() since that name is used as a public API by + Ansible 2.4 wait_for_connection plug-in. :param str mode: Name of ContextService method to use to discard the context, either @@ -815,7 +817,10 @@ class Connection(ansible.plugins.connection.ConnectionBase): bad somehow, and should be shut down and discarded. """ self._connect() - self._reset(mode='reset') + self._mitogen_reset(mode='reset') + + # Compatibility with Ansible 2.4 wait_for_connection plug-in. + _reset = reset def get_chain(self, use_login=False, use_fork=False): """ From ab4ccc6b920d0249341db58f51ecdf02b2a419de Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 16:36:03 +0000 Subject: [PATCH 087/662] issue #369: don't mass-kill all SSH clients in reconnection.yml It breaks my new development environment :) --- tests/ansible/integration/context_service/reconnection.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/ansible/integration/context_service/reconnection.yml b/tests/ansible/integration/context_service/reconnection.yml index f56719d8..eed1dfdb 100644 --- a/tests/ansible/integration/context_service/reconnection.yml +++ b/tests/ansible/integration/context_service/reconnection.yml @@ -5,15 +5,18 @@ hosts: test-targets any_errors_fatal: true tasks: + - mitogen_shutdown_all: + + - custom_python_detect_environment: + register: ssh_account_env - become: true custom_python_detect_environment: register: old_become_env - become: true - # This must be >1 for vanilla Ansible. shell: | - bash -c "( sleep 3; pkill -f sshd:; ) & disown" + bash -c "( sleep 3; kill -9 {{ssh_account_env.pid}}; ) & disown" - connection: local shell: sleep 3 From c4aec22a33c58841c0b09bbc4445fd03520e1917 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 16:37:42 +0000 Subject: [PATCH 088/662] issue #369: fix one more _reset() reference. --- ansible_mitogen/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 97c56498..b566b541 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -802,7 +802,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): gracefully shut down, and wait for shutdown to complete. Safe to call multiple times. """ - self._reset(mode='put') + self._mitogen_reset(mode='put') if self.broker: self.broker.shutdown() self.broker.join() From 8ed72e7e7bbce6e87db1ee0a90ee6b63d721d6ee Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 18:18:49 +0000 Subject: [PATCH 089/662] issue #369: avoid Ansible 2.5 bug (cond_reset_warn missing method) --- tests/ansible/integration/connection/reset.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ansible/integration/connection/reset.yml b/tests/ansible/integration/connection/reset.yml index 56e901b7..a0ba520b 100644 --- a/tests/ansible/integration/connection/reset.yml +++ b/tests/ansible/integration/connection/reset.yml @@ -6,8 +6,9 @@ - name: integration/connection/reset.yml hosts: test-targets tasks: - - when: is_mitogen - block: + - meta: end_play + when: not is_mitogen + - custom_python_detect_environment: register: out From 6c71c5bfefa5552a2241a7daabf308b52d6b9bfe Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 18:30:03 +0000 Subject: [PATCH 090/662] issue #369: disable reset_connection on Ansible<2.5.6 https://github.com/ansible/ansible/issues/27520 --- ansible_mitogen/connection.py | 27 +++++++++++++++---- .../ansible/integration/connection/reset.yml | 6 +++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index b566b541..e017608e 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -31,6 +31,7 @@ from __future__ import unicode_literals import logging import os +import pprint import random import stat import time @@ -699,11 +700,16 @@ class Connection(ansible.plugins.connection.ConnectionBase): representing the target. If no connection exists yet, ContextService will establish it before returning it or throwing an error. """ - dct = self.parent.call_service( - service_name='ansible_mitogen.services.ContextService', - method_name='get', - stack=mitogen.utils.cast(list(stack)), - ) + try: + dct = self.parent.call_service( + service_name='ansible_mitogen.services.ContextService', + method_name='get', + stack=mitogen.utils.cast(list(stack)), + ) + except mitogen.core.CallError: + LOG.warning('Connection failed; stack configuration was:\n%s', + pprint.pformat(stack)) + raise if dct['msg']: if dct['method_name'] in self.become_methods: @@ -809,6 +815,10 @@ class Connection(ansible.plugins.connection.ConnectionBase): self.broker = None self.router = None + reset_compat_msg = ( + 'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later' + ) + def reset(self): """ Explicitly terminate the connection to the remote host. This discards @@ -816,6 +826,13 @@ class Connection(ansible.plugins.connection.ConnectionBase): the 'disconnected' state, and informs ContextService the connection is bad somehow, and should be shut down and discarded. """ + if self._play_context.remote_addr is None: + # <2.5.6 incorrectly populate PlayContext for reset_connection + # https://github.com/ansible/ansible/issues/27520 + raise ansible.errors.AnsibleConnectionFailure( + self.reset_compat_msg + ) + self._connect() self._mitogen_reset(mode='reset') diff --git a/tests/ansible/integration/connection/reset.yml b/tests/ansible/integration/connection/reset.yml index a0ba520b..768cd2d5 100644 --- a/tests/ansible/integration/connection/reset.yml +++ b/tests/ansible/integration/connection/reset.yml @@ -9,6 +9,12 @@ - meta: end_play when: not is_mitogen + - debug: msg="reset.yml skipped on Ansible<2.5.6" + when: ansible_version.full < '2.5.6' + + - meta: end_play + when: ansible_version.full < '2.5.6' + - custom_python_detect_environment: register: out From e8fc9e490f5879ad5f52aeddb5d98596c8f0f6f6 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 19:14:58 +0000 Subject: [PATCH 091/662] tests: update osa_delegate_to_self to match connection parameters --- tests/ansible/integration/delegation/osa_delegate_to_self.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ansible/integration/delegation/osa_delegate_to_self.yml b/tests/ansible/integration/delegation/osa_delegate_to_self.yml index 0915bbb8..a9cd6c6e 100644 --- a/tests/ansible/integration/delegation/osa_delegate_to_self.yml +++ b/tests/ansible/integration/delegation/osa_delegate_to_self.yml @@ -22,6 +22,7 @@ 'docker_path': None, 'kind': 'lxc', 'lxc_info_path': None, + 'lxc_path': None, 'machinectl_path': None, 'python_path': None, 'username': None, From cbd4129cb9f06f2b149f2596ac6e045dea3c7d87 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 31 Oct 2018 19:15:23 +0000 Subject: [PATCH 092/662] tests: fix paramiko_unblemished.yml --- .../integration/connection_loader/paramiko_unblemished.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ansible/integration/connection_loader/paramiko_unblemished.yml b/tests/ansible/integration/connection_loader/paramiko_unblemished.yml index a71af868..de8de4b0 100644 --- a/tests/ansible/integration/connection_loader/paramiko_unblemished.yml +++ b/tests/ansible/integration/connection_loader/paramiko_unblemished.yml @@ -1,6 +1,6 @@ # Ensure paramiko connections aren't grabbed. -- name: integration/connection_loader__paramiko_unblemished.yml +- name: integration/connection_loader/paramiko_unblemished.yml hosts: test-targets any_errors_fatal: true tasks: From fd326f5ad715bcecbbecdfdfae4e3f36bf7b6381 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 00:49:18 +0000 Subject: [PATCH 093/662] tests: stub tests for doas/mitogen_doas --- .../integration/stub_connections/all.yml | 3 +- .../stub_connections/mitogen_doas.yml | 21 +++++++++++++ .../{sudo.yml => mitogen_sudo.yml} | 2 +- tests/data/stubs/stub-doas.py | 9 ++++++ tests/doas_test.py | 31 +++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/ansible/integration/stub_connections/mitogen_doas.yml rename tests/ansible/integration/stub_connections/{sudo.yml => mitogen_sudo.yml} (90%) create mode 100755 tests/data/stubs/stub-doas.py create mode 100644 tests/doas_test.py diff --git a/tests/ansible/integration/stub_connections/all.yml b/tests/ansible/integration/stub_connections/all.yml index 5a3f37cf..a9744ab7 100644 --- a/tests/ansible/integration/stub_connections/all.yml +++ b/tests/ansible/integration/stub_connections/all.yml @@ -1,6 +1,7 @@ - import_playbook: kubectl.yml - import_playbook: lxc.yml - import_playbook: lxd.yml +- import_playbook: mitogen_doas.yml +- import_playbook: mitogen_sudo.yml - import_playbook: setns_lxc.yml - import_playbook: setns_lxd.yml -- import_playbook: sudo.yml diff --git a/tests/ansible/integration/stub_connections/mitogen_doas.yml b/tests/ansible/integration/stub_connections/mitogen_doas.yml new file mode 100644 index 00000000..40d4f4b0 --- /dev/null +++ b/tests/ansible/integration/stub_connections/mitogen_doas.yml @@ -0,0 +1,21 @@ + +- name: integration/stub_connections/mitogen_doas.yml + hosts: test-targets + gather_facts: false + any_errors_fatal: true + tasks: + - meta: end_play + when: not is_mitogen + + - custom_python_detect_environment: + vars: + ansible_connection: mitogen_doas + ansible_become_exe: stub-doas.py + ansible_user: someuser + register: out + + - debug: var=out.env.ORIGINAL_ARGV + - assert: + that: + - out.env.THIS_IS_STUB_DOAS == '1' + - (out.env.ORIGINAL_ARGV|from_json)[1:3] == ['-u', 'someuser'] diff --git a/tests/ansible/integration/stub_connections/sudo.yml b/tests/ansible/integration/stub_connections/mitogen_sudo.yml similarity index 90% rename from tests/ansible/integration/stub_connections/sudo.yml rename to tests/ansible/integration/stub_connections/mitogen_sudo.yml index b5e6f263..b82b3ac2 100644 --- a/tests/ansible/integration/stub_connections/sudo.yml +++ b/tests/ansible/integration/stub_connections/mitogen_sudo.yml @@ -1,5 +1,5 @@ -- name: integration/stub_connections/sudo.yml +- name: integration/stub_connections/mitogen_sudo.yml hosts: test-targets gather_facts: false any_errors_fatal: true diff --git a/tests/data/stubs/stub-doas.py b/tests/data/stubs/stub-doas.py new file mode 100755 index 00000000..08caf044 --- /dev/null +++ b/tests/data/stubs/stub-doas.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python + +import json +import os +import sys + +os.environ['ORIGINAL_ARGV'] = json.dumps(sys.argv) +os.environ['THIS_IS_STUB_DOAS'] = '1' +os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) diff --git a/tests/doas_test.py b/tests/doas_test.py new file mode 100644 index 00000000..0e27c2ab --- /dev/null +++ b/tests/doas_test.py @@ -0,0 +1,31 @@ + +import os + +import mitogen +import mitogen.parent + +import unittest2 + +import testlib + + +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): + doas_path = testlib.data_path('stubs/stub-doas.py') + + def test_okay(self): + context = self.router.doas( + doas_path=self.doas_path, + username='someuser', + ) + argv = eval(context.call(os.getenv, 'ORIGINAL_ARGV')) + self.assertEquals(argv[:4], [ + self.doas_path, + '-u', + 'someuser', + '--', + ]) + self.assertEquals('1', context.call(os.getenv, 'THIS_IS_STUB_DOAS')) + + +if __name__ == '__main__': + unittest2.main() From a7ee23719a28ac829e670d615c1364ea3239672e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 11:51:01 +0000 Subject: [PATCH 094/662] issue #388: move a ton of documentation back into the source --- docs/api.rst | 665 ++-------------------------------------------- mitogen/core.py | 372 ++++++++++++++++++++++++-- mitogen/parent.py | 42 +++ mitogen/select.py | 150 ++++++++++- mitogen/utils.py | 64 +++++ 5 files changed, 625 insertions(+), 668 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ea20ada7..844bb900 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -84,237 +84,16 @@ Message Class ============= .. currentmodule:: mitogen.core - -.. class:: Message - - Messages are the fundamental unit of communication, comprising fields from - the :ref:`stream-protocol` header, an optional reference to the receiving - :class:`mitogen.core.Router` for ingress messages, and helper methods for - deserialization and generating replies. - - .. attribute:: router - - The :class:`mitogen.core.Router` responsible for routing the - message. This is :data:`None` for locally originated messages. - - .. attribute:: receiver - - The :class:`mitogen.core.Receiver` over which the message was last - received. Part of the :class:`mitogen.select.Select` interface. - Defaults to :data:`None`. - - .. attribute:: dst_id - - Integer target context ID. :class:`mitogen.core.Router` delivers - messages locally when their :attr:`dst_id` matches - :data:`mitogen.context_id`, otherwise they are routed up or downstream. - - .. attribute:: src_id - - Integer source context ID. Used as the target of replies if any are - generated. - - .. attribute:: auth_id - - The context ID under whose authority the message is acting. See - :ref:`source-verification`. - - .. attribute:: handle - - Integer target handle in the destination context. This is one of the - :ref:`standard-handles`, or a dynamically generated handle used to - receive a one-time reply, such as the return value of a function call. - - .. attribute:: reply_to - - Integer target handle to direct any reply to this message. Used to - receive a one-time reply, such as the return value of a function call. - :data:`IS_DEAD` has a special meaning when it appears in this field. - - .. attribute:: data - - Message data, which may be raw or pickled. - - .. attribute:: is_dead - - :data:`True` if :attr:`reply_to` is set to the magic value - :data:`mitogen.core.IS_DEAD`, indicating the sender considers the - channel dead. - - .. py:method:: __init__ (\**kwargs) - - Construct a message from from the supplied `kwargs`. :attr:`src_id` - and :attr:`auth_id` are always set to :data:`mitogen.context_id`. - - .. py:classmethod:: pickled (obj, \**kwargs) - - Construct a pickled message, setting :attr:`data` to the - serialization of `obj`, and setting remaining fields using `kwargs`. - - :returns: - The new message. - - .. method:: unpickle (throw=True) - - Unpickle :attr:`data`, optionally raising any exceptions present. - - :param bool throw: - If :data:`True`, raise exceptions, otherwise it is the caller's - responsibility. - - :raises mitogen.core.CallError: - The serialized data contained CallError exception. - :raises mitogen.core.ChannelError: - The `is_dead` field was set. - - .. method:: reply (obj, router=None, \**kwargs) - - Compose a reply to this message and send it using :attr:`router`, or - `router` is :attr:`router` is :data:`None`. - - :param obj: - Either a :class:`Message`, or an object to be serialized in order - to construct a new message. - :param router: - Optional router to use if :attr:`router` is :data:`None`. - :param kwargs: - Optional keyword parameters overriding message fields in the reply. - +.. autoclass:: Message + :members: Router Class ============ .. currentmodule:: mitogen.core - -.. class:: Router - - Route messages between parent and child contexts, and invoke handlers - defined on our parent context. :meth:`Router.route() ` straddles - the :class:`Broker ` and user threads, it is safe - to call anywhere. - - **Note:** This is the somewhat limited core version of the Router class - used by child contexts. The master subclass is documented below this one. - - .. attribute:: unidirectional - - When :data:`True`, permit children to only communicate with the current - context or a parent of the current context. Routing between siblings or - children of parents is prohibited, ensuring no communication is - possible between intentionally partitioned networks, such as when a - program simultaneously manipulates hosts spread across a corporate and - a production network, or production networks that are otherwise - air-gapped. - - Sending a prohibited message causes an error to be logged and a dead - message to be sent in reply to the errant message, if that message has - ``reply_to`` set. - - The value of :data:`unidirectional` becomes the default for the - :meth:`local() ` `unidirectional` - parameter. - - .. method:: stream_by_id (dst_id) - - Return the :class:`mitogen.core.Stream` that should be used to - communicate with `dst_id`. If a specific route for `dst_id` is not - known, a reference to the parent context's stream is returned. - - .. method:: add_route (target_id, via_id) - - Arrange for messages whose `dst_id` is `target_id` to be forwarded on - the directly connected stream for `via_id`. This method is called - automatically in response to ``ADD_ROUTE`` messages, but remains public - for now while the design has not yet settled, and situations may arise - where routing is not fully automatic. - - .. method:: register (context, stream) - - Register a new context and its associated stream, and add the stream's - receive side to the I/O multiplexer. This This method remains public - for now while hte design has not yet settled. - - .. method:: add_handler (fn, handle=None, persist=True, respondent=None, policy=None) - - Invoke `fn(msg)` for each Message sent to `handle` from this context. - Unregister after one invocation if `persist` is :data:`False`. If - `handle` is :data:`None`, a new handle is allocated and returned. - - :param int handle: - If not :data:`None`, an explicit handle to register, usually one of - the ``mitogen.core.*`` constants. If unspecified, a new unused - handle will be allocated. - - :param bool persist: - If :data:`False`, the handler will be unregistered after a single - message has been received. - - :param mitogen.core.Context respondent: - Context that messages to this handle are expected to be sent from. - If specified, arranges for a dead message to be delivered to `fn` - when disconnection of the context is detected. - - In future `respondent` will likely also be used to prevent other - contexts from sending messages to the handle. - - :param function policy: - Function invoked as `policy(msg, stream)` where `msg` is a - :class:`mitogen.core.Message` about to be delivered, and - `stream` is the :class:`mitogen.core.Stream` on which it was - received. The function must return :data:`True`, otherwise an - error is logged and delivery is refused. - - Two built-in policy functions exist: - - * :func:`mitogen.core.has_parent_authority`: requires the - message arrived from a parent context, or a context acting with a - parent context's authority (``auth_id``). - - * :func:`mitogen.parent.is_immediate_child`: requires the - message arrived from an immediately connected child, for use in - messaging patterns where either something becomes buggy or - insecure by permitting indirect upstream communication. - - In case of refusal, and the message's ``reply_to`` field is - nonzero, a :class:`mitogen.core.CallError` is delivered to the - sender indicating refusal occurred. - - :return: - `handle`, or if `handle` was :data:`None`, the newly allocated - handle. - - .. method:: del_handler (handle) - - Remove the handle registered for `handle` - - :raises KeyError: - The handle wasn't registered. - - .. method:: _async_route(msg, stream=None) - - Arrange for `msg` to be forwarded towards its destination. If its - destination is the local context, then arrange for it to be dispatched - using the local handlers. - - This is a lower overhead version of :meth:`route` that may only be - called from the I/O multiplexer thread. - - :param mitogen.core.Stream stream: - If not :data:`None`, a reference to the stream the message arrived - on. Used for performing source route verification, to ensure - sensitive messages such as ``CALL_FUNCTION`` arrive only from - trusted contexts. - - .. method:: route(msg) - - Arrange for the :class:`Message` `msg` to be delivered to its - destination using any relevant downstream context, or if none is found, - by forwarding the message upstream towards the master context. If `msg` - is destined for the local context, it is dispatched using the handles - registered with :meth:`add_handler`. - - This may be called from any thread. +.. autoclass:: Router + :members: .. currentmodule:: mitogen.master @@ -362,11 +141,6 @@ Router Class ``ALLOCATE_ID`` message that causes the master to emit matching ``ADD_ROUTE`` messages prior to replying. - .. method:: context_by_id (context_id, via_id=None) - - Messy factory/lookup function to find a context by its ID, or construct - it. In future this will be replaced by a much more sensible interface. - .. _context-factories: **Context Factories** @@ -803,53 +577,8 @@ Context Class ============= .. currentmodule:: mitogen.core - -.. class:: Context - - Represent a remote context regardless of connection method. - - **Note:** This is the somewhat limited core version of the Context class - used by child contexts. The master subclass is documented below this one. - - .. method:: send (msg) - - Arrange for `msg` to be delivered to this context. - :attr:`dst_id ` is set to the target context ID. - - :param mitogen.core.Message msg: - The message. - - .. method:: send_async (msg, persist=False) - - Arrange for `msg` to be delivered to this context, with replies - directed to a newly constructed receiver. :attr:`dst_id - ` is set to the target context ID, and :attr:`reply_to - ` is set to the newly constructed receiver's handle. - - :param bool persist: - If :data:`False`, the handler will be unregistered after a single - message has been received. - - :param mitogen.core.Message msg: - The message. - - :returns: - :class:`mitogen.core.Receiver` configured to receive any replies - sent to the message's `reply_to` handle. - - .. method:: send_await (msg, deadline=None) - - Like :meth:`send_async`, but expect a single reply (`persist=False`) - delivered within `deadline` seconds. - - :param mitogen.core.Message msg: - The message. - :param float deadline: - If not :data:`None`, seconds before timing out waiting for a reply. - :returns: - The deserialized reply. - :raises mitogen.core.TimeoutError: - No message was received and `deadline` passed. +.. autoclass:: Context + :members: .. currentmodule:: mitogen.parent @@ -857,340 +586,33 @@ Context Class .. autoclass:: CallChain :members: -.. class:: Context - - Extend :class:`mitogen.core.Context` with functionality useful to masters, - and child contexts who later become parents. Currently when this class is - required, the target context's router is upgraded at runtime. - - .. attribute:: default_call_chain - - A :class:`CallChain` instance constructed by default, with pipelining - disabled. :meth:`call`, :meth:`call_async` and :meth:`call_no_reply` - use this instance. - - .. method:: shutdown (wait=False) - - Arrange for the context to receive a ``SHUTDOWN`` message, triggering - graceful shutdown. - - Due to a lack of support for timers, no attempt is made yet to force - terminate a hung context using this method. This will be fixed shortly. - - :param bool wait: - If :data:`True`, block the calling thread until the context has - completely terminated. - :returns: - If `wait` is :data:`False`, returns a :class:`mitogen.core.Latch` - whose :meth:`get() ` method returns - :data:`None` when shutdown completes. The `timeout` parameter may - be used to implement graceful timeouts. - - .. method:: call_async (fn, \*args, \*\*kwargs) - - See :meth:`CallChain.call_async`. - - .. method:: call (fn, \*args, \*\*kwargs) - - See :meth:`CallChain.call`. - - .. method:: call_no_reply (fn, \*args, \*\*kwargs) - - See :meth:`CallChain.call_no_reply`. +.. autoclass:: Context + :members: Receiver Class ============== .. currentmodule:: mitogen.core - -.. class:: Receiver (router, handle=None, persist=True, respondent=None) - - Receivers are used to wait for pickled responses from another context to be - sent to a handle registered in this context. A receiver may be single-use - (as in the case of :meth:`mitogen.parent.Context.call_async`) or - multiple use. - - :param mitogen.core.Router router: - Router to register the handler on. - - :param int handle: - If not :data:`None`, an explicit handle to register, otherwise an - unused handle is chosen. - - :param bool persist: - If :data:`True`, do not unregister the receiver's handler after the - first message. - - :param mitogen.core.Context respondent: - Reference to the context this receiver is receiving from. If not - :data:`None`, arranges for the receiver to receive a dead message if - messages can no longer be routed to the context, due to disconnection - or exit. - - .. attribute:: notify = None - - If not :data:`None`, a reference to a function invoked as - `notify(receiver)` when a new message is delivered to this receiver. - Used by :class:`mitogen.select.Select` to implement waiting on - multiple receivers. - - .. py:method:: to_sender () - - Return a :class:`mitogen.core.Sender` configured to deliver messages - to this receiver. Since a Sender can be serialized, this makes it - convenient to pass `(context_id, handle)` pairs around:: - - def deliver_monthly_report(sender): - for line in open('monthly_report.txt'): - sender.send(line) - sender.close() - - remote = router.ssh(hostname='mainframe') - recv = mitogen.core.Receiver(router) - remote.call(deliver_monthly_report, recv.to_sender()) - for msg in recv: - print(msg) - - .. py:method:: empty () - - Return :data:`True` if calling :meth:`get` would block. - - As with :class:`Queue.Queue`, :data:`True` may be returned even - though a subsequent call to :meth:`get` will succeed, since a - message may be posted at any moment between :meth:`empty` and - :meth:`get`. - - :meth:`empty` is only useful to avoid a race while installing - :attr:`notify`: - - .. code-block:: python - - recv.notify = _my_notify_function - if not recv.empty(): - _my_notify_function(recv) - - # It is guaranteed the receiver was empty after the notification - # function was installed, or that it was non-empty and the - # notification function was invoked at least once. - - .. py:method:: close () - - Cause :class:`mitogen.core.ChannelError` to be raised in any thread - waiting in :meth:`get` on this receiver. - - .. py:method:: get (timeout=None) - - Sleep waiting for a message to arrive on this receiver. - - :param float timeout: - If not :data:`None`, specifies a timeout in seconds. - - :raises mitogen.core.ChannelError: - The remote end indicated the channel should be closed, or - communication with its parent context was lost. - - :raises mitogen.core.TimeoutError: - Timeout was reached. - - :returns: - `(msg, data)` tuple, where `msg` is the - :class:`mitogen.core.Message` that was received, and `data` is - its unpickled data part. - - .. py:method:: get_data (timeout=None) - - Like :meth:`get`, except only return the data part. - - .. py:method:: __iter__ () - - Block and yield `(msg, data)` pairs delivered to this receiver until - :class:`mitogen.core.ChannelError` is raised. +.. autoclass:: Receiver + :members: Sender Class ============ .. currentmodule:: mitogen.core - -.. class:: Sender (context, dst_handle) - - Senders are used to send pickled messages to a handle in another context, - it is the inverse of :class:`mitogen.core.Sender`. - - Senders may be serialized, making them convenient to wire up data flows. - See :meth:`mitogen.core.Receiver.to_sender` for more information. - - :param mitogen.core.Context context: - Context to send messages to. - :param int dst_handle: - Destination handle to send messages to. - - .. py:method:: close () - - Send a dead message to the remote end, causing :meth:`ChannelError` - to be raised in any waiting thread. - - .. py:method:: send (data) - - Send `data` to the remote end. +.. autoclass:: Sender + :members: Select Class ============ .. module:: mitogen.select - .. currentmodule:: mitogen.select - -.. class:: Select (receivers=(), oneshot=True) - - Support scatter/gather asynchronous calls and waiting on multiple - receivers, channels, and sub-Selects. Accepts a sequence of - :class:`mitogen.core.Receiver` or :class:`mitogen.select.Select` - instances and returns the first value posted to any receiver or select. - - If `oneshot` is :data:`True`, then remove each receiver as it yields a - result; since :meth:`__iter__` terminates once the final receiver is - removed, this makes it convenient to respond to calls made in parallel: - - .. code-block:: python - - total = 0 - recvs = [c.call_async(long_running_operation) for c in contexts] - - for msg in mitogen.select.Select(recvs): - print('Got %s from %s' % (msg, msg.receiver)) - total += msg.unpickle() - - # Iteration ends when last Receiver yields a result. - print('Received total %s from %s receivers' % (total, len(recvs))) - - :class:`Select` may drive a long-running scheduler: - - .. code-block:: python - - with mitogen.select.Select(oneshot=False) as select: - while running(): - for msg in select: - process_result(msg.receiver.context, msg.unpickle()) - for context, workfunc in get_new_work(): - select.add(context.call_async(workfunc)) - - :class:`Select` may be nested: - - .. code-block:: python - - subselects = [ - mitogen.select.Select(get_some_work()), - mitogen.select.Select(get_some_work()), - mitogen.select.Select([ - mitogen.select.Select(get_some_work()), - mitogen.select.Select(get_some_work()) - ]) - ] - - for msg in mitogen.select.Select(selects): - print(msg.unpickle()) - - .. py:classmethod:: all (it) - - Take an iterable of receivers and retrieve a :class:`Message` from - each, returning the result of calling `msg.unpickle()` on each in turn. - Results are returned in the order they arrived. - - This is sugar for handling batch - :meth:`Context.call_async ` - invocations: - - .. code-block:: python - - print('Total disk usage: %.02fMiB' % (sum( - mitogen.select.Select.all( - context.call_async(get_disk_usage) - for context in contexts - ) / 1048576.0 - ),)) - - However, unlike in a naive comprehension such as: - - .. code-block:: python - - recvs = [c.call_async(get_disk_usage) for c in contexts] - sum(recv.get().unpickle() for recv in recvs) - - Result processing happens in the order results arrive, rather than the - order requests were issued, so :meth:`all` should always be faster. - - .. py:method:: get (timeout=None, block=True) - - Fetch the next available value from any receiver, or raise - :class:`mitogen.core.TimeoutError` if no value is available within - `timeout` seconds. - - On success, the message's :attr:`receiver - ` attribute is set to the receiver. - - :param float timeout: - Timeout in seconds. - :param bool block: - If :data:`False`, immediately raise - :class:`mitogen.core.TimeoutError` if the select is empty. - :return: - :class:`mitogen.core.Message` - :raises mitogen.core.TimeoutError: - Timeout was reached. - :raises mitogen.core.LatchError: - :meth:`close` has been called, and the underlying latch is no - longer valid. - - .. py:method:: __bool__ () - - Return :data:`True` if any receivers are registered with this select. - - .. py:method:: close () - - Remove the select's notifier function from each registered receiver, - mark the associated latch as closed, and cause any thread currently - sleeping in :meth:`get` to be woken with - :class:`mitogen.core.LatchError`. - - This is necessary to prevent memory leaks in long-running receivers. It - is called automatically when the Python :keyword:`with` statement is - used. - - .. py:method:: empty () - - Return :data:`True` if calling :meth:`get` would block. - - As with :class:`Queue.Queue`, :data:`True` may be returned even - though a subsequent call to :meth:`get` will succeed, since a - message may be posted at any moment between :meth:`empty` and - :meth:`get`. - - :meth:`empty` may return :data:`False` even when :meth:`get` - would block if another thread has drained a receiver added to this - select. This can be avoided by only consuming each receiver from a - single thread. - - .. py:method:: __iter__ (self) - - Yield the result of :meth:`get` until no receivers remain in the - select, either because `oneshot` is :data:`True`, or each receiver was - explicitly removed via :meth:`remove`. - - .. py:method:: add (recv) - - Add the :class:`mitogen.core.Receiver` or - :class:`mitogen.core.Channel` `recv` to the select. - - .. py:method:: remove (recv) - - Remove the :class:`mitogen.core.Receiver` or - :class:`mitogen.core.Channel` `recv` from the select. Note that if - the receiver has notified prior to :meth:`remove`, then it will - still be returned by a subsequent :meth:`get`. This may change in a - future version. +.. autoclass:: Select + :members: Channel Class @@ -1327,64 +749,13 @@ Utility Functions A random assortment of utility functions useful on masters and children. .. currentmodule:: mitogen.utils -.. function:: cast (obj) - - Many tools love to subclass built-in types in order to implement useful - functionality, such as annotating the safety of a Unicode string, or adding - additional methods to a dict. However, cPickle loves to preserve those - subtypes during serialization, resulting in CallError during :meth:`call - ` in the target when it tries to deserialize - the data. - - This function walks the object graph `obj`, producing a copy with any - custom sub-types removed. The functionality is not default since the - resulting walk may be computationally expensive given a large enough graph. - - See :ref:`serialization-rules` for a list of supported types. - - :param obj: - Object to undecorate. - :returns: - Undecorated object. - -.. currentmodule:: mitogen.utils -.. function:: disable_site_packages +.. autofunction:: cast - Remove all entries mentioning ``site-packages`` or ``Extras`` from the - system path. Used primarily for testing on OS X within a virtualenv, where - OS X bundles some ancient version of the :mod:`six` module. .. currentmodule:: mitogen.utils -.. function:: log_to_file (path=None, io=False, level='INFO') - - Install a new :class:`logging.Handler` writing applications logs to the - filesystem. Useful when debugging slave IO problems. - - Parameters to this function may be overridden at runtime using environment - variables. See :ref:`logging-env-vars`. - - :param str path: - If not :data:`None`, a filesystem path to write logs to. Otherwise, - logs are written to :data:`sys.stderr`. - - :param bool io: - If :data:`True`, include extremely verbose IO logs in the output. - Useful for debugging hangs, less useful for debugging application code. - - :param str level: - Name of the :mod:`logging` package constant that is the minimum - level to log at. Useful levels are ``DEBUG``, ``INFO``, ``WARNING``, - and ``ERROR``. - -.. currentmodule:: mitogen.utils -.. function:: run_with_router(func, \*args, \**kwargs) - - Arrange for `func(router, \*args, \**kwargs)` to run with a temporary - :class:`mitogen.master.Router`, ensuring the Router and Broker are - correctly shut down during normal or exceptional return. - - :returns: - `func`'s return value. +.. autofunction:: disable_site_packages +.. autofunction:: log_to_file +.. autofunction:: run_with_router(func, \*args, \**kwargs) .. currentmodule:: mitogen.utils .. decorator:: with_router diff --git a/mitogen/core.py b/mitogen/core.py index d3909773..f2dece48 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -500,18 +500,53 @@ else: class Message(object): + """ + Messages are the fundamental unit of communication, comprising fields from + the :ref:`stream-protocol` header, an optional reference to the receiving + :class:`mitogen.core.Router` for ingress messages, and helper methods for + deserialization and generating replies. + """ + #: Integer target context ID. :class:`Router` delivers messages locally + #: when their :attr:`dst_id` matches :data:`mitogen.context_id`, otherwise + #: they are routed up or downstream. dst_id = None + + #: Integer source context ID. Used as the target of replies if any are + #: generated. src_id = None + + #: Context ID under whose authority the message is acting. See + #: :ref:`source-verification`. auth_id = None + + #: Integer target handle in the destination context. This is one of the + #: :ref:`standard-handles`, or a dynamically generated handle used to + #: receive a one-time reply, such as the return value of a function call. handle = None + + #: Integer target handle to direct any reply to this message. Used to + #: receive a one-time reply, such as the return value of a function call. + #: :data:`IS_DEAD` has a special meaning when it appears in this field. reply_to = None + + #: Raw message data bytes. data = b('') + _unpickled = object() + #: The :class:`Router` responsible for routing the message. This is + #: :data:`None` for locally originated messages. router = None + + #: The :class:`Receiver` over which the message was last received. Part of + #: the :class:`mitogen.select.Select` interface. Defaults to :data:`None`. receiver = None def __init__(self, **kwargs): + """ + Construct a message from from the supplied `kwargs`. :attr:`src_id` and + :attr:`auth_id` are always set to :data:`mitogen.context_id`. + """ self.src_id = mitogen.context_id self.auth_id = mitogen.context_id vars(self).update(kwargs) @@ -551,14 +586,28 @@ class Message(object): @property def is_dead(self): + """ + :data:`True` if :attr:`reply_to` is set to the magic value + :data:`IS_DEAD`, indicating the sender considers the channel dead. + """ return self.reply_to == IS_DEAD @classmethod def dead(cls, **kwargs): + """ + Syntax helper to construct a dead message. + """ return cls(reply_to=IS_DEAD, **kwargs) @classmethod def pickled(cls, obj, **kwargs): + """ + Construct a pickled message, setting :attr:`data` to the serialization + of `obj`, and setting remaining fields using `kwargs`. + + :returns: + The new message. + """ self = cls(**kwargs) try: self.data = pickle.dumps(obj, protocol=2) @@ -568,6 +617,18 @@ class Message(object): return self def reply(self, msg, router=None, **kwargs): + """ + Compose a reply to this message and send it using :attr:`router`, or + `router` is :attr:`router` is :data:`None`. + + :param obj: + Either a :class:`Message`, or an object to be serialized in order + to construct a new message. + :param router: + Optional router to use if :attr:`router` is :data:`None`. + :param kwargs: + Optional keyword parameters overriding message fields in the reply. + """ if not isinstance(msg, Message): msg = Message.pickled(msg) msg.dst_id = self.src_id @@ -584,7 +645,18 @@ class Message(object): UNPICKLER_KWARGS = {} def unpickle(self, throw=True, throw_dead=True): - """Deserialize `data` into an object.""" + """ + Unpickle :attr:`data`, optionally raising any exceptions present. + + :param bool throw_dead: + If :data:`True`, raise exceptions, otherwise it is the caller's + responsibility. + + :raises CallError: + The serialized data contained CallError exception. + :raises ChannelError: + The `is_dead` field was set. + """ _vv and IOLOG.debug('%r.unpickle()', self) if throw_dead and self.is_dead: raise ChannelError(ChannelError.remote_msg) @@ -616,25 +688,42 @@ class Message(object): class Sender(object): + """ + Senders are used to send pickled messages to a handle in another context, + it is the inverse of :class:`mitogen.core.Sender`. + + Senders may be serialized, making them convenient to wire up data flows. + See :meth:`mitogen.core.Receiver.to_sender` for more information. + + :param Context context: + Context to send messages to. + :param int dst_handle: + Destination handle to send messages to. + """ def __init__(self, context, dst_handle): self.context = context self.dst_handle = dst_handle - def __repr__(self): - return 'Sender(%r, %r)' % (self.context, self.dst_handle) - - def __reduce__(self): - return _unpickle_sender, (self.context.context_id, self.dst_handle) + def send(self, data): + """ + Send `data` to the remote end. + """ + _vv and IOLOG.debug('%r.send(%r..)', self, repr(data)[:100]) + self.context.send(Message.pickled(data, handle=self.dst_handle)) def close(self): - """Indicate this channel is closed to the remote side.""" + """ + Send a dead message to the remote, causing :meth:`ChannelError` to be + raised in any waiting thread. + """ _vv and IOLOG.debug('%r.close()', self) self.context.send(Message.dead(handle=self.dst_handle)) - def send(self, data): - """Send `data` to the remote.""" - _vv and IOLOG.debug('%r.send(%r..)', self, repr(data)[:100]) - self.context.send(Message.pickled(data, handle=self.dst_handle)) + def __repr__(self): + return 'Sender(%r, %r)' % (self.context, self.dst_handle) + + def __reduce__(self): + return _unpickle_sender, (self.context.context_id, self.dst_handle) def _unpickle_sender(router, context_id, dst_handle): @@ -646,12 +735,39 @@ def _unpickle_sender(router, context_id, dst_handle): class Receiver(object): + """ + Receivers maintain a thread-safe queue of messages sent to a handle of this + context from another context. + + :param mitogen.core.Router router: + Router to register the handler on. + + :param int handle: + If not :data:`None`, an explicit handle to register, otherwise an + unused handle is chosen. + + :param bool persist: + If :data:`False`, unregister the handler after one message is received. + Single-message receivers are intended for RPC-like transactions, such + as in the case of :meth:`mitogen.parent.Context.call_async`. + + :param mitogen.core.Context respondent: + Context this receiver is receiving from. If not :data:`None`, arranges + for the receiver to receive a dead message if messages can no longer be + routed to the context, due to disconnection or exit. + """ + #: If not :data:`None`, a reference to a function invoked as + #: `notify(receiver)` when a new message is delivered to this receiver. + #: Used by :class:`mitogen.select.Select` to implement waiting on multiple + #: receivers. notify = None + raise_channelerror = True def __init__(self, router, handle=None, persist=True, respondent=None, policy=None): self.router = router + #: The handle. self.handle = handle # Avoid __repr__ crash in add_handler() self._latch = Latch() # Must exist prior to .add_handler() self.handle = router.add_handler( @@ -666,26 +782,73 @@ class Receiver(object): return 'Receiver(%r, %r)' % (self.router, self.handle) def to_sender(self): + """ + Return a :class:`Sender` configured to deliver messages to this + receiver. As senders are serializable, this makes it convenient to pass + `(context_id, handle)` pairs around:: + + def deliver_monthly_report(sender): + for line in open('monthly_report.txt'): + sender.send(line) + sender.close() + + remote = router.ssh(hostname='mainframe') + recv = mitogen.core.Receiver(router) + remote.call(deliver_monthly_report, recv.to_sender()) + for msg in recv: + print(msg) + """ context = Context(self.router, mitogen.context_id) return Sender(context, self.handle) def _on_receive(self, msg): - """Callback from the Stream; appends data to the internal queue.""" + """ + Callback from the Stream; appends data to the internal queue. + """ _vv and IOLOG.debug('%r._on_receive(%r)', self, msg) self._latch.put(msg) if self.notify: self.notify(self) def close(self): + """ + Unregister the receiver's handle from its associated router, and cause + :class:`ChannelError` to be raised in any thread waiting in :meth:`get` + on this receiver. + """ if self.handle: self.router.del_handler(self.handle) self.handle = None self._latch.put(Message.dead()) def empty(self): + """ + Return :data:`True` if calling :meth:`get` would block. + + As with :class:`Queue.Queue`, :data:`True` may be returned even though + a subsequent call to :meth:`get` will succeed, since a message may be + posted at any moment between :meth:`empty` and :meth:`get`. + """ return self._latch.empty() def get(self, timeout=None, block=True, throw_dead=True): + """ + Sleep waiting for a message to arrive on this receiver. + + :param float timeout: + If not :data:`None`, specifies a timeout in seconds. + + :raises mitogen.core.ChannelError: + The remote end indicated the channel should be closed, or + communication with its parent context was lost. + + :raises mitogen.core.TimeoutError: + Timeout was reached. + + :returns: + `(msg, data)` tuple, where `msg` is the :class:`Message` that was + received, and `data` is its unpickled data part. + """ _vv and IOLOG.debug('%r.get(timeout=%r, block=%r)', self, timeout, block) msg = self._latch.get(timeout=timeout, block=block) if msg.is_dead and throw_dead: @@ -696,6 +859,10 @@ class Receiver(object): return msg def __iter__(self): + """ + Yield consecutive :class:`Message` instances delivered to this receiver + until :class:`ChannelError` is raised. + """ while True: msg = self.get(throw_dead=False) if msg.is_dead: @@ -1213,6 +1380,28 @@ class Stream(BasicStream): class Context(object): + """ + Represent a remote context regardless of the underlying connection method. + Context objects are simple facades that emit messages through an + associated router, and have :ref:`signals` raised against them in response + to various events relating to the context. + + **Note:** This is the somewhat limited core version, used by child + contexts. The master subclass is documented below this one. + + Contexts maintain no internal state and are thread-safe. + + Prefer :meth:`Router.context_by_id` over constructing context objects + explicitly, as that method is deduplicating, and returns the only context + instance :ref:`signals` will be raised on. + + :param Router router: + Router to emit messages through. + :param int context_id: + Context ID. + :param str name: + Context name. + """ remote_name = None def __init__(self, router, context_id, name=None): @@ -1231,6 +1420,23 @@ class Context(object): fire(self, 'disconnect') def send_async(self, msg, persist=False): + """ + Arrange for `msg` to be delivered to this context, with replies + directed to a newly constructed receiver. :attr:`dst_id + ` is set to the target context ID, and :attr:`reply_to + ` is set to the newly constructed receiver's handle. + + :param bool persist: + If :data:`False`, the handler will be unregistered after a single + message has been received. + + :param mitogen.core.Message msg: + The message. + + :returns: + :class:`Receiver` configured to receive any replies sent to the + message's `reply_to` handle. + """ if self.router.broker._thread == threading.currentThread(): # TODO raise SystemError('Cannot making blocking call on broker thread') @@ -1254,8 +1460,13 @@ class Context(object): return self.send_async(msg) def send(self, msg): - """send `obj` to `handle`, and tell the broker we have output. May - be called from any thread.""" + """ + Arrange for `msg` to be delivered to this context. :attr:`dst_id + ` is set to the target context ID. + + :param Message msg: + Message. + """ msg.dst_id = self.context_id self.router.route(msg) @@ -1264,7 +1475,19 @@ class Context(object): return recv.get().unpickle() def send_await(self, msg, deadline=None): - """Send `msg` and wait for a response with an optional timeout.""" + """ + Like :meth:`send_async`, but expect a single reply (`persist=False`) + delivered within `deadline` seconds. + + :param mitogen.core.Message msg: + The message. + :param float deadline: + If not :data:`None`, seconds before timing out waiting for a reply. + :returns: + Deserialized reply. + :raises TimeoutError: + No message was received and `deadline` passed. + """ receiver = self.send_async(msg) response = receiver.get(deadline) data = response.unpickle() @@ -1710,8 +1933,33 @@ class IoLogger(BasicStream): class Router(object): + """ + Route messages between contexts, and invoke local handlers for messages + addressed to this context. :meth:`Router.route() ` straddles the + :class:`Broker ` and user threads, it is safe to call + anywhere. + + **Note:** This is the somewhat limited core version of the Router class + used by child contexts. The master subclass is documented below this one. + """ context_class = Context max_message_size = 128 * 1048576 + + #: When :data:`True`, permit children to only communicate with the current + #: context or a parent of the current context. Routing between siblings or + #: children of parents is prohibited, ensuring no communication is possible + #: between intentionally partitioned networks, such as when a program + #: simultaneously manipulates hosts spread across a corporate and a + #: production network, or production networks that are otherwise + #: air-gapped. + #: + #: Sending a prohibited message causes an error to be logged and a dead + #: message to be sent in reply to the errant message, if that message has + #: ``reply_to`` set. + #: + #: The value of :data:`unidirectional` becomes the default for the + #: :meth:`local() ` `unidirectional` + #: parameter. unidirectional = False def __init__(self, broker): @@ -1751,7 +1999,7 @@ class Router(object): fire(self._context_by_id[target_id], 'disconnect') - def on_stream_disconnect(self, stream): + def _on_stream_disconnect(self, stream): for context in self._context_by_id.values(): stream_ = self._stream_by_id.get(context.context_id) if stream_ is stream: @@ -1764,6 +2012,10 @@ class Router(object): func(Message.dead()) def context_by_id(self, context_id, via_id=None, create=True, name=None): + """ + Messy factory/lookup function to find a context by its ID, or construct + it. In future this will be replaced by a much more sensible interface. + """ context = self._context_by_id.get(context_id) if create and not context: context = self.context_class(self, context_id, name=name) @@ -1773,21 +2025,85 @@ class Router(object): return context def register(self, context, stream): + """ + Register a newly constructed context and its associated stream, and add + the stream's receive side to the I/O multiplexer. This method remains + public while the design has not yet settled. + """ _v and LOG.debug('register(%r, %r)', context, stream) self._stream_by_id[context.context_id] = stream self._context_by_id[context.context_id] = context self.broker.start_receive(stream) - listen(stream, 'disconnect', lambda: self.on_stream_disconnect(stream)) + listen(stream, 'disconnect', lambda: self._on_stream_disconnect(stream)) def stream_by_id(self, dst_id): + """ + Return the :class:`Stream` that should be used to communicate with + `dst_id`. If a specific route for `dst_id` is not known, a reference to + the parent context's stream is returned. + """ return self._stream_by_id.get(dst_id, self._stream_by_id.get(mitogen.parent_id)) def del_handler(self, handle): + """ + Remove the handle registered for `handle` + + :raises KeyError: + The handle wasn't registered. + """ del self._handle_map[handle] def add_handler(self, fn, handle=None, persist=True, policy=None, respondent=None): + """ + Invoke `fn(msg)` for each Message sent to `handle` from this context. + Unregister after one invocation if `persist` is :data:`False`. If + `handle` is :data:`None`, a new handle is allocated and returned. + + :param int handle: + If not :data:`None`, an explicit handle to register, usually one of + the ``mitogen.core.*`` constants. If unspecified, a new unused + handle will be allocated. + + :param bool persist: + If :data:`False`, the handler will be unregistered after a single + message has been received. + + :param Context respondent: + Context that messages to this handle are expected to be sent from. + If specified, arranges for a dead message to be delivered to `fn` + when disconnection of the context is detected. + + In future `respondent` will likely also be used to prevent other + contexts from sending messages to the handle. + + :param function policy: + Function invoked as `policy(msg, stream)` where `msg` is a + :class:`mitogen.core.Message` about to be delivered, and `stream` + is the :class:`mitogen.core.Stream` on which it was received. The + function must return :data:`True`, otherwise an error is logged and + delivery is refused. + + Two built-in policy functions exist: + + * :func:`has_parent_authority`: requires the message arrived from a + parent context, or a context acting with a parent context's + authority (``auth_id``). + + * :func:`mitogen.parent.is_immediate_child`: requires the + message arrived from an immediately connected child, for use in + messaging patterns where either something becomes buggy or + insecure by permitting indirect upstream communication. + + In case of refusal, and the message's ``reply_to`` field is + nonzero, a :class:`mitogen.core.CallError` is delivered to the + sender indicating refusal occurred. + + :return: + `handle`, or if `handle` was :data:`None`, the newly allocated + handle. + """ handle = handle or next(self._last_handle) _vv and IOLOG.debug('%r.add_handler(%r, %r, %r)', self, fn, handle, persist) @@ -1848,6 +2164,19 @@ class Router(object): msg.reply(Message.dead(), router=self) def _async_route(self, msg, in_stream=None): + """ + Arrange for `msg` to be forwarded towards its destination. If its + destination is the local context, then arrange for it to be dispatched + using the local handlers. + + This is a lower overhead version of :meth:`route` that may only be + called from the I/O multiplexer thread. + + :param Stream in_stream: + If not :data:`None`, the stream the message arrived on. Used for + performing source route verification, to ensure sensitive messages + such as ``CALL_FUNCTION`` arrive only from trusted contexts. + """ _vv and IOLOG.debug('%r._async_route(%r, %r)', self, msg, in_stream) if len(msg.data) > self.max_message_size: @@ -1902,6 +2231,15 @@ class Router(object): out_stream._send(msg) def route(self, msg): + """ + Arrange for the :class:`Message` `msg` to be delivered to its + destination using any relevant downstream context, or if none is found, + by forwarding the message upstream towards the master context. If `msg` + is destined for the local context, it is dispatched using the handles + registered with :meth:`add_handler`. + + This may be called from any thread. + """ self.broker.defer(self._async_route, msg) diff --git a/mitogen/parent.py b/mitogen/parent.py index 78a62380..2f9b2079 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1390,7 +1390,16 @@ class CallChain(object): class Context(mitogen.core.Context): + """ + Extend :class:`mitogen.core.Context` with functionality useful to masters, + and child contexts who later become parents. Currently when this class is + required, the target context's router is upgraded at runtime. + """ + #: A :class:`CallChain` instance constructed by default, with pipelining + #: disabled. :meth:`call`, :meth:`call_async` and :meth:`call_no_reply` use + #: this instance. call_chain_class = CallChain + via = None def __init__(self, *args, **kwargs): @@ -1406,15 +1415,41 @@ class Context(mitogen.core.Context): return hash((self.router, self.context_id)) def call_async(self, fn, *args, **kwargs): + """ + See :meth:`CallChain.call_async`. + """ return self.default_call_chain.call_async(fn, *args, **kwargs) def call(self, fn, *args, **kwargs): + """ + See :meth:`CallChain.call`. + """ return self.default_call_chain.call(fn, *args, **kwargs) def call_no_reply(self, fn, *args, **kwargs): + """ + See :meth:`CallChain.call_no_reply`. + """ self.default_call_chain.call_no_reply(fn, *args, **kwargs) def shutdown(self, wait=False): + """ + Arrange for the context to receive a ``SHUTDOWN`` message, triggering + graceful shutdown. + + Due to a lack of support for timers, no attempt is made yet to force + terminate a hung context using this method. This will be fixed shortly. + + :param bool wait: + If :data:`True`, block the calling thread until the context has + completely terminated. + + :returns: + If `wait` is :data:`False`, returns a :class:`mitogen.core.Latch` + whose :meth:`get() ` method returns + :data:`None` when shutdown completes. The `timeout` parameter may + be used to implement graceful timeouts. + """ LOG.debug('%r.shutdown() sending SHUTDOWN', self) latch = mitogen.core.Latch() mitogen.core.listen(self, 'disconnect', lambda: latch.put(None)) @@ -1666,6 +1701,13 @@ class Router(mitogen.core.Router): msg.reply(None) def add_route(self, target_id, stream): + """ + Arrange for messages whose `dst_id` is `target_id` to be forwarded on + the directly connected stream for `via_id`. This method is called + automatically in response to :data:`mitogen.core.ADD_ROUTE` messages, + but remains public while the design has not yet settled, and situations + may arise where routing is not fully automatic. + """ LOG.debug('%r.add_route(%r, %r)', self, target_id, stream) assert isinstance(target_id, int) assert isinstance(stream, Stream) diff --git a/mitogen/select.py b/mitogen/select.py index ce4023a9..6d46c336 100644 --- a/mitogen/select.py +++ b/mitogen/select.py @@ -34,11 +34,57 @@ class Error(mitogen.core.Error): class Select(object): - notify = None + """ + Support scatter/gather asynchronous calls and waiting on multiple + receivers, channels, and sub-Selects. Accepts a sequence of + :class:`mitogen.core.Receiver` or :class:`mitogen.select.Select` instances + and returns the first value posted to any receiver or select. - @classmethod - def all(cls, receivers): - return list(msg.unpickle() for msg in cls(receivers)) + If `oneshot` is :data:`True`, then remove each receiver as it yields a + result; since :meth:`__iter__` terminates once the final receiver is + removed, this makes it convenient to respond to calls made in parallel: + + .. code-block:: python + + total = 0 + recvs = [c.call_async(long_running_operation) for c in contexts] + + for msg in mitogen.select.Select(recvs): + print('Got %s from %s' % (msg, msg.receiver)) + total += msg.unpickle() + + # Iteration ends when last Receiver yields a result. + print('Received total %s from %s receivers' % (total, len(recvs))) + + :class:`Select` may drive a long-running scheduler: + + .. code-block:: python + + with mitogen.select.Select(oneshot=False) as select: + while running(): + for msg in select: + process_result(msg.receiver.context, msg.unpickle()) + for context, workfunc in get_new_work(): + select.add(context.call_async(workfunc)) + + :class:`Select` may be nested: + + .. code-block:: python + + subselects = [ + mitogen.select.Select(get_some_work()), + mitogen.select.Select(get_some_work()), + mitogen.select.Select([ + mitogen.select.Select(get_some_work()), + mitogen.select.Select(get_some_work()) + ]) + ] + + for msg in mitogen.select.Select(selects): + print(msg.unpickle()) + """ + + notify = None def __init__(self, receivers=(), oneshot=True): self._receivers = [] @@ -47,12 +93,46 @@ class Select(object): for recv in receivers: self.add(recv) + @classmethod + def all(cls, receivers): + """ + Take an iterable of receivers and retrieve a :class:`Message` from + each, returning the result of calling `msg.unpickle()` on each in turn. + Results are returned in the order they arrived. + + This is sugar for handling batch :meth:`Context.call_async + ` invocations: + + .. code-block:: python + + print('Total disk usage: %.02fMiB' % (sum( + mitogen.select.Select.all( + context.call_async(get_disk_usage) + for context in contexts + ) / 1048576.0 + ),)) + + However, unlike in a naive comprehension such as: + + .. code-block:: python + + recvs = [c.call_async(get_disk_usage) for c in contexts] + sum(recv.get().unpickle() for recv in recvs) + + Result processing happens in the order results arrive, rather than the + order requests were issued, so :meth:`all` should always be faster. + """ + return list(msg.unpickle() for msg in cls(receivers)) + def _put(self, value): self._latch.put(value) if self.notify: self.notify(self) def __bool__(self): + """ + Return :data:`True` if any receivers are registered with this select. + """ return bool(self._receivers) def __enter__(self): @@ -62,6 +142,11 @@ class Select(object): self.close() def __iter__(self): + """ + Yield the result of :meth:`get` until no receivers remain in the + select, either because `oneshot` is :data:`True`, or each receiver was + explicitly removed via :meth:`remove`. + """ while self._receivers: yield self.get() @@ -80,6 +165,14 @@ class Select(object): owned_msg = 'Cannot add: Receiver is already owned by another Select' def add(self, recv): + """ + Add the :class:`mitogen.core.Receiver` or :class:`Select` `recv` to the + select. + + :raises mitogen.select.Error: + An attempt was made to add a :class:`Select` to which this select + is indirectly a member of. + """ if isinstance(recv, Select): recv._check_no_loop(self) @@ -95,6 +188,12 @@ class Select(object): not_present_msg = 'Instance is not a member of this Select' def remove(self, recv): + """ + Remove the :class:`mitogen.core.Receiver` or :class:`Select` `recv` + from the select. Note that if the receiver has notified prior to + :meth:`remove`, it will still be returned by a subsequent :meth:`get`. + This may change in a future version. + """ try: if recv.notify != self._put: raise ValueError @@ -104,16 +203,59 @@ class Select(object): raise Error(self.not_present_msg) def close(self): + """ + Remove the select's notifier function from each registered receiver, + mark the associated latch as closed, and cause any thread currently + sleeping in :meth:`get` to be woken with + :class:`mitogen.core.LatchError`. + + This is necessary to prevent memory leaks in long-running receivers. It + is called automatically when the Python :keyword:`with` statement is + used. + """ for recv in self._receivers[:]: self.remove(recv) self._latch.close() def empty(self): + """ + Return :data:`True` if calling :meth:`get` would block. + + As with :class:`Queue.Queue`, :data:`True` may be returned even though + a subsequent call to :meth:`get` will succeed, since a message may be + posted at any moment between :meth:`empty` and :meth:`get`. + + :meth:`empty` may return :data:`False` even when :meth:`get` would + block if another thread has drained a receiver added to this select. + This can be avoided by only consuming each receiver from a single + thread. + """ return self._latch.empty() empty_msg = 'Cannot get(), Select instance is empty' def get(self, timeout=None, block=True): + """ + Fetch the next available value from any receiver, or raise + :class:`mitogen.core.TimeoutError` if no value is available within + `timeout` seconds. + + On success, the message's :attr:`receiver + ` attribute is set to the receiver. + + :param float timeout: + Timeout in seconds. + :param bool block: + If :data:`False`, immediately raise + :class:`mitogen.core.TimeoutError` if the select is empty. + :return: + :class:`mitogen.core.Message` + :raises mitogen.core.TimeoutError: + Timeout was reached. + :raises mitogen.core.LatchError: + :meth:`close` has been called, and the underlying latch is no + longer valid. + """ if not self._receivers: raise Error(self.empty_msg) diff --git a/mitogen/utils.py b/mitogen/utils.py index 4fd80aa1..31669ea9 100644 --- a/mitogen/utils.py +++ b/mitogen/utils.py @@ -46,6 +46,11 @@ else: def disable_site_packages(): + """ + Remove all entries mentioning ``site-packages`` or ``Extras`` from + :attr:sys.path. Used primarily for testing on OS X within a virtualenv, + where OS X bundles some ancient version of the :mod:`six` module. + """ for entry in sys.path[:]: if 'site-packages' in entry or 'Extras' in entry: sys.path.remove(entry) @@ -65,6 +70,26 @@ def log_get_formatter(): def log_to_file(path=None, io=False, level='INFO'): + """ + Install a new :class:`logging.Handler` writing applications logs to the + filesystem. Useful when debugging slave IO problems. + + Parameters to this function may be overridden at runtime using environment + variables. See :ref:`logging-env-vars`. + + :param str path: + If not :data:`None`, a filesystem path to write logs to. Otherwise, + logs are written to :data:`sys.stderr`. + + :param bool io: + If :data:`True`, include extremely verbose IO logs in the output. + Useful for debugging hangs, less useful for debugging application code. + + :param str level: + Name of the :mod:`logging` package constant that is the minimum level + to log at. Useful levels are ``DEBUG``, ``INFO``, ``WARNING``, and + ``ERROR``. + """ log = logging.getLogger('') if path: fp = open(path, 'w', 1) @@ -94,6 +119,14 @@ def log_to_file(path=None, io=False, level='INFO'): def run_with_router(func, *args, **kwargs): + """ + Arrange for `func(router, \*args, \**kwargs)` to run with a temporary + :class:`mitogen.master.Router`, ensuring the Router and Broker are + correctly shut down during normal or exceptional return. + + :returns: + `func`'s return value. + """ broker = mitogen.master.Broker() router = mitogen.master.Router(broker) try: @@ -104,6 +137,17 @@ def run_with_router(func, *args, **kwargs): def with_router(func): + """ + Decorator version of :func:`run_with_router`. Example: + + .. code-block:: python + + @with_router + def do_stuff(router, arg): + pass + + do_stuff(blah, 123) + """ def wrapper(*args, **kwargs): return run_with_router(func, *args, **kwargs) if mitogen.core.PY3: @@ -122,7 +166,27 @@ PASSTHROUGH = ( mitogen.core.Secret, ) + def cast(obj): + """ + Many tools love to subclass built-in types in order to implement useful + functionality, such as annotating the safety of a Unicode string, or adding + additional methods to a dict. However, cPickle loves to preserve those + subtypes during serialization, resulting in CallError during :meth:`call + ` in the target when it tries to deserialize + the data. + + This function walks the object graph `obj`, producing a copy with any + custom sub-types removed. The functionality is not default since the + resulting walk may be computationally expensive given a large enough graph. + + See :ref:`serialization-rules` for a list of supported types. + + :param obj: + Object to undecorate. + :returns: + Undecorated object. + """ if isinstance(obj, dict): return dict((cast(k), cast(v)) for k, v in iteritems(obj)) if isinstance(obj, (list, tuple)): From c9ecc82f855ea988e4265f526d9990c96efb0413 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 13:33:51 +0000 Subject: [PATCH 095/662] issue #400: add logic to work around AWX callback bug. --- ansible_mitogen/loaders.py | 2 ++ ansible_mitogen/strategy.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/ansible_mitogen/loaders.py b/ansible_mitogen/loaders.py index 08c59278..21a2c145 100644 --- a/ansible_mitogen/loaders.py +++ b/ansible_mitogen/loaders.py @@ -34,6 +34,7 @@ from __future__ import absolute_import try: from ansible.plugins.loader import action_loader + from ansible.plugins.loader import callback_loader from ansible.plugins.loader import connection_loader from ansible.plugins.loader import module_loader from ansible.plugins.loader import module_utils_loader @@ -41,6 +42,7 @@ try: from ansible.plugins.loader import strategy_loader except ImportError: # Ansible <2.4 from ansible.plugins import action_loader + from ansible.plugins import callback_loader from ansible.plugins import connection_loader from ansible.plugins import module_loader from ansible.plugins import module_utils_loader diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py index e105984c..217a6def 100644 --- a/ansible_mitogen/strategy.py +++ b/ansible_mitogen/strategy.py @@ -65,6 +65,29 @@ def wrap_connection_loader__get(name, *args, **kwargs): return connection_loader__get(name, *args, **kwargs) +def patch_awx_callback(): + """ + issue #400: AWX loads a display callback that suffers from thread-safety + issues. Detect the presence of older AWX versions and patch the bug. + """ + cls = ansible_mitogen.loaders.callback_loader.get( + 'awx_display', + class_only=True, + ) + if cls is None: # callback does not exist. + return + + # Callback load will have updated sys.path. Now import its guts. + try: + from awx_display_callback.events import event_context + except ImportError: + return # Newer or ancient AWX. + + # Executing this before starting additional threads avoids race. + with event_context.set_global(): + pass + + class StrategyMixin(object): """ This mix-in enhances any built-in strategy by arranging for various Mitogen @@ -164,6 +187,7 @@ class StrategyMixin(object): ansible_mitogen.process.MuxProcess.start() self._add_connection_plugin_path() self._install_wrappers() + patch_awx_callback() try: return super(StrategyMixin, self).run(iterator, play_context) finally: From f2d288bb1e7eca9f9c99c08651889163214f5228 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 14:23:15 +0000 Subject: [PATCH 096/662] tests: ensure minify() result can be compiled for all of core. --- mitogen/kubectl.py | 1 - mitogen/parent.py | 1 + mitogen/service.py | 2 ++ ...minimize_source_test.py => minify_test.py} | 22 ++++++++++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) rename tests/{minimize_source_test.py => minify_test.py} (73%) diff --git a/mitogen/kubectl.py b/mitogen/kubectl.py index c2be24c1..681da3b1 100644 --- a/mitogen/kubectl.py +++ b/mitogen/kubectl.py @@ -1,4 +1,3 @@ -# coding: utf-8 # Copyright 2018, Yannig Perré # # Redistribution and use in source and binary forms, with or without diff --git a/mitogen/parent.py b/mitogen/parent.py index 2f9b2079..a11b362e 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -618,6 +618,7 @@ class EofError(mitogen.core.StreamError): the child process. """ # inherits from StreamError to maintain compatibility. + pass class Argv(object): diff --git a/mitogen/service.py b/mitogen/service.py index ffb7308e..dc90b67f 100644 --- a/mitogen/service.py +++ b/mitogen/service.py @@ -395,11 +395,13 @@ class Service(object): Called when a message arrives on any of :attr:`select`'s registered receivers. """ + pass def on_shutdown(self): """ Called by Pool.shutdown() once the last worker thread has exitted. """ + pass class Pool(object): diff --git a/tests/minimize_source_test.py b/tests/minify_test.py similarity index 73% rename from tests/minimize_source_test.py rename to tests/minify_test.py index b98cdebd..2bb335da 100644 --- a/tests/minimize_source_test.py +++ b/tests/minify_test.py @@ -1,3 +1,5 @@ +import glob + import unittest2 import mitogen.minify @@ -12,7 +14,7 @@ def read_sample(fname): return sample -class MinimizeSource(unittest2.TestCase): +class MinimizeSourceTest(unittest2.TestCase): func = staticmethod(mitogen.minify.minimize_source) def test_class(self): @@ -51,5 +53,23 @@ class MinimizeSource(unittest2.TestCase): self.assertEqual(expected, self.func(original)) +class MitogenCoreTest(unittest2.TestCase): + # Verify minimize_source() succeeds for all built-in modules. + func = staticmethod(mitogen.minify.minimize_source) + + def read_source(self, name): + fp = open(name) + try: + return fp.read() + finally: + fp.close() + + def test_minify_all(self): + for name in glob.glob('mitogen/*.py'): + original = self.read_source(name) + minified = self.func(original) + compile(minified, name, 'exec') + + if __name__ == '__main__': unittest2.main() From aeec2b5054c9e12f8c6247e48dc4381cd17ec3de Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 14:33:19 +0000 Subject: [PATCH 097/662] tests: pad out minify_test to verify all internal modules --- tests/minify_test.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/minify_test.py b/tests/minify_test.py index 2bb335da..98307059 100644 --- a/tests/minify_test.py +++ b/tests/minify_test.py @@ -1,4 +1,6 @@ +import codecs import glob +import pprint import unittest2 @@ -58,17 +60,46 @@ class MitogenCoreTest(unittest2.TestCase): func = staticmethod(mitogen.minify.minimize_source) def read_source(self, name): - fp = open(name) + fp = codecs.open(name, encoding='utf-8') try: return fp.read() finally: fp.close() + def _test_syntax_valid(self, minified, name): + compile(minified, name, 'exec') + + def _test_line_counts_match(self, original, minified): + self.assertEquals(original.count('\n'), + minified.count('\n')) + + def _test_non_blank_lines_match(self, name, original, minified): + # Verify first token matches. We just want to ensure line numbers make + # sense, this is good enough. + olines = original.splitlines() + mlines = minified.splitlines() + for i, (orig, mini) in enumerate(zip(olines, mlines)): + if i < 2: + assert orig == mini + continue + + owords = orig.split() + mwords = mini.split() + assert len(mwords) == 0 or (mwords[0] == owords[0]), pprint.pformat({ + 'line': i+1, + 'filename': name, + 'owords': owords, + 'mwords': mwords, + }) + def test_minify_all(self): for name in glob.glob('mitogen/*.py'): original = self.read_source(name) minified = self.func(original) - compile(minified, name, 'exec') + + self._test_syntax_valid(minified, name) + self._test_line_counts_match(original, minified) + self._test_non_blank_lines_match(name, original, minified) if __name__ == '__main__': From 40d11b32b04acae35ef4e0c954d45ac2115ce5d2 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 15:06:18 +0000 Subject: [PATCH 098/662] issue #400: update Changelog. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7c7609de..130063fe 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,6 +50,9 @@ Fixes now print a useful hint when Python fails to start, as no useful error is normally logged to the console by these tools. +* `#400 `_: work around a threading + bug in the AWX display callback when running with high verbosity setting. + * `#409 `_: the setns method was silently broken due to missing tests. Basic coverage was added to prevent a recurrence. @@ -100,6 +103,7 @@ Mitogen would not be possible without the support of users. A huge thanks for bug reports, features and fixes in this release contributed by `Brian Candler `_, `Guy Knights `_, +`Jiří Vávra `_, `Jonathan Rosser `_, and `Mehdi `_. From d9b268625989709ca96ed9067877b42e4dbe4276 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 15:14:25 +0000 Subject: [PATCH 099/662] docs: update Changelog --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 130063fe..33c4489a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,12 @@ Mitogen for Ansible Enhancements ^^^^^^^^^^^^ +* `#76 `_: disconnect propagation has + improved, allowing Ansible to cancel waits for responses from targets that + where abruptly disconnected. This increases the chance a task will fail + gracefully, rather than hanging due to the connection being severed, for + example because of network failure or EC2 instance maintenance. + * `#369 `_: :meth:`Connection.reset` is implemented, allowing `meta: reset_connection `_ to shut From 58c0e45661f5edc9d9e07614394e719d9663dd50 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 15:40:20 +0000 Subject: [PATCH 100/662] issue #400: rework the monkeypatch. --- ansible_mitogen/loaders.py | 2 -- ansible_mitogen/strategy.py | 53 ++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/ansible_mitogen/loaders.py b/ansible_mitogen/loaders.py index 21a2c145..08c59278 100644 --- a/ansible_mitogen/loaders.py +++ b/ansible_mitogen/loaders.py @@ -34,7 +34,6 @@ from __future__ import absolute_import try: from ansible.plugins.loader import action_loader - from ansible.plugins.loader import callback_loader from ansible.plugins.loader import connection_loader from ansible.plugins.loader import module_loader from ansible.plugins.loader import module_utils_loader @@ -42,7 +41,6 @@ try: from ansible.plugins.loader import strategy_loader except ImportError: # Ansible <2.4 from ansible.plugins import action_loader - from ansible.plugins import callback_loader from ansible.plugins import connection_loader from ansible.plugins import module_loader from ansible.plugins import module_utils_loader diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py index 217a6def..3cdf85f9 100644 --- a/ansible_mitogen/strategy.py +++ b/ansible_mitogen/strategy.py @@ -28,12 +28,41 @@ from __future__ import absolute_import import os +import threading import ansible_mitogen.loaders import ansible_mitogen.mixins import ansible_mitogen.process +def _patch_awx_callback(): + """ + issue #400: AWX loads a display callback that suffers from thread-safety + issues. Detect the presence of older AWX versions and patch the bug. + """ + # AWX uses sitecustomize.py to force-load this package. If it exists, we're + # running under AWX. + try: + from awx_display_callback.events import EventContext + from awx_display_callback.events import event_context + except ImportError: + return + + if hasattr(EventContext(), '_local'): + # Patched version. + return + + def patch_add_local(self, **kwargs): + tls = vars(self._local) + ctx = tls.setdefault('_ctx', {}) + ctx.update(kwargs) + + EventContext._local = threading.local() + EventContext.add_local = patch_add_local + +_patch_awx_callback() + + def wrap_action_loader__get(name, *args, **kwargs): """ While the mitogen strategy is active, trap action_loader.get() calls, @@ -65,29 +94,6 @@ def wrap_connection_loader__get(name, *args, **kwargs): return connection_loader__get(name, *args, **kwargs) -def patch_awx_callback(): - """ - issue #400: AWX loads a display callback that suffers from thread-safety - issues. Detect the presence of older AWX versions and patch the bug. - """ - cls = ansible_mitogen.loaders.callback_loader.get( - 'awx_display', - class_only=True, - ) - if cls is None: # callback does not exist. - return - - # Callback load will have updated sys.path. Now import its guts. - try: - from awx_display_callback.events import event_context - except ImportError: - return # Newer or ancient AWX. - - # Executing this before starting additional threads avoids race. - with event_context.set_global(): - pass - - class StrategyMixin(object): """ This mix-in enhances any built-in strategy by arranging for various Mitogen @@ -187,7 +193,6 @@ class StrategyMixin(object): ansible_mitogen.process.MuxProcess.start() self._add_connection_plugin_path() self._install_wrappers() - patch_awx_callback() try: return super(StrategyMixin, self).run(iterator, play_context) finally: From c148c869e63054af28ef75eaef5a5f308e6efab1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:04:18 +0000 Subject: [PATCH 101/662] issue #76, #370: add disconnect cleanup test --- ansible_mitogen/services.py | 17 +++++++ .../integration/context_service/all.yml | 1 + .../context_service/disconnect_cleanup.yml | 46 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 tests/ansible/integration/context_service/disconnect_cleanup.yml diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index 0ce87e09..a5f31b1c 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -268,6 +268,23 @@ class ContextService(mitogen.service.Service): finally: self._lock.release() + @mitogen.service.expose(mitogen.service.AllowParents()) + def dump(self): + """ + For testing, return a list of dicts describing every currently + connected context. + """ + return [ + { + 'context_name': context.name, + 'via': getattr(self._via_by_context.get(context), + 'name', None), + 'refs': self._refs_by_context.get(context), + } + for context, key in sorted(self._key_by_context.items(), + key=lambda c_k: c_k[0].context_id) + ] + @mitogen.service.expose(mitogen.service.AllowParents()) def shutdown_all(self): """ diff --git a/tests/ansible/integration/context_service/all.yml b/tests/ansible/integration/context_service/all.yml index e70199f8..c10d67cb 100644 --- a/tests/ansible/integration/context_service/all.yml +++ b/tests/ansible/integration/context_service/all.yml @@ -1,2 +1,3 @@ +- import_playbook: disconnect_cleanup.yml - import_playbook: lru_one_target.yml - import_playbook: reconnection.yml diff --git a/tests/ansible/integration/context_service/disconnect_cleanup.yml b/tests/ansible/integration/context_service/disconnect_cleanup.yml new file mode 100644 index 00000000..b657a0dc --- /dev/null +++ b/tests/ansible/integration/context_service/disconnect_cleanup.yml @@ -0,0 +1,46 @@ +# issue #76, #370: ensure context state is forgotten on disconnect, including +# state of dependent contexts (e.g. sudo, connection delegation, ..). + +- name: integration/context_service/disconnect_cleanup.yml + hosts: test-targets + any_errors_fatal: true + tasks: + - meta: end_play + when: not is_mitogen + + # Start with a clean slate. + - mitogen_shutdown_all: + + # Connect a few users. + - shell: "true" + become: true + become_user: "mitogen__user{{item}}" + with_items: [1, 2, 3] + + # Verify current state. + - mitogen_action_script: + script: | + self._connection._connect() + result['dump'] = self._connection.parent.call_service( + service_name='ansible_mitogen.services.ContextService', + method_name='dump' + ) + register: out + + - assert: + that: out.dump|length == 4 # ssh account + 3 sudo accounts + + - meta: reset_connection + + # Verify current state. + - mitogen_action_script: + script: | + self._connection._connect() + result['dump'] = self._connection.parent.call_service( + service_name='ansible_mitogen.services.ContextService', + method_name='dump' + ) + register: out + + - assert: + that: out.dump|length == 1 # just the ssh account From bac28bc5cab23927d10eb8d6fbbde1ac117899bd Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:06:09 +0000 Subject: [PATCH 102/662] issue #76, #370: add fix for disconnect cleanup test Simply listen to RouteMonitor's Context "disconnect" and forget contexts according to RouteMonitor's rules, rather than duplicate them (and screw it up). --- ansible_mitogen/services.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index a5f31b1c..c97d71bb 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -297,23 +297,19 @@ class ContextService(mitogen.service.Service): finally: self._lock.release() - def _on_stream_disconnect(self, stream): + def _on_context_disconnect(self, context): """ - Respond to Stream disconnection by deleting any record of contexts - reached via that stream. This method runs in the Broker thread and must - not to block. + Respond to Context disconnect event by deleting any record of the no + longer reachable context. This method runs in the Broker thread and + must not to block. """ # TODO: there is a race between creation of a context and disconnection # of its related stream. An error reply should be sent to any message # in _latches_by_key below. self._lock.acquire() try: - routes = self.router.route_monitor.get_routes(stream) - for context in list(self._key_by_context): - if context.context_id in routes: - LOG.info('Dropping %r due to disconnect of %r', - context, stream) - self._forget_context_unlocked(context) + LOG.info('Forgetting %r due to stream disconnect', context) + self._forget_context_unlocked(context) finally: self._lock.release() @@ -379,13 +375,10 @@ class ContextService(mitogen.service.Service): context = method(via=via, unidirectional=True, **spec['kwargs']) if via and spec.get('enable_lru'): self._update_lru(context, spec, via) - else: - # For directly connected contexts, listen to the associated - # Stream's disconnect event and use it to invalidate dependent - # Contexts. - stream = self.router.stream_by_id(context.context_id) - mitogen.core.listen(stream, 'disconnect', - lambda: self._on_stream_disconnect(stream)) + + # Forget the context when its disconnect event fires. + mitogen.core.listen(context, 'disconnect', + lambda: self._on_context_disconnect(context)) self._send_module_forwards(context) init_child_result = context.call( From 5be9a55bf4ba4f51e20ef22d9d04e6bd425b3920 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:15:16 +0000 Subject: [PATCH 103/662] core: allow Context to be pickled by non-Mitogen pickler. --- mitogen/core.py | 18 ++++++++++-------- tests/serialization_test.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index f2dece48..faa2d516 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -553,7 +553,7 @@ class Message(object): assert isinstance(self.data, BytesType) def _unpickle_context(self, context_id, name): - return _unpickle_context(self.router, context_id, name) + return _unpickle_context(context_id, name, router=self.router) def _unpickle_sender(self, context_id, dst_handle): return _unpickle_sender(self.router, context_id, dst_handle) @@ -1498,14 +1498,16 @@ class Context(object): return 'Context(%s, %r)' % (self.context_id, self.name) -def _unpickle_context(router, context_id, name): - if not (isinstance(router, Router) and - isinstance(context_id, (int, long)) and context_id >= 0 and ( - (name is None) or - (isinstance(name, UnicodeType) and len(name) < 100)) - ): +def _unpickle_context(context_id, name, router=None): + if not (isinstance(context_id, (int, long)) and context_id >= 0 and ( + (name is None) or + (isinstance(name, UnicodeType) and len(name) < 100)) + ): raise TypeError('cannot unpickle Context: bad input') - return router.context_by_id(context_id, name=name) + + if isinstance(router, Router): + return router.context_by_id(context_id, name=name) + return Context(None, context_id, name) # For plain Jane pickle. class Poller(object): diff --git a/tests/serialization_test.py b/tests/serialization_test.py index 3b56e10a..f108ff37 100644 --- a/tests/serialization_test.py +++ b/tests/serialization_test.py @@ -6,11 +6,14 @@ except ImportError: from StringIO import StringIO as StringIO from StringIO import StringIO as BytesIO +import pickle import unittest2 import mitogen.core from mitogen.core import b +import testlib + def roundtrip(v): msg = mitogen.core.Message.pickled(v) @@ -33,5 +36,30 @@ class BlobTest(unittest2.TestCase): self.assertEquals(b(''), roundtrip(v)) +class ContextTest(testlib.RouterMixin, unittest2.TestCase): + klass = mitogen.core.Context + + # Ensure Context can be round-tripped by regular pickle in addition to + # Mitogen's hacked pickle. Users may try to call pickle on a Context in + # strange circumstances, and it's often used to glue pieces of an app + # together (e.g. Ansible). + + def test_mitogen_roundtrip(self): + c = self.router.fork() + r = mitogen.core.Receiver(self.router) + r.to_sender().send(c) + c2 = r.get().unpickle() + self.assertEquals(None, c2.router) + self.assertEquals(c.context_id, c2.context_id) + self.assertEquals(c.name, c2.name) + + def test_vanilla_roundtrip(self): + c = self.router.fork() + c2 = pickle.loads(pickle.dumps(c)) + self.assertEquals(None, c2.router) + self.assertEquals(c.context_id, c2.context_id) + self.assertEquals(c.name, c2.name) + + if __name__ == '__main__': unittest2.main() From f3f36d6244521a5f5a73207921cb56f3919fa59b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:20:50 +0000 Subject: [PATCH 104/662] docs: add connection: "smart" to known issues. --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 33c4489a..4fa850bd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -499,6 +499,12 @@ Mitogen for Ansible **Known Issues** +* On OS X when a SSH password is specified and the default connection type of + ``smart`` is used, Ansible may select the Paramiko plug-in rather than + Mitogen. If you specify a password on OS X, ensure ``connection: ssh`` + appears in your playbook, ``ansible.cfg``, or as ``-c ssh`` on the + command-line. + * The ``raw`` action executes as a regular Mitogen connection, which requires Python on the target, precluding its use for installing Python. This will be addressed in a future 0.2 release. For now, simply mix Mitogen and vanilla From 7fd4549ad16f449bbea9dee72581b3dda9d87067 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:25:19 +0000 Subject: [PATCH 105/662] issue #370: update Changelog. --- docs/changelog.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4fa850bd..b7c41808 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -52,6 +52,10 @@ Fixes was invoked using sudo without appropriate flags to cause the ``HOME`` environment variable to be reset to match the target account. +* `#370 `_: the Ansible + `reboot `_ + module is supported. + * `#373 `_: the LXC and LXD methods now print a useful hint when Python fails to start, as no useful error is normally logged to the console by these tools. @@ -110,8 +114,9 @@ bug reports, features and fixes in this release contributed by `Brian Candler `_, `Guy Knights `_, `Jiří Vávra `_, -`Jonathan Rosser `_, and -`Mehdi `_. +`Jonathan Rosser `_, +`Mehdi `_, and +`Mohammed Naser `_. v0.2.3 (2018-10-23) From 59d0f0df653834a0465bfc9e50e784b97814e707 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:31:21 +0000 Subject: [PATCH 106/662] docs: split Known Issues out into a separate heading and update it --- docs/changelog.rst | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b7c41808..e513078f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -502,7 +502,31 @@ Mitogen for Ansible * Built-in file transfer compatible with connection delegation. -**Known Issues** +Core Library +~~~~~~~~~~~~ + +* Synchronous connection establishment via OpenSSH, sudo, su, Docker, LXC and + FreeBSD Jails, local subprocesses and :func:`os.fork`. Parallel connection + setup is possible using multiple threads. Connections may be used from one or + many threads after establishment. + +* UNIX masters and children, with Linux, MacOS, FreeBSD, NetBSD, OpenBSD and + Windows Subsystem for Linux explicitly supported. + +* Automatic tests covering Python 2.6, 2.7 and 3.6 on Linux only. + + +Known Issues +------------ + +Mitogen For Ansible +~~~~~~~~~~~~~~~~~~~ + +* The Ansible 2.7 `reboot + `_ module + may require a ``pre_reboot_delay`` on systemd hosts, as insufficient time + exists for the reboot command's exit status to be reported before necessary + processes are torn down. * On OS X when a SSH password is specified and the default connection type of ``smart`` is used, Ansible may select the Paramiko plug-in rather than @@ -541,8 +565,6 @@ Mitogen for Ansible ``ansible_python_interpreter`` setting, contrary to the Ansible documentation. This will be addressed in a future 0.2 release. -* The Ansible 2.7 ``reboot`` module is not yet supported. - * Performance does not scale linearly with target count. This requires significant additional work, as major bottlenecks exist in the surrounding Ansible code. Performance-related bug reports for any scenario remain @@ -574,19 +596,6 @@ Mitogen for Ansible Core Library ~~~~~~~~~~~~ -* Synchronous connection establishment via OpenSSH, sudo, su, Docker, LXC and - FreeBSD Jails, local subprocesses and :func:`os.fork`. Parallel connection - setup is possible using multiple threads. Connections may be used from one or - many threads after establishment. - -* UNIX masters and children, with Linux, MacOS, FreeBSD, NetBSD, OpenBSD and - Windows Subsystem for Linux explicitly supported. - -* Automatic tests covering Python 2.6, 2.7 and 3.6 on Linux only. - - -**Known Issues** - * Serialization is still based on :mod:`pickle`. While there is high confidence remote code execution is impossible in Mitogen's configuration, an untrusted context may at least trigger disproportionately high memory usage injecting From 1af2d9aef1ecb2ffff2ede9f40ce71713cb78410 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:44:42 +0000 Subject: [PATCH 107/662] docs: move Known Issues to the top. --- docs/changelog.rst | 216 ++++++++++++++++++++++----------------------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e513078f..079d53f9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,114 @@ Release Notes +Known Issues +------------ + +Mitogen For Ansible +~~~~~~~~~~~~~~~~~~~ + +* The Ansible 2.7 `reboot + `_ module + may require a ``pre_reboot_delay`` on systemd hosts, as insufficient time + exists for the reboot command's exit status to be reported before necessary + processes are torn down. + +* On OS X when a SSH password is specified and the default connection type of + ``smart`` is used, Ansible may select the Paramiko plug-in rather than + Mitogen. If you specify a password on OS X, ensure ``connection: ssh`` + appears in your playbook, ``ansible.cfg``, or as ``-c ssh`` on the + command-line. + +* The ``raw`` action executes as a regular Mitogen connection, which requires + Python on the target, precluding its use for installing Python. This will be + addressed in a future 0.2 release. For now, simply mix Mitogen and vanilla + Ansible strategies in your playbook: + + .. code-block:: yaml + + - hosts: web-servers + strategy: linear + tasks: + - name: Install Python if necessary. + raw: test -e /usr/bin/python || apt install -y python-minimal + + - hosts: web-servers + strategy: mitogen_linear + roles: + - nginx + - initech_app + - y2k_fix + +.. * When running with ``-vvv``, log messages will be printed to the console + *after* the Ansible run completes, as connection multiplexer shutdown only + begins after Ansible exits. This is due to a lack of suitable shutdown hook + in Ansible, and is fairly harmless, albeit cosmetically annoying. A future + release may include a solution. + +.. * Configurations will break that rely on the `hashbang argument splitting + behaviour `_ of the + ``ansible_python_interpreter`` setting, contrary to the Ansible + documentation. This will be addressed in a future 0.2 release. + +* Performance does not scale linearly with target count. This requires + significant additional work, as major bottlenecks exist in the surrounding + Ansible code. Performance-related bug reports for any scenario remain + welcome with open arms. + +* Performance on Python 3 is significantly worse than on Python 2. While this + has not yet been investigated, at least some of the regression appears to be + part of the core library, and should therefore be straightforward to fix as + part of 0.2.x. + +* *Module Replacer* style Ansible modules are not supported. + +* Actions are single-threaded for each `(host, user account)` combination, + including actions that execute on the local machine. Playbooks may experience + slowdown compared to vanilla Ansible if they employ long-running + ``local_action`` or ``delegate_to`` tasks delegating many target hosts to a + single machine and user account. + +* Connection Delegation remains in preview and has bugs around how it infers + connections. Connection establishment will remain single-threaded for the 0.2 + series, however connection inference bugs will be addressed in a future 0.2 + release. + +* Connection Delegation does not support automatic tunnelling of SSH-dependent + actions, such as the ``synchronize`` module. This will be addressed in the + 0.3 series. + + +Core Library +~~~~~~~~~~~~ + +* Serialization is still based on :mod:`pickle`. While there is high confidence + remote code execution is impossible in Mitogen's configuration, an untrusted + context may at least trigger disproportionately high memory usage injecting + small messages (*"billion laughs attack"*). Replacement is an important + future priority, but not critical for an initial release. + +* Child processes are not reliably reaped, leading to a pileup of zombie + processes when a program makes many short-lived connections in a single + invocation. This does not impact Mitogen for Ansible, however it limits the + usefulness of the core library. A future 0.2 release will address it. + +* Some races remain around :class:`mitogen.core.Broker ` destruction, + disconnection and corresponding file descriptor closure. These are only + problematic in situations where child process reaping is also problematic. + +* The `fakessh` component does not shut down correctly and requires flow + control added to the design. While minimal fixes are possible, due to the + absence of flow control the original design is functionally incomplete. + +* The multi-threaded :ref:`service` remains in a state of design flux and + should be considered obsolete, despite heavy use in Mitogen for Ansible. A + future replacement may be integrated more tightly with, or entirely replace + the RPC dispatcher on the main thread. + +* Documentation is in a state of disrepair. This will be improved over the 0.2 + series. + + v0.2.4 (2018-??-??) ------------------ @@ -514,111 +622,3 @@ Core Library Windows Subsystem for Linux explicitly supported. * Automatic tests covering Python 2.6, 2.7 and 3.6 on Linux only. - - -Known Issues ------------- - -Mitogen For Ansible -~~~~~~~~~~~~~~~~~~~ - -* The Ansible 2.7 `reboot - `_ module - may require a ``pre_reboot_delay`` on systemd hosts, as insufficient time - exists for the reboot command's exit status to be reported before necessary - processes are torn down. - -* On OS X when a SSH password is specified and the default connection type of - ``smart`` is used, Ansible may select the Paramiko plug-in rather than - Mitogen. If you specify a password on OS X, ensure ``connection: ssh`` - appears in your playbook, ``ansible.cfg``, or as ``-c ssh`` on the - command-line. - -* The ``raw`` action executes as a regular Mitogen connection, which requires - Python on the target, precluding its use for installing Python. This will be - addressed in a future 0.2 release. For now, simply mix Mitogen and vanilla - Ansible strategies in your playbook: - - .. code-block:: yaml - - - hosts: web-servers - strategy: linear - tasks: - - name: Install Python if necessary. - raw: test -e /usr/bin/python || apt install -y python-minimal - - - hosts: web-servers - strategy: mitogen_linear - roles: - - nginx - - initech_app - - y2k_fix - -.. * When running with ``-vvv``, log messages will be printed to the console - *after* the Ansible run completes, as connection multiplexer shutdown only - begins after Ansible exits. This is due to a lack of suitable shutdown hook - in Ansible, and is fairly harmless, albeit cosmetically annoying. A future - release may include a solution. - -.. * Configurations will break that rely on the `hashbang argument splitting - behaviour `_ of the - ``ansible_python_interpreter`` setting, contrary to the Ansible - documentation. This will be addressed in a future 0.2 release. - -* Performance does not scale linearly with target count. This requires - significant additional work, as major bottlenecks exist in the surrounding - Ansible code. Performance-related bug reports for any scenario remain - welcome with open arms. - -* Performance on Python 3 is significantly worse than on Python 2. While this - has not yet been investigated, at least some of the regression appears to be - part of the core library, and should therefore be straightforward to fix as - part of 0.2.x. - -* *Module Replacer* style Ansible modules are not supported. - -* Actions are single-threaded for each `(host, user account)` combination, - including actions that execute on the local machine. Playbooks may experience - slowdown compared to vanilla Ansible if they employ long-running - ``local_action`` or ``delegate_to`` tasks delegating many target hosts to a - single machine and user account. - -* Connection Delegation remains in preview and has bugs around how it infers - connections. Connection establishment will remain single-threaded for the 0.2 - series, however connection inference bugs will be addressed in a future 0.2 - release. - -* Connection Delegation does not support automatic tunnelling of SSH-dependent - actions, such as the ``synchronize`` module. This will be addressed in the - 0.3 series. - - -Core Library -~~~~~~~~~~~~ - -* Serialization is still based on :mod:`pickle`. While there is high confidence - remote code execution is impossible in Mitogen's configuration, an untrusted - context may at least trigger disproportionately high memory usage injecting - small messages (*"billion laughs attack"*). Replacement is an important - future priority, but not critical for an initial release. - -* Child processes are not reliably reaped, leading to a pileup of zombie - processes when a program makes many short-lived connections in a single - invocation. This does not impact Mitogen for Ansible, however it limits the - usefulness of the core library. A future 0.2 release will address it. - -* Some races remain around :class:`mitogen.core.Broker ` destruction, - disconnection and corresponding file descriptor closure. These are only - problematic in situations where child process reaping is also problematic. - -* The `fakessh` component does not shut down correctly and requires flow - control added to the design. While minimal fixes are possible, due to the - absence of flow control the original design is functionally incomplete. - -* The multi-threaded :ref:`service` remains in a state of design flux and - should be considered obsolete, despite heavy use in Mitogen for Ansible. A - future replacement may be integrated more tightly with, or entirely replace - the RPC dispatcher on the main thread. - -* Documentation is in a state of disrepair. This will be improved over the 0.2 - series. From a098943e3c5c8b6f7d72bacba6189a8f3d94e293 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:45:35 +0000 Subject: [PATCH 108/662] docs: update install steps to point directly at Known Issues. --- docs/ansible.rst | 2 +- docs/changelog.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/ansible.rst b/docs/ansible.rst index 0918b2f1..c30fffc2 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -57,7 +57,7 @@ write files. Installation ------------ -1. Thoroughly review :ref:`noteworthy_differences` and :ref:`changelog`. +1. Thoroughly review :ref:`noteworthy_differences` and :ref:`known_issues`. 2. Download and extract |mitogen_url|. 3. Modify ``ansible.cfg``: diff --git a/docs/changelog.rst b/docs/changelog.rst index 079d53f9..632274b6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,8 @@ Release Notes +.. _known_issues: + Known Issues ------------ From 677dbdb0e7e9e3df04cbc774bd11ee8a077094cf Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:55:47 +0000 Subject: [PATCH 109/662] docs: update Changelog; closes #351. --- docs/changelog.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 632274b6..f58b02c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -134,9 +134,10 @@ Mitogen for Ansible Enhancements ^^^^^^^^^^^^ -* `#76 `_: disconnect propagation has - improved, allowing Ansible to cancel waits for responses from targets that - where abruptly disconnected. This increases the chance a task will fail +* `#76 `_, + `#351 `_: disconnect propagation + has improved, allowing Ansible to cancel waits for responses from targets + that where abruptly disconnected. This increases the chance a task will fail gracefully, rather than hanging due to the connection being severed, for example because of network failure or EC2 instance maintenance. @@ -221,6 +222,7 @@ Thanks! Mitogen would not be possible without the support of users. A huge thanks for bug reports, features and fixes in this release contributed by +`Berend De Schouwer `_, `Brian Candler `_, `Guy Knights `_, `Jiří Vávra `_, From f8f2f9d718d250065cff537fca0df1579c2d69c0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 20:58:13 +0000 Subject: [PATCH 110/662] docs: update Changelog; closes #352. --- docs/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f58b02c6..5224f51d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -135,7 +135,8 @@ Enhancements ^^^^^^^^^^^^ * `#76 `_, - `#351 `_: disconnect propagation + `#351 `_, + `#352 `_: disconnect propagation has improved, allowing Ansible to cancel waits for responses from targets that where abruptly disconnected. This increases the chance a task will fail gracefully, rather than hanging due to the connection being severed, for @@ -227,6 +228,7 @@ bug reports, features and fixes in this release contributed by `Guy Knights `_, `Jiří Vávra `_, `Jonathan Rosser `_, +`Josh Smift `_, `Mehdi `_, and `Mohammed Naser `_. From 1f9c412b4bbe1cf5866d2160714b1f82da6b2c37 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Nov 2018 22:08:15 +0000 Subject: [PATCH 111/662] Add cute demo GIF to README.md. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 979afc66..158607fd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # Mitogen + Please see the documentation. + +![](https://i.imgur.com/eBM6LhJ.gif) From 07fefa406756c9834cda844f354119e6ac0699f0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 00:28:12 +0000 Subject: [PATCH 112/662] kubectl: paper over importer issue by removing unicode. --- mitogen/kubectl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitogen/kubectl.py b/mitogen/kubectl.py index 681da3b1..e6758ec4 100644 --- a/mitogen/kubectl.py +++ b/mitogen/kubectl.py @@ -1,4 +1,4 @@ -# Copyright 2018, Yannig Perré +# Copyright 2018, Yannig Perre # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: From 1d32ed3b5a809d7dbac3569b51abfdb9b88f17c8 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 10:04:11 +0000 Subject: [PATCH 113/662] core: avoid shutdown() in IoLogger on WSL; closes #333. --- mitogen/core.py | 5 ++++- mitogen/parent.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index faa2d516..83880621 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -108,6 +108,7 @@ try: except NameError: BaseException = Exception +IS_WSL = 'Microsoft' in os.uname()[2] PY3 = sys.version_info > (3,) if PY3: b = str.encode @@ -1920,7 +1921,9 @@ class IoLogger(BasicStream): def on_shutdown(self, broker): """Shut down the write end of the logging socket.""" _v and LOG.debug('%r.on_shutdown()', self) - self._wsock.shutdown(socket.SHUT_WR) + if not IS_WSL: + # #333: WSL generates invalid readiness indication on shutdown() + self._wsock.shutdown(socket.SHUT_WR) self._wsock.close() self.transmit_side.close() diff --git a/mitogen/parent.py b/mitogen/parent.py index a11b362e..0fffdd67 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -68,7 +68,6 @@ from mitogen.core import LOG from mitogen.core import IOLOG -IS_WSL = 'Microsoft' in os.uname()[2] itervalues = getattr(dict, 'itervalues', dict.values) if mitogen.core.PY3: @@ -178,7 +177,7 @@ def disable_echo(fd): old = termios.tcgetattr(fd) new = cfmakeraw(old) flags = getattr(termios, 'TCSASOFT', 0) - if not IS_WSL: + if not mitogen.core.IS_WSL: # issue #319: Windows Subsystem for Linux as of July 2018 throws EINVAL # if TCSAFLUSH is specified. flags |= termios.TCSAFLUSH From 1f8c09e43f68e38ff90ab22af6fd56987a1a8302 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 10:06:05 +0000 Subject: [PATCH 114/662] issue #333: update Changelog. --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5224f51d..a7109a8d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -154,6 +154,11 @@ Enhancements Fixes ^^^^^ +* `#323 `_, + `#333 `_: work around a Windows + Subsystem for Linux bug that would cause tracebacks to be rendered during + shutdown. + * `#334 `_: the SSH method tilde-expands private key paths using Ansible's logic. Previously Mitogen passed the path unmodified to SSH, which would expand it using @@ -225,6 +230,7 @@ Mitogen would not be possible without the support of users. A huge thanks for bug reports, features and fixes in this release contributed by `Berend De Schouwer `_, `Brian Candler `_, +`Duane Zamrok `_, `Guy Knights `_, `Jiří Vávra `_, `Jonathan Rosser `_, From 711aed7a4c3e49921d5f83f33b9f74f5f4237f46 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 11:02:02 +0000 Subject: [PATCH 115/662] core: split _broker_shutdown() out into its own function. Makes _broker_main() logic much clearer. --- mitogen/core.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 83880621..6cb311d3 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2330,28 +2330,31 @@ class Broker(object): for (side, func) in self.poller.poll(timeout): self._call(side.stream, func) + def _broker_shutdown(self): + for _, (side, _) in self.poller.readers + self.poller.writers: + self._call(side.stream, side.stream.on_shutdown) + + deadline = time.time() + self.shutdown_timeout + while self.keep_alive() and time.time() < deadline: + self._loop_once(max(0, deadline - time.time())) + + if self.keep_alive(): + LOG.error('%r: some streams did not close gracefully. ' + 'The most likely cause for this is one or ' + 'more child processes still connected to ' + 'our stdout/stderr pipes.', self) + + for _, (side, _) in self.poller.readers + self.poller.writers: + LOG.error('_broker_main() force disconnecting %r', side) + side.stream.on_disconnect(self) + def _broker_main(self): try: while self._alive: self._loop_once() fire(self, 'shutdown') - for _, (side, _) in self.poller.readers + self.poller.writers: - self._call(side.stream, side.stream.on_shutdown) - - deadline = time.time() + self.shutdown_timeout - while self.keep_alive() and time.time() < deadline: - self._loop_once(max(0, deadline - time.time())) - - if self.keep_alive(): - LOG.error('%r: some streams did not close gracefully. ' - 'The most likely cause for this is one or ' - 'more child processes still connected to ' - 'our stdout/stderr pipes.', self) - - for _, (side, _) in self.poller.readers + self.poller.writers: - LOG.error('_broker_main() force disconnecting %r', side) - side.stream.on_disconnect(self) + self._broker_shutdown() except Exception: LOG.exception('_broker_main() crashed') From 804bacdadb14d60cf26808a64d0c6a70230ca643 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 12:56:13 +0000 Subject: [PATCH 116/662] docs: move most remaining docstrings back into *.py; closes #388 The remaining ones are decorators which don't seem to have an autodoc equivlent. --- docs/api.rst | 996 +++++++++++++++++++-------------------------- docs/changelog.rst | 2 +- docs/internals.rst | 196 +-------- mitogen/core.py | 254 ++++++++++-- mitogen/master.py | 55 +++ mitogen/parent.py | 14 + 6 files changed, 725 insertions(+), 792 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 844bb900..57b9a655 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -98,479 +98,441 @@ Router Class .. currentmodule:: mitogen.master -.. class:: Router (broker=None) +.. autoclass:: Router (broker=None) + :members: - Extend :class:`mitogen.core.Router` with functionality useful to - masters, and child contexts who later become masters. Currently when this - class is required, the target context's router is upgraded at runtime. - .. note:: +.. _context-factories: + +Connection Methods +================== - You may construct as many routers as desired, and use the same broker - for multiple routers, however usually only one broker and router need - exist. Multiple routers may be useful when dealing with separate trust - domains, for example, manipulating infrastructure belonging to separate - customers or projects. +.. method:: mitogen.parent.Router.fork (on_fork=None, on_start=None, debug=False, profiling=False, via=None) - :param mitogen.master.Broker broker: - :class:`Broker` instance to use. If not specified, a private - :class:`Broker` is created. + Construct a context on the local machine by forking the current + process. The forked child receives a new identity, sets up a new broker + and router, and responds to function calls identically to children + created using other methods. - .. attribute:: profiling + For long-lived processes, :meth:`local` is always better as it + guarantees a pristine interpreter state that inherited little from the + parent. Forking should only be used in performance-sensitive scenarios + where short-lived children must be spawned to isolate potentially buggy + code, and only after accounting for all the bad things possible as a + result of, at a minimum: - When :data:`True`, cause the broker thread and any subsequent broker - and main threads existing in any child to write - ``/tmp/mitogen.stats...log`` containing a - :mod:`cProfile` dump on graceful exit. Must be set prior to - construction of any :class:`Broker`, e.g. via: + * Files open in the parent remaining open in the child, + causing the lifetime of the underlying object to be extended + indefinitely. - .. code:: + * From the perspective of external components, this is observable + in the form of pipes and sockets that are never closed, which may + break anything relying on closure to signal protocol termination. - mitogen.master.Router.profiling = True + * Descriptors that reference temporary files will not have their disk + space reclaimed until the child exits. - .. method:: enable_debug + * Third party package state, such as urllib3's HTTP connection pool, + attempting to write to file descriptors shared with the parent, + causing random failures in both parent and child. - Cause this context and any descendant child contexts to write debug - logs to /tmp/mitogen..log. + * UNIX signal handlers installed in the parent process remaining active + in the child, despite associated resources, such as service threads, + child processes, resource usage counters or process timers becoming + absent or reset in the child. - .. method:: allocate_id + * Library code that makes assumptions about the process ID remaining + unchanged, for example to implement inter-process locking, or to + generate file names. - Arrange for a unique context ID to be allocated and associated with a - route leading to the active context. In masters, the ID is generated - directly, in children it is forwarded to the master via an - ``ALLOCATE_ID`` message that causes the master to emit matching - ``ADD_ROUTE`` messages prior to replying. + * Anonymous ``MAP_PRIVATE`` memory mappings whose storage requirement + doubles as either parent or child dirties their pages. + + * File-backed memory mappings that cannot have their space freed on + disk due to the mapping living on in the child. + + * Difficult to diagnose memory usage and latency spikes due to object + graphs becoming unreferenced in either parent or child, causing + immediate copy-on-write to large portions of the process heap. + + * Locks held in the parent causing random deadlocks in the child, such + as when another thread emits a log entry via the :mod:`logging` + package concurrent to another thread calling :meth:`fork`. + + * Objects existing in Thread-Local Storage of every non-:meth:`fork` + thread becoming permanently inaccessible, and never having their + object destructors called, including TLS usage by native extension + code, triggering many new variants of all the issues above. - .. _context-factories: + * Pseudo-Random Number Generator state that is easily observable by + network peers to be duplicate, violating requirements of + cryptographic protocols through one-time state reuse. In the worst + case, children continually reuse the same state due to repeatedly + forking from a static parent. - **Context Factories** - - .. method:: fork (on_fork=None, on_start=None, debug=False, profiling=False, via=None) - - Construct a context on the local machine by forking the current - process. The forked child receives a new identity, sets up a new broker - and router, and responds to function calls identically to children - created using other methods. - - For long-lived processes, :meth:`local` is always better as it - guarantees a pristine interpreter state that inherited little from the - parent. Forking should only be used in performance-sensitive scenarios - where short-lived children must be spawned to isolate potentially buggy - code, and only after accounting for all the bad things possible as a - result of, at a minimum: - - * Files open in the parent remaining open in the child, - causing the lifetime of the underlying object to be extended - indefinitely. - - * From the perspective of external components, this is observable - in the form of pipes and sockets that are never closed, which may - break anything relying on closure to signal protocol termination. - - * Descriptors that reference temporary files will not have their disk - space reclaimed until the child exits. - - * Third party package state, such as urllib3's HTTP connection pool, - attempting to write to file descriptors shared with the parent, - causing random failures in both parent and child. - - * UNIX signal handlers installed in the parent process remaining active - in the child, despite associated resources, such as service threads, - child processes, resource usage counters or process timers becoming - absent or reset in the child. - - * Library code that makes assumptions about the process ID remaining - unchanged, for example to implement inter-process locking, or to - generate file names. - - * Anonymous ``MAP_PRIVATE`` memory mappings whose storage requirement - doubles as either parent or child dirties their pages. - - * File-backed memory mappings that cannot have their space freed on - disk due to the mapping living on in the child. - - * Difficult to diagnose memory usage and latency spikes due to object - graphs becoming unreferenced in either parent or child, causing - immediate copy-on-write to large portions of the process heap. - - * Locks held in the parent causing random deadlocks in the child, such - as when another thread emits a log entry via the :mod:`logging` - package concurrent to another thread calling :meth:`fork`. - - * Objects existing in Thread-Local Storage of every non-:meth:`fork` - thread becoming permanently inaccessible, and never having their - object destructors called, including TLS usage by native extension - code, triggering many new variants of all the issues above. - - * Pseudo-Random Number Generator state that is easily observable by - network peers to be duplicate, violating requirements of - cryptographic protocols through one-time state reuse. In the worst - case, children continually reuse the same state due to repeatedly - forking from a static parent. - - :meth:`fork` cleans up Mitogen-internal objects, in addition to - locks held by the :mod:`logging` package, reseeds - :func:`random.random`, and the OpenSSL PRNG via - :func:`ssl.RAND_add`, but only if the :mod:`ssl` module is - already loaded. You must arrange for your program's state, including - any third party packages in use, to be cleaned up by specifying an - `on_fork` function. - - The associated stream implementation is - :class:`mitogen.fork.Stream`. - - :param function on_fork: - Function invoked as `on_fork()` from within the child process. This - permits supplying a program-specific cleanup function to break - locks and close file descriptors belonging to the parent from - within the child. - - :param function on_start: - Invoked as `on_start(econtext)` from within the child process after - it has been set up, but before the function dispatch loop starts. - This permits supplying a custom child main function that inherits - rich data structures that cannot normally be passed via a - serialization. - - :param mitogen.core.Context via: - Same as the `via` parameter for :meth:`local`. - - :param bool debug: - Same as the `debug` parameter for :meth:`local`. - - :param bool profiling: - Same as the `profiling` parameter for :meth:`local`. - - .. method:: local (remote_name=None, python_path=None, debug=False, connect_timeout=None, profiling=False, via=None) - - Construct a context on the local machine as a subprocess of the current - process. The associated stream implementation is - :class:`mitogen.master.Stream`. - - :param str remote_name: - The ``argv[0]`` suffix for the new process. If `remote_name` is - ``test``, the new process ``argv[0]`` will be ``mitogen:test``. - - If unspecified, defaults to ``@:``. - - This variable cannot contain slash characters, as the resulting - ``argv[0]`` must be presented in such a way as to allow Python to - determine its installation prefix. This is required to support - virtualenv. - - :param str|list python_path: - String or list path to the Python interpreter to use for bootstrap. - Defaults to :data:`sys.executable` for local connections, and - ``python`` for remote connections. - - It is possible to pass a list to invoke Python wrapped using - another tool, such as ``["/usr/bin/env", "python"]``. - - :param bool debug: - If :data:`True`, arrange for debug logging (:meth:`enable_debug`) to - be enabled in the new context. Automatically :data:`True` when - :meth:`enable_debug` has been called, but may be used - selectively otherwise. - - :param bool unidirectional: - If :data:`True`, arrange for the child's router to be constructed - with :attr:`unidirectional routing - ` enabled. Automatically - :data:`True` when it was enabled for this router, but may still be - explicitly set to :data:`False`. - - :param float connect_timeout: - Fractional seconds to wait for the subprocess to indicate it is - healthy. Defaults to 30 seconds. - - :param bool profiling: - If :data:`True`, arrange for profiling (:data:`profiling`) to be - enabled in the new context. Automatically :data:`True` when - :data:`profiling` is :data:`True`, but may be used selectively - otherwise. - - :param mitogen.core.Context via: - If not :data:`None`, arrange for construction to occur via RPCs - made to the context `via`, and for :data:`ADD_ROUTE - ` messages to be generated as appropriate. - - .. code-block:: python - - # SSH to the remote machine. - remote_machine = router.ssh(hostname='mybox.com') - - # Use the SSH connection to create a sudo connection. - remote_root = router.sudo(username='root', via=remote_machine) - - .. method:: doas (username=None, password=None, doas_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) - - Construct a context on the local machine over a ``doas`` invocation. - The ``doas`` process is started in a newly allocated pseudo-terminal, - and supports typing interactive passwords. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str username: - Username to use, defaults to ``root``. - :param str password: - The account password to use if requested. - :param str doas_path: - Filename or complete path to the ``doas`` binary. ``PATH`` will be - searched if given as a filename. Defaults to ``doas``. - :param bytes password_prompt: - A string that indicates ``doas`` is requesting a password. Defaults - to ``Password:``. - :param list incorrect_prompts: - List of bytestrings indicating the password is incorrect. Defaults - to `(b"doas: authentication failed")`. - :raises mitogen.doas.PasswordError: - A password was requested but none was provided, the supplied - password was incorrect, or the target account did not exist. - - .. method:: docker (container=None, image=None, docker_path=None, \**kwargs) - - Construct a context on the local machine within an existing or - temporary new Docker container using the ``docker`` program. One of - `container` or `image` must be specified. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str container: - Existing container to connect to. Defaults to :data:`None`. - :param str username: - Username within the container to :func:`setuid` to. Defaults to - :data:`None`, which Docker interprets as ``root``. - :param str image: - Image tag to use to construct a temporary container. Defaults to - :data:`None`. - :param str docker_path: - Filename or complete path to the Docker binary. ``PATH`` will be - searched if given as a filename. Defaults to ``docker``. - - .. method:: jail (container, jexec_path=None, \**kwargs) - - Construct a context on the local machine within a FreeBSD jail using - the ``jexec`` program. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str container: - Existing container to connect to. Defaults to :data:`None`. - :param str username: - Username within the container to :func:`setuid` to. Defaults to - :data:`None`, which ``jexec`` interprets as ``root``. - :param str jexec_path: - Filename or complete path to the ``jexec`` binary. ``PATH`` will be - searched if given as a filename. Defaults to ``/usr/sbin/jexec``. - - .. method:: kubectl (pod, kubectl_path=None, kubectl_args=None, \**kwargs) - - Construct a context in a container via the Kubernetes ``kubectl`` - program. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str pod: - Kubernetes pod to connect to. - :param str kubectl_path: - Filename or complete path to the ``kubectl`` binary. ``PATH`` will - be searched if given as a filename. Defaults to ``kubectl``. - :param list kubectl_args: - Additional arguments to pass to the ``kubectl`` command. - - .. method:: lxc (container, lxc_attach_path=None, \**kwargs) - - Construct a context on the local machine within an LXC classic - container using the ``lxc-attach`` program. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str container: - Existing container to connect to. Defaults to :data:`None`. - :param str lxc_attach_path: - Filename or complete path to the ``lxc-attach`` binary. ``PATH`` - will be searched if given as a filename. Defaults to - ``lxc-attach``. - - .. method:: lxc (container, lxc_attach_path=None, \**kwargs) - - Construct a context on the local machine within a LXD container using - the ``lxc`` program. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str container: - Existing container to connect to. Defaults to :data:`None`. - :param str lxc_path: - Filename or complete path to the ``lxc`` binary. ``PATH`` will be - searched if given as a filename. Defaults to ``lxc``. - - .. method:: setns (container, kind, username=None, docker_path=None, lxc_info_path=None, machinectl_path=None, \**kwargs) - - Construct a context in the style of :meth:`local`, but change the - active Linux process namespaces via calls to `setns(1)` before - executing Python. - - The namespaces to use, and the active root file system are taken from - the root PID of a running Docker, LXC, LXD, or systemd-nspawn - container. - - A program is required only to find the root PID, after which management - of the child Python interpreter is handled directly. - - :param str container: - Container to connect to. - :param str kind: - One of ``docker``, ``lxc``, ``lxd`` or ``machinectl``. - :param str username: - Username within the container to :func:`setuid` to. Defaults to - ``root``. - :param str docker_path: - Filename or complete path to the Docker binary. ``PATH`` will be - searched if given as a filename. Defaults to ``docker``. - :param str lxc_path: - Filename or complete path to the LXD ``lxc`` binary. ``PATH`` will - be searched if given as a filename. Defaults to ``lxc``. - :param str lxc_info_path: - Filename or complete path to the LXC ``lxc-info`` binary. ``PATH`` - will be searched if given as a filename. Defaults to ``lxc-info``. - :param str machinectl_path: - Filename or complete path to the ``machinectl`` binary. ``PATH`` - will be searched if given as a filename. Defaults to - ``machinectl``. - - .. method:: su (username=None, password=None, su_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) - - Construct a context on the local machine over a ``su`` invocation. The - ``su`` process is started in a newly allocated pseudo-terminal, and - supports typing interactive passwords. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str username: - Username to pass to ``su``, defaults to ``root``. - :param str password: - The account password to use if requested. - :param str su_path: - Filename or complete path to the ``su`` binary. ``PATH`` will be - searched if given as a filename. Defaults to ``su``. - :param bytes password_prompt: - The string that indicates ``su`` is requesting a password. Defaults - to ``Password:``. - :param str incorrect_prompts: - Strings that signal the password is incorrect. Defaults to `("su: - sorry", "su: authentication failure")`. - - :raises mitogen.su.PasswordError: - A password was requested but none was provided, the supplied - password was incorrect, or (on BSD) the target account did not - exist. - - .. method:: sudo (username=None, sudo_path=None, password=None, \**kwargs) - - Construct a context on the local machine over a ``sudo`` invocation. - The ``sudo`` process is started in a newly allocated pseudo-terminal, - and supports typing interactive passwords. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str username: - Username to pass to sudo as the ``-u`` parameter, defaults to - ``root``. - :param str sudo_path: - Filename or complete path to the sudo binary. ``PATH`` will be - searched if given as a filename. Defaults to ``sudo``. - :param str password: - The password to use if/when sudo requests it. Depending on the sudo - configuration, this is either the current account password or the - target account password. :class:`mitogen.sudo.PasswordError` - will be raised if sudo requests a password but none is provided. - :param bool set_home: - If :data:`True`, request ``sudo`` set the ``HOME`` environment - variable to match the target UNIX account. - :param bool preserve_env: - If :data:`True`, request ``sudo`` to preserve the environment of - the parent process. - :param str selinux_type: - If not :data:`None`, the SELinux security context to use. - :param str selinux_role: - If not :data:`None`, the SELinux role to use. - :param list sudo_args: - Arguments in the style of :data:`sys.argv` that would normally - be passed to ``sudo``. The arguments are parsed in-process to set - equivalent parameters. Re-parsing ensures unsupported options cause - :class:`mitogen.core.StreamError` to be raised, and that - attributes of the stream match the actual behaviour of ``sudo``. - - .. method:: ssh (hostname, username=None, ssh_path=None, ssh_args=None, port=None, check_host_keys='enforce', password=None, identity_file=None, identities_only=True, compression=True, \**kwargs) - - Construct a remote context over an OpenSSH ``ssh`` invocation. - - The ``ssh`` process is started in a newly allocated pseudo-terminal to - support typing interactive passwords and responding to prompts, if a - password is specified, or `check_host_keys=accept`. In other scenarios, - ``BatchMode`` is enabled and no PTY is allocated. For many-target - configurations, both options should be avoided as most systems have a - conservative limit on the number of pseudo-terminals that may exist. - - Accepts all parameters accepted by :meth:`local`, in addition to: - - :param str username: - The SSH username; default is unspecified, which causes SSH to pick - the username to use. - :param str ssh_path: - Absolute or relative path to ``ssh``. Defaults to ``ssh``. - :param list ssh_args: - Additional arguments to pass to the SSH command. - :param int port: - Port number to connect to; default is unspecified, which causes SSH - to pick the port number. - :param str check_host_keys: - Specifies the SSH host key checking mode. Defaults to ``enforce``. - - * ``ignore``: no host key checking is performed. Connections never - fail due to an unknown or changed host key. - * ``accept``: known hosts keys are checked to ensure they match, - new host keys are automatically accepted and verified in future - connections. - * ``enforce``: known host keys are checked to ensure they match, - unknown hosts cause a connection failure. - :param str password: - Password to type if/when ``ssh`` requests it. If not specified and - a password is requested, :class:`mitogen.ssh.PasswordError` is - raised. - :param str identity_file: - Path to an SSH private key file to use for authentication. Default - is unspecified, which causes SSH to pick the identity file. - - When this option is specified, only `identity_file` will be used by - the SSH client to perform authenticaion; agent authentication is - automatically disabled, as is reading the default private key from - ``~/.ssh/id_rsa``, or ``~/.ssh/id_dsa``. - :param bool identities_only: - If :data:`True` and a password or explicit identity file is - specified, instruct the SSH client to disable any authentication - identities inherited from the surrounding environment, such as - those loaded in any running ``ssh-agent``, or default key files - present in ``~/.ssh``. This ensures authentication attempts only - occur using the supplied password or SSH key. - :param bool compression: - If :data:`True`, enable ``ssh`` compression support. Compression - has a minimal effect on the size of modules transmitted, as they - are already compressed, however it has a large effect on every - remaining message in the otherwise uncompressed stream protocol, - such as function call arguments and return values. - :param int ssh_debug_level: - Optional integer `0..3` indicating the SSH client debug level. - :raises mitogen.ssh.PasswordError: - A password was requested but none was specified, or the specified - password was incorrect. - - :raises mitogen.ssh.HostKeyError: - When `check_host_keys` is set to either ``accept``, indicates a - previously recorded key no longer matches the remote machine. When - set to ``enforce``, as above, but additionally indicates no - previously recorded key exists for the remote machine. + :meth:`fork` cleans up Mitogen-internal objects, in addition to + locks held by the :mod:`logging` package, reseeds + :func:`random.random`, and the OpenSSL PRNG via + :func:`ssl.RAND_add`, but only if the :mod:`ssl` module is + already loaded. You must arrange for your program's state, including + any third party packages in use, to be cleaned up by specifying an + `on_fork` function. + + The associated stream implementation is + :class:`mitogen.fork.Stream`. + + :param function on_fork: + Function invoked as `on_fork()` from within the child process. This + permits supplying a program-specific cleanup function to break + locks and close file descriptors belonging to the parent from + within the child. + + :param function on_start: + Invoked as `on_start(econtext)` from within the child process after + it has been set up, but before the function dispatch loop starts. + This permits supplying a custom child main function that inherits + rich data structures that cannot normally be passed via a + serialization. + + :param mitogen.core.Context via: + Same as the `via` parameter for :meth:`local`. + + :param bool debug: + Same as the `debug` parameter for :meth:`local`. + + :param bool profiling: + Same as the `profiling` parameter for :meth:`local`. + +.. method:: mitogen.parent.Router.local (remote_name=None, python_path=None, debug=False, connect_timeout=None, profiling=False, via=None) + + Construct a context on the local machine as a subprocess of the current + process. The associated stream implementation is + :class:`mitogen.master.Stream`. + + :param str remote_name: + The ``argv[0]`` suffix for the new process. If `remote_name` is + ``test``, the new process ``argv[0]`` will be ``mitogen:test``. + + If unspecified, defaults to ``@:``. + + This variable cannot contain slash characters, as the resulting + ``argv[0]`` must be presented in such a way as to allow Python to + determine its installation prefix. This is required to support + virtualenv. + + :param str|list python_path: + String or list path to the Python interpreter to use for bootstrap. + Defaults to :data:`sys.executable` for local connections, and + ``python`` for remote connections. + + It is possible to pass a list to invoke Python wrapped using + another tool, such as ``["/usr/bin/env", "python"]``. + + :param bool debug: + If :data:`True`, arrange for debug logging (:meth:`enable_debug`) to + be enabled in the new context. Automatically :data:`True` when + :meth:`enable_debug` has been called, but may be used + selectively otherwise. + + :param bool unidirectional: + If :data:`True`, arrange for the child's router to be constructed + with :attr:`unidirectional routing + ` enabled. Automatically + :data:`True` when it was enabled for this router, but may still be + explicitly set to :data:`False`. + + :param float connect_timeout: + Fractional seconds to wait for the subprocess to indicate it is + healthy. Defaults to 30 seconds. + + :param bool profiling: + If :data:`True`, arrange for profiling (:data:`profiling`) to be + enabled in the new context. Automatically :data:`True` when + :data:`profiling` is :data:`True`, but may be used selectively + otherwise. + + :param mitogen.core.Context via: + If not :data:`None`, arrange for construction to occur via RPCs + made to the context `via`, and for :data:`ADD_ROUTE + ` messages to be generated as appropriate. + + .. code-block:: python + + # SSH to the remote machine. + remote_machine = router.ssh(hostname='mybox.com') + + # Use the SSH connection to create a sudo connection. + remote_root = router.sudo(username='root', via=remote_machine) + +.. method:: mitogen.parent.Router.doas (username=None, password=None, doas_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) + + Construct a context on the local machine over a ``doas`` invocation. + The ``doas`` process is started in a newly allocated pseudo-terminal, + and supports typing interactive passwords. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str username: + Username to use, defaults to ``root``. + :param str password: + The account password to use if requested. + :param str doas_path: + Filename or complete path to the ``doas`` binary. ``PATH`` will be + searched if given as a filename. Defaults to ``doas``. + :param bytes password_prompt: + A string that indicates ``doas`` is requesting a password. Defaults + to ``Password:``. + :param list incorrect_prompts: + List of bytestrings indicating the password is incorrect. Defaults + to `(b"doas: authentication failed")`. + :raises mitogen.doas.PasswordError: + A password was requested but none was provided, the supplied + password was incorrect, or the target account did not exist. + +.. method:: mitogen.parent.Router.docker (container=None, image=None, docker_path=None, \**kwargs) + + Construct a context on the local machine within an existing or + temporary new Docker container using the ``docker`` program. One of + `container` or `image` must be specified. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str container: + Existing container to connect to. Defaults to :data:`None`. + :param str username: + Username within the container to :func:`setuid` to. Defaults to + :data:`None`, which Docker interprets as ``root``. + :param str image: + Image tag to use to construct a temporary container. Defaults to + :data:`None`. + :param str docker_path: + Filename or complete path to the Docker binary. ``PATH`` will be + searched if given as a filename. Defaults to ``docker``. + +.. method:: mitogen.parent.Router.jail (container, jexec_path=None, \**kwargs) + + Construct a context on the local machine within a FreeBSD jail using + the ``jexec`` program. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str container: + Existing container to connect to. Defaults to :data:`None`. + :param str username: + Username within the container to :func:`setuid` to. Defaults to + :data:`None`, which ``jexec`` interprets as ``root``. + :param str jexec_path: + Filename or complete path to the ``jexec`` binary. ``PATH`` will be + searched if given as a filename. Defaults to ``/usr/sbin/jexec``. + +.. method:: mitogen.parent.Router.kubectl (pod, kubectl_path=None, kubectl_args=None, \**kwargs) + + Construct a context in a container via the Kubernetes ``kubectl`` + program. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str pod: + Kubernetes pod to connect to. + :param str kubectl_path: + Filename or complete path to the ``kubectl`` binary. ``PATH`` will + be searched if given as a filename. Defaults to ``kubectl``. + :param list kubectl_args: + Additional arguments to pass to the ``kubectl`` command. + +.. method:: mitogen.parent.Router.lxc (container, lxc_attach_path=None, \**kwargs) + + Construct a context on the local machine within an LXC classic + container using the ``lxc-attach`` program. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str container: + Existing container to connect to. Defaults to :data:`None`. + :param str lxc_attach_path: + Filename or complete path to the ``lxc-attach`` binary. ``PATH`` + will be searched if given as a filename. Defaults to + ``lxc-attach``. + +.. method:: mitogen.parent.Router.lxc (container, lxc_attach_path=None, \**kwargs) + + Construct a context on the local machine within a LXD container using + the ``lxc`` program. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str container: + Existing container to connect to. Defaults to :data:`None`. + :param str lxc_path: + Filename or complete path to the ``lxc`` binary. ``PATH`` will be + searched if given as a filename. Defaults to ``lxc``. + +.. method:: mitogen.parent.Router.setns (container, kind, username=None, docker_path=None, lxc_info_path=None, machinectl_path=None, \**kwargs) + + Construct a context in the style of :meth:`local`, but change the + active Linux process namespaces via calls to `setns(1)` before + executing Python. + + The namespaces to use, and the active root file system are taken from + the root PID of a running Docker, LXC, LXD, or systemd-nspawn + container. + + A program is required only to find the root PID, after which management + of the child Python interpreter is handled directly. + + :param str container: + Container to connect to. + :param str kind: + One of ``docker``, ``lxc``, ``lxd`` or ``machinectl``. + :param str username: + Username within the container to :func:`setuid` to. Defaults to + ``root``. + :param str docker_path: + Filename or complete path to the Docker binary. ``PATH`` will be + searched if given as a filename. Defaults to ``docker``. + :param str lxc_path: + Filename or complete path to the LXD ``lxc`` binary. ``PATH`` will + be searched if given as a filename. Defaults to ``lxc``. + :param str lxc_info_path: + Filename or complete path to the LXC ``lxc-info`` binary. ``PATH`` + will be searched if given as a filename. Defaults to ``lxc-info``. + :param str machinectl_path: + Filename or complete path to the ``machinectl`` binary. ``PATH`` + will be searched if given as a filename. Defaults to + ``machinectl``. + +.. method:: mitogen.parent.Router.su (username=None, password=None, su_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) + + Construct a context on the local machine over a ``su`` invocation. The + ``su`` process is started in a newly allocated pseudo-terminal, and + supports typing interactive passwords. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str username: + Username to pass to ``su``, defaults to ``root``. + :param str password: + The account password to use if requested. + :param str su_path: + Filename or complete path to the ``su`` binary. ``PATH`` will be + searched if given as a filename. Defaults to ``su``. + :param bytes password_prompt: + The string that indicates ``su`` is requesting a password. Defaults + to ``Password:``. + :param str incorrect_prompts: + Strings that signal the password is incorrect. Defaults to `("su: + sorry", "su: authentication failure")`. + + :raises mitogen.su.PasswordError: + A password was requested but none was provided, the supplied + password was incorrect, or (on BSD) the target account did not + exist. + +.. method:: mitogen.parent.Router.sudo (username=None, sudo_path=None, password=None, \**kwargs) + + Construct a context on the local machine over a ``sudo`` invocation. + The ``sudo`` process is started in a newly allocated pseudo-terminal, + and supports typing interactive passwords. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str username: + Username to pass to sudo as the ``-u`` parameter, defaults to + ``root``. + :param str sudo_path: + Filename or complete path to the sudo binary. ``PATH`` will be + searched if given as a filename. Defaults to ``sudo``. + :param str password: + The password to use if/when sudo requests it. Depending on the sudo + configuration, this is either the current account password or the + target account password. :class:`mitogen.sudo.PasswordError` + will be raised if sudo requests a password but none is provided. + :param bool set_home: + If :data:`True`, request ``sudo`` set the ``HOME`` environment + variable to match the target UNIX account. + :param bool preserve_env: + If :data:`True`, request ``sudo`` to preserve the environment of + the parent process. + :param str selinux_type: + If not :data:`None`, the SELinux security context to use. + :param str selinux_role: + If not :data:`None`, the SELinux role to use. + :param list sudo_args: + Arguments in the style of :data:`sys.argv` that would normally + be passed to ``sudo``. The arguments are parsed in-process to set + equivalent parameters. Re-parsing ensures unsupported options cause + :class:`mitogen.core.StreamError` to be raised, and that + attributes of the stream match the actual behaviour of ``sudo``. + +.. method:: mitogen.parent.Router.ssh (hostname, username=None, ssh_path=None, ssh_args=None, port=None, check_host_keys='enforce', password=None, identity_file=None, identities_only=True, compression=True, \**kwargs) + + Construct a remote context over an OpenSSH ``ssh`` invocation. + + The ``ssh`` process is started in a newly allocated pseudo-terminal to + support typing interactive passwords and responding to prompts, if a + password is specified, or `check_host_keys=accept`. In other scenarios, + ``BatchMode`` is enabled and no PTY is allocated. For many-target + configurations, both options should be avoided as most systems have a + conservative limit on the number of pseudo-terminals that may exist. + + Accepts all parameters accepted by :meth:`local`, in addition to: + + :param str username: + The SSH username; default is unspecified, which causes SSH to pick + the username to use. + :param str ssh_path: + Absolute or relative path to ``ssh``. Defaults to ``ssh``. + :param list ssh_args: + Additional arguments to pass to the SSH command. + :param int port: + Port number to connect to; default is unspecified, which causes SSH + to pick the port number. + :param str check_host_keys: + Specifies the SSH host key checking mode. Defaults to ``enforce``. + + * ``ignore``: no host key checking is performed. Connections never + fail due to an unknown or changed host key. + * ``accept``: known hosts keys are checked to ensure they match, + new host keys are automatically accepted and verified in future + connections. + * ``enforce``: known host keys are checked to ensure they match, + unknown hosts cause a connection failure. + :param str password: + Password to type if/when ``ssh`` requests it. If not specified and + a password is requested, :class:`mitogen.ssh.PasswordError` is + raised. + :param str identity_file: + Path to an SSH private key file to use for authentication. Default + is unspecified, which causes SSH to pick the identity file. + + When this option is specified, only `identity_file` will be used by + the SSH client to perform authenticaion; agent authentication is + automatically disabled, as is reading the default private key from + ``~/.ssh/id_rsa``, or ``~/.ssh/id_dsa``. + :param bool identities_only: + If :data:`True` and a password or explicit identity file is + specified, instruct the SSH client to disable any authentication + identities inherited from the surrounding environment, such as + those loaded in any running ``ssh-agent``, or default key files + present in ``~/.ssh``. This ensures authentication attempts only + occur using the supplied password or SSH key. + :param bool compression: + If :data:`True`, enable ``ssh`` compression support. Compression + has a minimal effect on the size of modules transmitted, as they + are already compressed, however it has a large effect on every + remaining message in the otherwise uncompressed stream protocol, + such as function call arguments and return values. + :param int ssh_debug_level: + Optional integer `0..3` indicating the SSH client debug level. + :raises mitogen.ssh.PasswordError: + A password was requested but none was specified, or the specified + password was incorrect. + + :raises mitogen.ssh.HostKeyError: + When `check_host_keys` is set to either ``accept``, indicates a + previously recorded key no longer matches the remote machine. When + set to ``enforce``, as above, but additionally indicates no + previously recorded key exists for the remote machine. Context Class @@ -619,126 +581,22 @@ Channel Class ============= .. currentmodule:: mitogen.core +.. autoclass:: Channel + :members: -.. class:: Channel (router, context, dst_handle, handle=None) - - A channel inherits from :class:`mitogen.core.Sender` and - `mitogen.core.Receiver` to provide bidirectional functionality. - - Since all handles aren't known until after both ends are constructed, for - both ends to communicate through a channel, it is necessary for one end to - retrieve the handle allocated to the other and reconfigure its own channel - to match. Currently this is a manual task. Broker Class ============ .. currentmodule:: mitogen.core -.. class:: Broker - - Responsible for handling I/O multiplexing in a private thread. - - **Note:** This is the somewhat limited core version of the Broker class - used by child contexts. The master subclass is documented below. - - .. attribute:: shutdown_timeout = 3.0 - - Seconds grace to allow :class:`streams ` to shutdown - gracefully before force-disconnecting them during :meth:`shutdown`. - - .. method:: defer (func, \*args, \*kwargs) - - Arrange for `func(\*args, \**kwargs)` to be executed on the broker - thread, or immediately if the current thread is the broker thread. Safe - to call from any thread. - - .. method:: defer_sync (func) - - Arrange for `func()` to execute on the broker thread, blocking the - current thread until a result or exception is available. - - :returns: - Call result. - - .. method:: start_receive (stream) - - Mark the :attr:`receive_side ` on `stream` as - ready for reading. Safe to call from any thread. When the associated - file descriptor becomes ready for reading, - :meth:`BasicStream.on_receive` will be called. - - .. method:: stop_receive (stream) - - Mark the :attr:`receive_side ` on `stream` as - not ready for reading. Safe to call from any thread. - - .. method:: _start_transmit (stream) - - Mark the :attr:`transmit_side ` on `stream` as - ready for writing. Must only be called from the Broker thread. When the - associated file descriptor becomes ready for writing, - :meth:`BasicStream.on_transmit` will be called. - - .. method:: stop_receive (stream) - - Mark the :attr:`transmit_side ` on `stream` as - not ready for writing. Safe to call from any thread. - - .. method:: shutdown - - Request broker gracefully disconnect streams and stop. - - .. method:: join - - Wait for the broker to stop, expected to be called after - :meth:`shutdown`. - - .. method:: keep_alive - - Return :data:`True` if any reader's :attr:`Side.keep_alive` - attribute is :data:`True`, or any - :class:`Context ` is still - registered that is not the master. Used to delay shutdown while some - important work is in progress (e.g. log draining). - - **Internal Methods** - - .. method:: _broker_main - - Handle events until :meth:`shutdown`. On shutdown, invoke - :meth:`Stream.on_shutdown` for every active stream, then allow up to - :attr:`shutdown_timeout` seconds for the streams to unregister - themselves before forcefully calling - :meth:`Stream.on_disconnect`. +.. autoclass:: Broker + :members: .. currentmodule:: mitogen.master -.. class:: Broker (install_watcher=True) - - .. note:: - - You may construct as many brokers as desired, and use the same broker - for multiple routers, however usually only one broker need exist. - Multiple brokers may be useful when dealing with sets of children with - differing lifetimes. For example, a subscription service where - non-payment results in termination for one customer. - - :param bool install_watcher: - If :data:`True`, an additional thread is started to monitor the - lifetime of the main thread, triggering :meth:`shutdown` - automatically in case the user forgets to call it, or their code - crashed. - - You should not rely on this functionality in your program, it is only - intended as a fail-safe and to simplify the API for new users. In - particular, alternative Python implementations may not be able to - support watching the main thread. - - .. attribute:: shutdown_timeout = 5.0 - - Seconds grace to allow :class:`streams ` to shutdown - gracefully before force-disconnecting them during :meth:`shutdown`. +.. autoclass:: Broker + :members: Utility Functions diff --git a/docs/changelog.rst b/docs/changelog.rst index a7109a8d..6c095dfb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -126,7 +126,7 @@ Core Library v0.2.4 (2018-??-??) ------------------- +------------------- Mitogen for Ansible ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/internals.rst b/docs/internals.rst index 9c533952..fc9d57ac 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -35,162 +35,33 @@ Side Class ========== .. currentmodule:: mitogen.core - -.. class:: Side (stream, fd, keep_alive=True) - - Represent a single side of a :py:class:`BasicStream`. This exists to allow - streams implemented using unidirectional (e.g. UNIX pipe) and bidirectional - (e.g. UNIX socket) file descriptors to operate identically. - - :param mitogen.core.Stream stream: - The stream this side is associated with. - - :param int fd: - Underlying file descriptor. - - :param bool keep_alive: - Value for :py:attr:`keep_alive` - - During construction, the file descriptor has its :py:data:`os.O_NONBLOCK` - flag enabled using :py:func:`fcntl.fcntl`. - - .. attribute:: stream - - The :py:class:`Stream` for which this is a read or write side. - - .. attribute:: fd - - Integer file descriptor to perform IO on, or :data:`None` if - :py:meth:`close` has been called. - - .. attribute:: keep_alive - - If :data:`True`, causes presence of this side in :py:class:`Broker`'s - active reader set to defer shutdown until the side is disconnected. - - .. method:: fileno - - Return :py:attr:`fd` if it is not :data:`None`, otherwise raise - :py:class:`StreamError`. This method is implemented so that - :py:class:`Side` can be used directly by :py:func:`select.select`. - - .. method:: close - - Call :py:func:`os.close` on :py:attr:`fd` if it is not :data:`None`, - then set it to :data:`None`. - - .. method:: read (n=CHUNK_SIZE) - - Read up to `n` bytes from the file descriptor, wrapping the underlying - :py:func:`os.read` call with :py:func:`io_op` to trap common - disconnection conditions. - - :py:meth:`read` always behaves as if it is reading from a regular UNIX - file; socket, pipe, and TTY disconnection errors are masked and result - in a 0-sized read just like a regular file. - - :returns: - Bytes read, or the empty to string to indicate disconnection was - detected. - - .. method:: write (s) - - Write as much of the bytes from `s` as possible to the file descriptor, - wrapping the underlying :py:func:`os.write` call with :py:func:`io_op` - to trap common disconnection connditions. - - :returns: - Number of bytes written, or :data:`None` if disconnection was - detected. +.. autoclass:: Side + :members: Stream Classes ============== .. currentmodule:: mitogen.core - -.. class:: BasicStream - - .. attribute:: receive_side - - A :py:class:`Side` representing the stream's receive file descriptor. - - .. attribute:: transmit_side - - A :py:class:`Side` representing the stream's transmit file descriptor. - - .. method:: on_disconnect (broker) - - Called by :py:class:`Broker` to force disconnect the stream. The base - implementation simply closes :py:attr:`receive_side` and - :py:attr:`transmit_side` and unregisters the stream from the broker. - - .. method:: on_receive (broker) - - Called by :py:class:`Broker` when the stream's :py:attr:`receive_side` has - been marked readable using :py:meth:`Broker.start_receive` and the - broker has detected the associated file descriptor is ready for - reading. - - Subclasses must implement this method if - :py:meth:`Broker.start_receive` is ever called on them, and the method - must call :py:meth:`on_disconect` if reading produces an empty string. - - .. method:: on_transmit (broker) - - Called by :py:class:`Broker` when the stream's :py:attr:`transmit_side` - has been marked writeable using :py:meth:`Broker._start_transmit` and - the broker has detected the associated file descriptor is ready for - writing. - - Subclasses must implement this method if - :py:meth:`Broker._start_transmit` is ever called on them. - - .. method:: on_shutdown (broker) - - Called by :py:meth:`Broker.shutdown` to allow the stream time to - gracefully shutdown. The base implementation simply called - :py:meth:`on_disconnect`. +.. autoclass:: BasicStream + :members: .. autoclass:: Stream :members: - .. method:: pending_bytes () - - Returns the number of bytes queued for transmission on this stream. - This can be used to limit the amount of data buffered in RAM by an - otherwise unlimited consumer. - - For an accurate result, this method should be called from the Broker - thread, using a wrapper like: - - :: - - def get_pending_bytes(self, stream): - latch = mitogen.core.Latch() - self.broker.defer( - lambda: latch.put(stream.pending_bytes()) - ) - return latch.get() - - .. currentmodule:: mitogen.fork - .. autoclass:: Stream :members: .. currentmodule:: mitogen.parent - .. autoclass:: Stream :members: .. currentmodule:: mitogen.ssh - .. autoclass:: Stream :members: .. currentmodule:: mitogen.sudo - .. autoclass:: Stream :members: @@ -212,6 +83,7 @@ Poller Class .. currentmodule:: mitogen.core .. autoclass:: Poller + :members: .. currentmodule:: mitogen.parent .. autoclass:: KqueuePoller @@ -256,64 +128,16 @@ ExternalContext Class ===================== .. currentmodule:: mitogen.core +.. autoclass:: ExternalContext + :members: -.. class:: ExternalContext - - External context implementation. - - .. attribute:: broker - - The :py:class:`mitogen.core.Broker` instance. - - .. attribute:: context - - The :py:class:`mitogen.core.Context` instance. - - .. attribute:: channel - - The :py:class:`mitogen.core.Channel` over which - :py:data:`CALL_FUNCTION` requests are received. - - .. attribute:: stdout_log - - The :py:class:`mitogen.core.IoLogger` connected to ``stdout``. - - .. attribute:: importer - - The :py:class:`mitogen.core.Importer` instance. - - .. attribute:: stdout_log - - The :py:class:`IoLogger` connected to ``stdout``. - - .. attribute:: stderr_log - - The :py:class:`IoLogger` connected to ``stderr``. - - .. method:: _dispatch_calls - - Implementation for the main thread in every child context. mitogen.master ============== -.. currentmodule:: mitogen.master - -.. class:: ProcessMonitor - - Install a :py:data:`signal.SIGCHLD` handler that generates callbacks when a - specific child process has exitted. - - .. method:: add (pid, callback) - - Add a callback function to be notified of the exit status of a process. - - :param int pid: - Process ID to be notified of. - - :param callback: - Function invoked as `callback(status)`, where `status` is the raw - exit status of the child process. +.. currentmodule:: mitogen.parent +.. autoclass:: ProcessMonitor + :members: Blocking I/O Functions diff --git a/mitogen/core.py b/mitogen/core.py index 6cb311d3..2b1771a5 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -100,7 +100,7 @@ CALL_SERVICE = 110 #: * a remote receiver is disconnected or explicitly closed. #: * a related message could not be delivered due to no route existing for it. #: * a router is being torn down, as a sentinel value to notify -#: :py:meth:`mitogen.core.Router.add_handler` callbacks to clean up. +#: :meth:`mitogen.core.Router.add_handler` callbacks to clean up. IS_DEAD = 999 try: @@ -187,7 +187,7 @@ class Error(Exception): class LatchError(Error): - """Raised when an attempt is made to use a :py:class:`mitogen.core.Latch` + """Raised when an attempt is made to use a :class:`mitogen.core.Latch` that has been marked closed.""" pass @@ -239,7 +239,7 @@ class Kwargs(dict): class CallError(Error): """Serializable :class:`Error` subclass raised when - :py:meth:`Context.call() ` fails. A copy of + :meth:`Context.call() ` fails. A copy of the traceback from the external context is appended to the exception message.""" def __init__(self, fmt=None, *args): @@ -872,6 +872,15 @@ class Receiver(object): class Channel(Sender, Receiver): + """ + A channel inherits from :class:`mitogen.core.Sender` and + `mitogen.core.Receiver` to provide bidirectional functionality. + + Since all handles aren't known until after both ends are constructed, for + both ends to communicate through a channel, it is necessary for one end to + retrieve the handle allocated to the other and reconfigure its own channel + to match. Currently this is a manual task. + """ def __init__(self, router, context, dst_handle, handle=None): Sender.__init__(self, context, dst_handle) Receiver.__init__(self, router, handle) @@ -1160,12 +1169,35 @@ class LogHandler(logging.Handler): class Side(object): + """ + Represent a single side of a :class:`BasicStream`. This exists to allow + streams implemented using unidirectional (e.g. UNIX pipe) and bidirectional + (e.g. UNIX socket) file descriptors to operate identically. + + :param mitogen.core.Stream stream: + The stream this side is associated with. + + :param int fd: + Underlying file descriptor. + + :param bool keep_alive: + Value for :attr:`keep_alive` + + During construction, the file descriptor has its :data:`os.O_NONBLOCK` flag + enabled using :func:`fcntl.fcntl`. + """ _fork_refs = weakref.WeakValueDictionary() def __init__(self, stream, fd, cloexec=True, keep_alive=True, blocking=False): + #: The :class:`Stream` for which this is a read or write side. self.stream = stream + #: Integer file descriptor to perform IO on, or :data:`None` if + #: :meth:`close` has been called. self.fd = fd self.closed = False + #: If :data:`True`, causes presence of this side in + #: :class:`Broker`'s active reader set to defer shutdown until the + #: side is disconnected. self.keep_alive = keep_alive self._fork_refs[id(self)] = self if cloexec: @@ -1182,12 +1214,29 @@ class Side(object): side.close() def close(self): + """ + Call :func:`os.close` on :attr:`fd` if it is not :data:`None`, + then set it to :data:`None`. + """ if not self.closed: _vv and IOLOG.debug('%r.close()', self) self.closed = True os.close(self.fd) def read(self, n=CHUNK_SIZE): + """ + Read up to `n` bytes from the file descriptor, wrapping the underlying + :func:`os.read` call with :func:`io_op` to trap common disconnection + conditions. + + :meth:`read` always behaves as if it is reading from a regular UNIX + file; socket, pipe, and TTY disconnection errors are masked and result + in a 0-sized read like a regular file. + + :returns: + Bytes read, or the empty to string to indicate disconnection was + detected. + """ if self.closed: # Refuse to touch the handle after closed, it may have been reused # by another thread. TODO: synchronize read()/write()/close(). @@ -1198,6 +1247,15 @@ class Side(object): return s def write(self, s): + """ + Write as much of the bytes from `s` as possible to the file descriptor, + wrapping the underlying :func:`os.write` call with :func:`io_op` to + trap common disconnection connditions. + + :returns: + Number of bytes written, or :data:`None` if disconnection was + detected. + """ if self.closed or self.fd is None: # Refuse to touch the handle after closed, it may have been reused # by another thread. @@ -1210,10 +1268,50 @@ class Side(object): class BasicStream(object): + #: A :class:`Side` representing the stream's receive file descriptor. receive_side = None + + #: A :class:`Side` representing the stream's transmit file descriptor. transmit_side = None + def on_receive(self, broker): + """ + Called by :class:`Broker` when the stream's :attr:`receive_side` has + been marked readable using :meth:`Broker.start_receive` and the broker + has detected the associated file descriptor is ready for reading. + + Subclasses must implement this if :meth:`Broker.start_receive` is ever + called on them, and the method must call :meth:`on_disconect` if + reading produces an empty string. + """ + + def on_transmit(self, broker): + """ + Called by :class:`Broker` when the stream's :attr:`transmit_side` + has been marked writeable using :meth:`Broker._start_transmit` and + the broker has detected the associated file descriptor is ready for + writing. + + Subclasses must implement this if :meth:`Broker._start_transmit` is + ever called on them. + """ + + def on_shutdown(self, broker): + """ + Called by :meth:`Broker.shutdown` to allow the stream time to + gracefully shutdown. The base implementation simply called + :meth:`on_disconnect`. + """ + _v and LOG.debug('%r.on_shutdown()', self) + fire(self, 'shutdown') + self.on_disconnect(broker) + def on_disconnect(self, broker): + """ + Called by :class:`Broker` to force disconnect the stream. The base + implementation simply closes :attr:`receive_side` and + :attr:`transmit_side` and unregisters the stream from the broker. + """ LOG.debug('%r.on_disconnect()', self) if self.receive_side: broker.stop_receive(self) @@ -1223,19 +1321,14 @@ class BasicStream(object): self.transmit_side.close() fire(self, 'disconnect') - def on_shutdown(self, broker): - _v and LOG.debug('%r.on_shutdown()', self) - fire(self, 'shutdown') - self.on_disconnect(broker) - class Stream(BasicStream): """ - :py:class:`BasicStream` subclass implementing mitogen's :ref:`stream + :class:`BasicStream` subclass implementing mitogen's :ref:`stream protocol `. """ - #: If not :data:`None`, :py:class:`Router` stamps this into - #: :py:attr:`Message.auth_id` of every message received on this stream. + #: If not :data:`None`, :class:`Router` stamps this into + #: :attr:`Message.auth_id` of every message received on this stream. auth_id = None #: If not :data:`False`, indicates the stream has :attr:`auth_id` set and @@ -1272,7 +1365,7 @@ class Stream(BasicStream): def on_receive(self, broker): """Handle the next complete message on the stream. Raise - :py:class:`StreamError` on failure.""" + :class:`StreamError` on failure.""" _vv and IOLOG.debug('%r.on_receive()', self) buf = self.receive_side.read() @@ -1329,6 +1422,14 @@ class Stream(BasicStream): return True def pending_bytes(self): + """ + Return the number of bytes queued for transmission on this stream. This + can be used to limit the amount of data buffered in RAM by an otherwise + unlimited consumer. + + For an accurate result, this method should be called from the Broker + thread, for example by using :meth:`Broker.defer_sync`. + """ return self._output_buf_len def on_transmit(self, broker): @@ -1572,15 +1673,15 @@ class Poller(object): class Latch(object): """ - A latch is a :py:class:`Queue.Queue`-like object that supports mutation and - waiting from multiple threads, however unlike :py:class:`Queue.Queue`, + A latch is a :class:`Queue.Queue`-like object that supports mutation and + waiting from multiple threads, however unlike :class:`Queue.Queue`, waiting threads always remain interruptible, so CTRL+C always succeeds, and waits where a timeout is set experience no wake up latency. These properties are not possible in combination using the built-in threading primitives available in Python 2.x. Latches implement queues using the UNIX self-pipe trick, and a per-thread - :py:func:`socket.socketpair` that is lazily created the first time any + :func:`socket.socketpair` that is lazily created the first time any latch attempts to sleep on a thread, and dynamically associated with the waiting Latch only for duration of the wait. @@ -1626,7 +1727,7 @@ class Latch(object): def close(self): """ Mark the latch as closed, and cause every sleeping thread to be woken, - with :py:class:`mitogen.core.LatchError` raised in each thread. + with :class:`mitogen.core.LatchError` raised in each thread. """ self._lock.acquire() try: @@ -1640,17 +1741,17 @@ class Latch(object): def empty(self): """ - Return :py:data:`True` if calling :py:meth:`get` would block. + Return :data:`True` if calling :meth:`get` would block. - As with :py:class:`Queue.Queue`, :py:data:`True` may be returned even - though a subsequent call to :py:meth:`get` will succeed, since a - message may be posted at any moment between :py:meth:`empty` and - :py:meth:`get`. + As with :class:`Queue.Queue`, :data:`True` may be returned even + though a subsequent call to :meth:`get` will succeed, since a + message may be posted at any moment between :meth:`empty` and + :meth:`get`. - As with :py:class:`Queue.Queue`, :py:data:`False` may be returned even - though a subsequent call to :py:meth:`get` will block, since another - waiting thread may be woken at any moment between :py:meth:`empty` and - :py:meth:`get`. + As with :class:`Queue.Queue`, :data:`False` may be returned even + though a subsequent call to :meth:`get` will block, since another + waiting thread may be woken at any moment between :meth:`empty` and + :meth:`get`. """ return len(self._queue) == 0 @@ -1683,14 +1784,14 @@ class Latch(object): Return the next enqueued object, or sleep waiting for one. :param float timeout: - If not :py:data:`None`, specifies a timeout in seconds. + If not :data:`None`, specifies a timeout in seconds. :param bool block: - If :py:data:`False`, immediately raise - :py:class:`mitogen.core.TimeoutError` if the latch is empty. + If :data:`False`, immediately raise + :class:`mitogen.core.TimeoutError` if the latch is empty. :raises mitogen.core.LatchError: - :py:meth:`close` has been called, and the object is no longer valid. + :meth:`close` has been called, and the object is no longer valid. :raises mitogen.core.TimeoutError: Timeout was reached. @@ -1771,7 +1872,7 @@ class Latch(object): exists. :raises mitogen.core.LatchError: - :py:meth:`close` has been called, and the object is no longer valid. + :meth:`close` has been called, and the object is no longer valid. """ _vv and IOLOG.debug('%r.put(%r)', self, obj) self._lock.acquire() @@ -1807,7 +1908,7 @@ class Latch(object): class Waker(BasicStream): """ - :py:class:`BasicStream` subclass implementing the `UNIX self-pipe trick`_. + :class:`BasicStream` subclass implementing the `UNIX self-pipe trick`_. Used to wake the multiplexer when another thread needs to modify its state (via a cross-thread function call). @@ -1893,8 +1994,8 @@ class Waker(BasicStream): class IoLogger(BasicStream): """ - :py:class:`BasicStream` subclass that sets up redirection of a standard - UNIX file descriptor back into the Python :py:mod:`logging` package. + :class:`BasicStream` subclass that sets up redirection of a standard + UNIX file descriptor back into the Python :mod:`logging` package. """ _buf = '' @@ -2126,8 +2227,8 @@ class Router(object): return handle def on_shutdown(self, broker): - """Called during :py:meth:`Broker.shutdown`, informs callbacks - registered with :py:meth:`add_handle_cb` the connection is dead.""" + """Called during :meth:`Broker.shutdown`, informs callbacks registered + with :meth:`add_handle_cb` the connection is dead.""" _v and LOG.debug('%r.on_shutdown(%r)', self, broker) fire(self, 'shutdown') for handle, (persist, fn) in self._handle_map.iteritems(): @@ -2249,14 +2350,26 @@ class Router(object): class Broker(object): + """ + Responsible for handling I/O multiplexing in a private thread. + + **Note:** This is the somewhat limited core version of the Broker class + used by child contexts. The master subclass is documented below. + """ poller_class = Poller _waker = None _thread = None + + #: Seconds grace to allow :class:`streams ` to shutdown gracefully + #: before force-disconnecting them during :meth:`shutdown`. shutdown_timeout = 3.0 def __init__(self, poller_class=None): self._alive = True self._waker = Waker(self) + #: Arrange for `func(\*args, \**kwargs)` to be executed on the broker + #: thread, or immediately if the current thread is the broker thread. + #: Safe to call from any thread. self.defer = self._waker.defer self.poller = self.poller_class() self.poller.start_receive( @@ -2272,6 +2385,12 @@ class Broker(object): self._waker.broker_ident = self._thread.ident def start_receive(self, stream): + """ + Mark the :attr:`receive_side ` on `stream` as + ready for reading. Safe to call from any thread. When the associated + file descriptor becomes ready for reading, + :meth:`BasicStream.on_receive` will be called. + """ _vv and IOLOG.debug('%r.start_receive(%r)', self, stream) side = stream.receive_side assert side and side.fd is not None @@ -2279,26 +2398,47 @@ class Broker(object): side.fd, (side, stream.on_receive)) def stop_receive(self, stream): + """ + Mark the :attr:`receive_side ` on `stream` as not + ready for reading. Safe to call from any thread. + """ _vv and IOLOG.debug('%r.stop_receive(%r)', self, stream) self.defer(self.poller.stop_receive, stream.receive_side.fd) def _start_transmit(self, stream): + """ + Mark the :attr:`transmit_side ` on `stream` as + ready for writing. Must only be called from the Broker thread. When the + associated file descriptor becomes ready for writing, + :meth:`BasicStream.on_transmit` will be called. + """ _vv and IOLOG.debug('%r._start_transmit(%r)', self, stream) side = stream.transmit_side assert side and side.fd is not None self.poller.start_transmit(side.fd, (side, stream.on_transmit)) def _stop_transmit(self, stream): + """ + Mark the :attr:`transmit_side ` on `stream` as not + ready for writing. + """ _vv and IOLOG.debug('%r._stop_transmit(%r)', self, stream) self.poller.stop_transmit(stream.transmit_side.fd) def keep_alive(self): + """ + Return :data:`True` if any reader's :attr:`Side.keep_alive` attribute + is :data:`True`, or any :class:`Context` is still registered that is + not the master. Used to delay shutdown while some important work is in + progress (e.g. log draining). + """ it = (side.keep_alive for (_, (side, _)) in self.poller.readers) return sum(it, 0) def defer_sync(self, func): """ - Block the calling thread while `func` runs on a broker thread. + Arrange for `func()` to execute on the broker thread, blocking the + current thread until a result or exception is available. :returns: Return value of `func()`. @@ -2349,6 +2489,12 @@ class Broker(object): side.stream.on_disconnect(self) def _broker_main(self): + """ + Handle events until :meth:`shutdown`. On shutdown, invoke + :meth:`Stream.on_shutdown` for every active stream, then allow up to + :attr:`shutdown_timeout` seconds for the streams to unregister + themselves before forcefully calling :meth:`Stream.on_disconnect`. + """ try: while self._alive: self._loop_once() @@ -2361,12 +2507,20 @@ class Broker(object): fire(self, 'exit') def shutdown(self): + """ + Request broker gracefully disconnect streams and stop. Safe to call + from any thread. + """ _v and LOG.debug('%r.shutdown()', self) def _shutdown(): self._alive = False self.defer(_shutdown) def join(self): + """ + Wait for the broker to stop, expected to be called after + :meth:`shutdown`. + """ self._thread.join() def __repr__(self): @@ -2438,6 +2592,34 @@ class Dispatcher(object): class ExternalContext(object): + """ + External context implementation. + + .. attribute:: broker + The :class:`mitogen.core.Broker` instance. + + .. attribute:: context + The :class:`mitogen.core.Context` instance. + + .. attribute:: channel + The :class:`mitogen.core.Channel` over which :data:`CALL_FUNCTION` + requests are received. + + .. attribute:: stdout_log + The :class:`mitogen.core.IoLogger` connected to ``stdout``. + + .. attribute:: importer + The :class:`mitogen.core.Importer` instance. + + .. attribute:: stdout_log + The :class:`IoLogger` connected to ``stdout``. + + .. attribute:: stderr_log + The :class:`IoLogger` connected to ``stderr``. + + .. method:: _dispatch_calls + Implementation for the main thread in every child context. + """ detached = False def __init__(self, config): diff --git a/mitogen/master.py b/mitogen/master.py index 73302910..4b1b164d 100644 --- a/mitogen/master.py +++ b/mitogen/master.py @@ -754,6 +754,26 @@ class ModuleResponder(object): class Broker(mitogen.core.Broker): + """ + .. note:: + + You may construct as many brokers as desired, and use the same broker + for multiple routers, however usually only one broker need exist. + Multiple brokers may be useful when dealing with sets of children with + differing lifetimes. For example, a subscription service where + non-payment results in termination for one customer. + + :param bool install_watcher: + If :data:`True`, an additional thread is started to monitor the + lifetime of the main thread, triggering :meth:`shutdown` + automatically in case the user forgets to call it, or their code + crashed. + + You should not rely on this functionality in your program, it is only + intended as a fail-safe and to simplify the API for new users. In + particular, alternative Python implementations may not be able to + support watching the main thread. + """ shutdown_timeout = 5.0 _watcher = None poller_class = mitogen.parent.PREFERRED_POLLER @@ -773,7 +793,32 @@ class Broker(mitogen.core.Broker): class Router(mitogen.parent.Router): + """ + Extend :class:`mitogen.core.Router` with functionality useful to masters, + and child contexts who later become masters. Currently when this class is + required, the target context's router is upgraded at runtime. + + .. note:: + + You may construct as many routers as desired, and use the same broker + for multiple routers, however usually only one broker and router need + exist. Multiple routers may be useful when dealing with separate trust + domains, for example, manipulating infrastructure belonging to separate + customers or projects. + + :param mitogen.master.Broker broker: + Broker to use. If not specified, a private :class:`Broker` is created. + """ broker_class = Broker + + #: When :data:`True`, cause the broker thread and any subsequent broker and + #: main threads existing in any child to write + #: ``/tmp/mitogen.stats...log`` containing a + #: :mod:`cProfile` dump on graceful exit. Must be set prior to construction + #: of any :class:`Broker`, e.g. via: + #: + #: .. code:: + #: mitogen.master.Router.profiling = True profiling = False def __init__(self, broker=None, max_message_size=None): @@ -796,6 +841,10 @@ class Router(mitogen.parent.Router): ) def enable_debug(self): + """ + Cause this context and any descendant child contexts to write debug + logs to ``/tmp/mitogen..log``. + """ mitogen.core.enable_debug_logging() self.debug = True @@ -830,6 +879,12 @@ class IdAllocator(object): BLOCK_SIZE = 1000 def allocate(self): + """ + Arrange for a unique context ID to be allocated and associated with a + route leading to the active context. In masters, the ID is generated + directly, in children it is forwarded to the master via a + :data:`mitogen.core.ALLOCATE_ID` message. + """ self.lock.acquire() try: id_ = self.next_id diff --git a/mitogen/parent.py b/mitogen/parent.py index 0fffdd67..780cecd7 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1822,6 +1822,10 @@ class Router(mitogen.core.Router): class ProcessMonitor(object): + """ + Install a :data:`signal.SIGCHLD` handler that generates callbacks when a + specific child process has exitted. This class is obsolete, do not use. + """ def __init__(self): # pid -> callback() self.callback_by_pid = {} @@ -1835,6 +1839,16 @@ class ProcessMonitor(object): del self.callback_by_pid[pid] def add(self, pid, callback): + """ + Add a callback function to be notified of the exit status of a process. + + :param int pid: + Process ID to be notified of. + + :param callback: + Function invoked as `callback(status)`, where `status` is the raw + exit status of the child process. + """ self.callback_by_pid[pid] = callback _instance = None From 5eff8ea4fb21d0ea34dfefc67f312ccf7a60bf12 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 13:01:47 +0000 Subject: [PATCH 117/662] tests: make result_shell_echo_hi compare less of the timedelta; closes #361 Assuming less than one second is too much to ask from Travis. --- tests/ansible/integration/async/result_shell_echo_hi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ansible/integration/async/result_shell_echo_hi.yml b/tests/ansible/integration/async/result_shell_echo_hi.yml index 8858037a..dbf40bde 100644 --- a/tests/ansible/integration/async/result_shell_echo_hi.yml +++ b/tests/ansible/integration/async/result_shell_echo_hi.yml @@ -24,7 +24,7 @@ that: - async_out.changed == True - async_out.cmd == "echo hi" - - 'async_out.delta.startswith("0:00:00")' + - 'async_out.delta.startswith("0:00:")' - async_out.end.startswith("20") - async_out.invocation.module_args._raw_params == "echo hi" - async_out.invocation.module_args._uses_shell == True From 8e4c164d931dbe890ba7c886714f5dae835ce21c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 13:43:41 +0000 Subject: [PATCH 118/662] issue #388: fix Sphinx markup --- docs/api.rst | 25 +++++++++++++------------ mitogen/master.py | 3 +-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 57b9a655..bfba1f77 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -107,7 +107,8 @@ Router Class Connection Methods ================== -.. method:: mitogen.parent.Router.fork (on_fork=None, on_start=None, debug=False, profiling=False, via=None) +.. currentmodule:: mitogen.parent +.. method:: Router.fork (on_fork=None, on_start=None, debug=False, profiling=False, via=None) Construct a context on the local machine by forking the current process. The forked child receives a new identity, sets up a new broker @@ -203,7 +204,7 @@ Connection Methods :param bool profiling: Same as the `profiling` parameter for :meth:`local`. -.. method:: mitogen.parent.Router.local (remote_name=None, python_path=None, debug=False, connect_timeout=None, profiling=False, via=None) +.. method:: Router.local (remote_name=None, python_path=None, debug=False, connect_timeout=None, profiling=False, via=None) Construct a context on the local machine as a subprocess of the current process. The associated stream implementation is @@ -264,7 +265,7 @@ Connection Methods # Use the SSH connection to create a sudo connection. remote_root = router.sudo(username='root', via=remote_machine) -.. method:: mitogen.parent.Router.doas (username=None, password=None, doas_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) +.. method:: Router.doas (username=None, password=None, doas_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) Construct a context on the local machine over a ``doas`` invocation. The ``doas`` process is started in a newly allocated pseudo-terminal, @@ -289,7 +290,7 @@ Connection Methods A password was requested but none was provided, the supplied password was incorrect, or the target account did not exist. -.. method:: mitogen.parent.Router.docker (container=None, image=None, docker_path=None, \**kwargs) +.. method:: Router.docker (container=None, image=None, docker_path=None, \**kwargs) Construct a context on the local machine within an existing or temporary new Docker container using the ``docker`` program. One of @@ -309,7 +310,7 @@ Connection Methods Filename or complete path to the Docker binary. ``PATH`` will be searched if given as a filename. Defaults to ``docker``. -.. method:: mitogen.parent.Router.jail (container, jexec_path=None, \**kwargs) +.. method:: Router.jail (container, jexec_path=None, \**kwargs) Construct a context on the local machine within a FreeBSD jail using the ``jexec`` program. @@ -325,7 +326,7 @@ Connection Methods Filename or complete path to the ``jexec`` binary. ``PATH`` will be searched if given as a filename. Defaults to ``/usr/sbin/jexec``. -.. method:: mitogen.parent.Router.kubectl (pod, kubectl_path=None, kubectl_args=None, \**kwargs) +.. method:: Router.kubectl (pod, kubectl_path=None, kubectl_args=None, \**kwargs) Construct a context in a container via the Kubernetes ``kubectl`` program. @@ -340,7 +341,7 @@ Connection Methods :param list kubectl_args: Additional arguments to pass to the ``kubectl`` command. -.. method:: mitogen.parent.Router.lxc (container, lxc_attach_path=None, \**kwargs) +.. method:: Router.lxc (container, lxc_attach_path=None, \**kwargs) Construct a context on the local machine within an LXC classic container using the ``lxc-attach`` program. @@ -354,7 +355,7 @@ Connection Methods will be searched if given as a filename. Defaults to ``lxc-attach``. -.. method:: mitogen.parent.Router.lxc (container, lxc_attach_path=None, \**kwargs) +.. method:: Router.lxc (container, lxc_attach_path=None, \**kwargs) Construct a context on the local machine within a LXD container using the ``lxc`` program. @@ -367,7 +368,7 @@ Connection Methods Filename or complete path to the ``lxc`` binary. ``PATH`` will be searched if given as a filename. Defaults to ``lxc``. -.. method:: mitogen.parent.Router.setns (container, kind, username=None, docker_path=None, lxc_info_path=None, machinectl_path=None, \**kwargs) +.. method:: Router.setns (container, kind, username=None, docker_path=None, lxc_info_path=None, machinectl_path=None, \**kwargs) Construct a context in the style of :meth:`local`, but change the active Linux process namespaces via calls to `setns(1)` before @@ -401,7 +402,7 @@ Connection Methods will be searched if given as a filename. Defaults to ``machinectl``. -.. method:: mitogen.parent.Router.su (username=None, password=None, su_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) +.. method:: Router.su (username=None, password=None, su_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs) Construct a context on the local machine over a ``su`` invocation. The ``su`` process is started in a newly allocated pseudo-terminal, and @@ -428,7 +429,7 @@ Connection Methods password was incorrect, or (on BSD) the target account did not exist. -.. method:: mitogen.parent.Router.sudo (username=None, sudo_path=None, password=None, \**kwargs) +.. method:: Router.sudo (username=None, sudo_path=None, password=None, \**kwargs) Construct a context on the local machine over a ``sudo`` invocation. The ``sudo`` process is started in a newly allocated pseudo-terminal, @@ -464,7 +465,7 @@ Connection Methods :class:`mitogen.core.StreamError` to be raised, and that attributes of the stream match the actual behaviour of ``sudo``. -.. method:: mitogen.parent.Router.ssh (hostname, username=None, ssh_path=None, ssh_args=None, port=None, check_host_keys='enforce', password=None, identity_file=None, identities_only=True, compression=True, \**kwargs) +.. method:: Router.ssh (hostname, username=None, ssh_path=None, ssh_args=None, port=None, check_host_keys='enforce', password=None, identity_file=None, identities_only=True, compression=True, \**kwargs) Construct a remote context over an OpenSSH ``ssh`` invocation. diff --git a/mitogen/master.py b/mitogen/master.py index 4b1b164d..65985b4d 100644 --- a/mitogen/master.py +++ b/mitogen/master.py @@ -815,9 +815,8 @@ class Router(mitogen.parent.Router): #: main threads existing in any child to write #: ``/tmp/mitogen.stats...log`` containing a #: :mod:`cProfile` dump on graceful exit. Must be set prior to construction - #: of any :class:`Broker`, e.g. via: + #: of any :class:`Broker`, e.g. via:: #: - #: .. code:: #: mitogen.master.Router.profiling = True profiling = False From 5bdb745f0796a27255e35a193fd9692a746a4061 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 13:52:20 +0000 Subject: [PATCH 119/662] docs: howitworks tweaks --- docs/howitworks.rst | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 5e2c10f5..65a6daee 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -16,17 +16,17 @@ The UNIX First Stage To allow delivery of the bootstrap compressed using :py:mod:`zlib`, it is necessary for something on the remote to be prepared to decompress the payload -and feed it to a Python interpreter. Since we would like to avoid writing an -error-prone shell fragment to implement this, and since we must avoid writing -to the remote machine's disk in case it is read-only, the Python process -started on the remote machine by Mitogen immediately forks in order to +and feed it to a Python interpreter [#f1]_. Since we would like to avoid +writing an error-prone shell fragment to implement this, and since we must +avoid writing to the remote machine's disk in case it is read-only, the Python +process started on the remote machine by Mitogen immediately forks in order to implement the decompression. Python Command Line ################### -The Python command line sent to the host is a :mod:`zlib`-compressed [#f1]_ and +The Python command line sent to the host is a :mod:`zlib`-compressed [#f2]_ and base64-encoded copy of the :py:meth:`mitogen.master.Stream._first_stage` function, which has been carefully optimized to reduce its size. Prior to compression and encoding, ``CONTEXT_NAME`` is replaced with the desired context @@ -65,10 +65,10 @@ allowing reading by the first stage of exactly the required bytes. Configuring argv[0] ################### -Forking provides us with an excellent opportunity for tidying up the eventual -Python interpreter, in particular, restarting it using a fresh command-line to -get rid of the large base64-encoded first stage parameter, and to replace -**argv[0]** with something descriptive. +Forking provides an excellent opportunity to tidy up the eventual Python +interpreter, in particular, restarting it using a fresh command-line to get rid +of the large base64-encoded first stage parameter, and to replace **argv[0]** +with something descriptive. After configuring its ``stdin`` to point to the read end of the pipe, the parent half of the fork re-executes Python, with **argv[0]** taken from the @@ -1018,7 +1018,13 @@ receive items in the order they are requested, as they become available. .. rubric:: Footnotes -.. [#f1] Compression may seem redundant, however it is basically free and reducing IO +.. [#f1] Although some connection methods such as SSH support compression, and + Mitogen enables SSH compression by default, there are circumstances where + disabling SSH compression is desirable, and many scenarios for future + connection methods where transport-layer compression is not supported at + all. + +.. [#f2] Compression may seem redundant, however it is basically free and reducing IO is always a good idea. The 33% / 200 byte saving may mean the presence or absence of an additional frame on the network, or in real world terms after accounting for SSH overhead, around a 2% reduced chance of a stall during From 6fdc45da1aa60d6f6a77a4236b96e08a055cb908 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 14:48:24 +0000 Subject: [PATCH 120/662] docs: Changelog concision --- docs/ansible.rst | 2 +- docs/changelog.rst | 57 +++++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/docs/ansible.rst b/docs/ansible.rst index c30fffc2..33c73d06 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -429,7 +429,7 @@ Temporary Files Temporary file handling in Ansible is tricky, and the precise behaviour varies across major versions. A variety of temporary files and directories are -created, depending on the operating mode: +created, depending on the operating mode. In the best case when pipelining is enabled and no temporary uploads are required, for each task Ansible will create one directory below a diff --git a/docs/changelog.rst b/docs/changelog.rst index 6c095dfb..5405f209 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -137,15 +137,14 @@ Enhancements * `#76 `_, `#351 `_, `#352 `_: disconnect propagation - has improved, allowing Ansible to cancel waits for responses from targets - that where abruptly disconnected. This increases the chance a task will fail - gracefully, rather than hanging due to the connection being severed, for - example because of network failure or EC2 instance maintenance. + has improved, allowing Ansible to cancel waits for responses from abruptly + disconnected targets. This ensures a task will gracefully fail rather than + hang, for example on network failure or EC2 instance maintenance. * `#369 `_: :meth:`Connection.reset` is implemented, allowing `meta: reset_connection `_ to shut - down the remote interpreter as expected, and improving support for the + down the remote interpreter as documented, and improving support for the `reboot `_ module. @@ -156,26 +155,22 @@ Fixes * `#323 `_, `#333 `_: work around a Windows - Subsystem for Linux bug that would cause tracebacks to be rendered during - shutdown. + Subsystem for Linux bug that caused tracebacks to appear during shutdown. * `#334 `_: the SSH method - tilde-expands private key paths using Ansible's logic. Previously Mitogen - passed the path unmodified to SSH, which would expand it using - :func:`os.getpwent`. - - This differs from :func:`os.path.expanduser`, which prefers the ``HOME`` + tilde-expands private key paths using Ansible's logic. Previously the path + was passed unmodified to SSH, which expanded it using :func:`os.getpwent`. + This differs from :func:`os.path.expanduser`, which uses the ``HOME`` environment variable if it is set, causing behaviour to diverge when Ansible - was invoked using sudo without appropriate flags to cause the ``HOME`` - environment variable to be reset to match the target account. + was invoked across user accounts via ``sudo``. * `#370 `_: the Ansible `reboot `_ module is supported. * `#373 `_: the LXC and LXD methods - now print a useful hint when Python fails to start, as no useful error is - normally logged to the console by these tools. + print a useful hint on failure, as no useful error is normally logged to the + console by these tools. * `#400 `_: work around a threading bug in the AWX display callback when running with high verbosity setting. @@ -195,21 +190,21 @@ Fixes Core Library ~~~~~~~~~~~~ -* `#76 `_: routing maintains the set - of destination context ID ever received on each stream, and when - disconnection occurs, propagates ``DEL_ROUTE`` messages downwards towards - every stream that ever communicated with a disappearing peer, rather than - simply toward parents. +* `#76 `_: routing records the + destination context IDs ever received on each stream, and when disconnection + occurs, propagates :data:`mitogen.core.DEL_ROUTE` messages towards every + stream that ever communicated with the disappearing peer, rather than simply + towards parents. - Conversations between nodes in any level of the tree receive ``DEL_ROUTE`` - messages when a participant disconnects, allowing receivers to be woken with - :class:`mitogen.core.ChannelError` to signal the connection has broken, even - when one participant is not a parent of the other. + Conversations between nodes anywhere in the tree receive + :data:`mitogen.core.DEL_ROUTE` when either participant disconnects, allowing + receivers to wake with :class:`mitogen.core.ChannelError`, even when one + participant is not a parent of the other. -* `#405 `_: if a message is rejected - due to being too large, and it has a ``reply_to`` set, a dead message is - returned to the sender. This ensures function calls exceeding the configured - maximum size crash rather than hang. +* `#405 `_: if an oversized message + is rejected, and it has a ``reply_to`` set, a dead message is returned to the + sender. This ensures function calls exceeding the configured maximum size + crash rather than hang. * `#411 `_: the SSH method typed "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` @@ -227,7 +222,7 @@ Thanks! ~~~~~~~ Mitogen would not be possible without the support of users. A huge thanks for -bug reports, features and fixes in this release contributed by +bug reports, testing, features and fixes in this release contributed by `Berend De Schouwer `_, `Brian Candler `_, `Duane Zamrok `_, @@ -441,7 +436,7 @@ Thanks! ~~~~~~~ Mitogen would not be possible without the support of users. A huge thanks for -bug reports, features and fixes in this release contributed by +bug reports, testing, features and fixes in this release contributed by `Alex Russu `_, `Alex Willmer `_, `atoom `_, From 16c364910a5c6db7dd5cd740d539e8f8a26c2574 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 15:09:40 +0000 Subject: [PATCH 121/662] core: avoid redundant write() calls in Waker.defer() Using _lock we can know for certain whether the Broker has received a wakeup byte yet. If it has, we can skip the wasted system call. Now on_receive() can exactly read the single byte that can possibly exist (modulo FD sharing bugs -- this could be improved on later) --- mitogen/core.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 2b1771a5..2ac9bdbe 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1945,17 +1945,14 @@ class Waker(BasicStream): def on_receive(self, broker): """ - Drain the pipe and fire callbacks. Reading multiple bytes is safe since - new bytes corresponding to future .defer() calls are written only after - .defer() takes _lock: either a byte we read corresponds to something - already on the queue by the time we take _lock, or a byte remains - buffered, causing another wake up, because it was written after we - released _lock. + Drain the pipe and fire callbacks. Since :attr:`_deferred` is + synchronized, :meth:`defer` and :meth:`on_receive` can conspire to + ensure only one byte needs to be pending regardless of queue length. """ _vv and IOLOG.debug('%r.on_receive()', self) - self.receive_side.read(128) self._lock.acquire() try: + self.receive_side.read(1) deferred = self._deferred self._deferred = [] finally: @@ -1969,6 +1966,18 @@ class Waker(BasicStream): func, args, kwargs) self._broker.shutdown() + def _wake(self): + """ + Wake the multiplexer by writing a byte. If Broker is midway through + teardown, the FD may already be closed, so ignore EBADF. + """ + try: + self.transmit_side.write(b(' ')) + except OSError: + e = sys.exc_info()[1] + if e.args[0] != errno.EBADF: + raise + def defer(self, func, *args, **kwargs): if threading.currentThread().ident == self.broker_ident: _vv and IOLOG.debug('%r.defer() [immediate]', self) @@ -1977,20 +1986,12 @@ class Waker(BasicStream): _vv and IOLOG.debug('%r.defer() [fd=%r]', self, self.transmit_side.fd) self._lock.acquire() try: + if not self._deferred: + self._wake() self._deferred.append((func, args, kwargs)) finally: self._lock.release() - # Wake the multiplexer by writing a byte. If the broker is in the midst - # of tearing itself down, the waker fd may already have been closed, so - # ignore EBADF here. - try: - self.transmit_side.write(b(' ')) - except OSError: - e = sys.exc_info()[1] - if e.args[0] != errno.EBADF: - raise - class IoLogger(BasicStream): """ From 87e8c45f760dbd036af7b575a492970eddd3d19d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 16:03:59 +0000 Subject: [PATCH 122/662] core: fix minify_test regression introduced in 804bacdadb14d60cf26808a64d0c6a70230ca643 The minifier can't handle empty function bodies, so the pass statements are necessary. --- mitogen/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mitogen/core.py b/mitogen/core.py index 2ac9bdbe..83efad42 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1284,6 +1284,7 @@ class BasicStream(object): called on them, and the method must call :meth:`on_disconect` if reading produces an empty string. """ + pass def on_transmit(self, broker): """ @@ -1295,6 +1296,7 @@ class BasicStream(object): Subclasses must implement this if :meth:`Broker._start_transmit` is ever called on them. """ + pass def on_shutdown(self, broker): """ From e4280dc14a783842249eef348f4dfef562f28383 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 17:03:05 +0000 Subject: [PATCH 123/662] core: Don't crash in Waker.__repr__ if partially initialized. --- mitogen/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 83efad42..9dec1eab 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1930,8 +1930,8 @@ class Waker(BasicStream): def __repr__(self): return 'Waker(%r rfd=%r, wfd=%r)' % ( self._broker, - self.receive_side.fd, - self.transmit_side.fd, + self.receive_side and self.receive_side.fd, + self.transmit_side and self.transmit_side.fd, ) @property From d1c2e7a834489874e71ed7999e8859d67d39dd30 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 17:03:27 +0000 Subject: [PATCH 124/662] issue #406: call Poller.close() during broker shutdown. --- mitogen/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mitogen/core.py b/mitogen/core.py index 9dec1eab..df4e54eb 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2491,6 +2491,8 @@ class Broker(object): LOG.error('_broker_main() force disconnecting %r', side) side.stream.on_disconnect(self) + self.poller.close() + def _broker_main(self): """ Handle events until :meth:`shutdown`. On shutdown, invoke From 4230a935577f043ce07136e70726b6e1edb30965 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 2 Nov 2018 17:05:22 +0000 Subject: [PATCH 125/662] issue #406: update Changelog. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5405f209..d9746375 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -206,6 +206,10 @@ Core Library sender. This ensures function calls exceeding the configured maximum size crash rather than hang. +* `#406 `_: + :class:`mitogen.core.Broker` did not call :meth:`mitogen.core.Poller.close` + during shutdown, leaking the underlying poller FD in masters and parents. + * `#411 `_: the SSH method typed "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` was configured. This would lead to connection timeouts due to the hung From e9a6e4c3d216b3c77878d141d4cecaf1ef570d8d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 12:39:29 +0000 Subject: [PATCH 126/662] issue #406: add test. --- tests/broker_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/broker_test.py b/tests/broker_test.py index 7d070e3d..c35e6161 100644 --- a/tests/broker_test.py +++ b/tests/broker_test.py @@ -1,6 +1,7 @@ import threading +import mock import unittest2 import testlib @@ -8,6 +9,19 @@ import testlib import mitogen.core +class ShutdownTest(testlib.TestCase): + klass = mitogen.core.Broker + + def test_poller_closed(self): + broker = self.klass() + actual_close = broker.poller.close + broker.poller.close = mock.Mock() + broker.shutdown() + broker.join() + self.assertEquals(1, len(broker.poller.close.mock_calls)) + actual_close() + + class DeferSyncTest(testlib.TestCase): klass = mitogen.core.Broker From 8a0b3437600aebf08f91e886cb272dc627bef3b8 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 13:28:37 +0000 Subject: [PATCH 127/662] issue #406: test for FD leak after every TestCase --- dev_requirements.txt | 1 + tests/call_error_test.py | 4 ++-- tests/docker_test.py | 2 +- tests/fakessh_test.py | 2 +- tests/fork_test.py | 5 +++-- tests/local_test.py | 4 ++-- tests/master_test.py | 2 +- tests/minify_test.py | 4 ++-- tests/parent_test.py | 8 ++++---- tests/responder_test.py | 8 ++++---- tests/router_test.py | 7 +++---- tests/serialization_test.py | 4 ++-- tests/ssh_test.py | 4 ++-- tests/testlib.py | 26 ++++++++++++++++++++++++++ tests/types_test.py | 6 ++++-- tests/unix_test.py | 6 +++--- tests/utils_test.py | 8 +++++--- 17 files changed, 66 insertions(+), 35 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index f48006e5..c536c154 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,5 @@ -r docs/docs-requirements.txt +psutil==5.4.8 coverage==4.5.1 Django==1.6.11 # Last version supporting 2.6. mock==2.0.0 diff --git a/tests/call_error_test.py b/tests/call_error_test.py index 447a80a9..1480a743 100644 --- a/tests/call_error_test.py +++ b/tests/call_error_test.py @@ -10,7 +10,7 @@ import testlib import plain_old_module -class ConstructorTest(unittest2.TestCase): +class ConstructorTest(testlib.TestCase): klass = mitogen.core.CallError def test_string_noargs(self): @@ -44,7 +44,7 @@ class ConstructorTest(unittest2.TestCase): self.assertTrue('test_from_exc_tb' in e.args[0]) -class PickleTest(unittest2.TestCase): +class PickleTest(testlib.TestCase): klass = mitogen.core.CallError def test_string_noargs(self): diff --git a/tests/docker_test.py b/tests/docker_test.py index 2d45609a..49c742ee 100644 --- a/tests/docker_test.py +++ b/tests/docker_test.py @@ -7,7 +7,7 @@ import unittest2 import testlib -class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): def test_okay(self): docker_path = testlib.data_path('stubs/stub-docker.py') context = self.router.docker( diff --git a/tests/fakessh_test.py b/tests/fakessh_test.py index c584acfe..63c70058 100644 --- a/tests/fakessh_test.py +++ b/tests/fakessh_test.py @@ -10,7 +10,7 @@ import mitogen.fakessh import testlib -class RsyncTest(testlib.DockerMixin, unittest2.TestCase): +class RsyncTest(testlib.DockerMixin, testlib.TestCase): @timeoutcontext.timeout(5) @unittest2.skip('broken') def test_rsync_from_master(self): diff --git a/tests/fork_test.py b/tests/fork_test.py index 8b396bbf..5e457c97 100644 --- a/tests/fork_test.py +++ b/tests/fork_test.py @@ -55,7 +55,7 @@ def exercise_importer(n): return simple_pkg.a.subtract_one_add_two(n) -class ForkTest(testlib.RouterMixin, unittest2.TestCase): +class ForkTest(testlib.RouterMixin, testlib.TestCase): def test_okay(self): context = self.router.fork() self.assertNotEqual(context.call(os.getpid), os.getpid()) @@ -84,7 +84,8 @@ class ForkTest(testlib.RouterMixin, unittest2.TestCase): context = self.router.fork(on_start=on_start) self.assertEquals(123, recv.get().unpickle()) -class DoubleChildTest(testlib.RouterMixin, unittest2.TestCase): + +class DoubleChildTest(testlib.RouterMixin, testlib.TestCase): def test_okay(self): # When forking from the master process, Mitogen had nothing to do with # setting up stdio -- that was inherited wherever the Master is running diff --git a/tests/local_test.py b/tests/local_test.py index fbf5c1c8..5a620e52 100644 --- a/tests/local_test.py +++ b/tests/local_test.py @@ -20,7 +20,7 @@ def get_os_environ(): return dict(os.environ) -class LocalTest(testlib.RouterMixin, unittest2.TestCase): +class LocalTest(testlib.RouterMixin, testlib.TestCase): stream_class = mitogen.ssh.Stream def test_stream_name(self): @@ -29,7 +29,7 @@ class LocalTest(testlib.RouterMixin, unittest2.TestCase): self.assertEquals('local.%d' % (pid,), context.name) -class PythonPathTest(testlib.RouterMixin, unittest2.TestCase): +class PythonPathTest(testlib.RouterMixin, testlib.TestCase): stream_class = mitogen.ssh.Stream def test_inherited(self): diff --git a/tests/master_test.py b/tests/master_test.py index 19a9b414..31d11013 100644 --- a/tests/master_test.py +++ b/tests/master_test.py @@ -6,7 +6,7 @@ import testlib import mitogen.master -class ScanCodeImportsTest(unittest2.TestCase): +class ScanCodeImportsTest(testlib.TestCase): func = staticmethod(mitogen.master.scan_code_imports) if mitogen.core.PY3: diff --git a/tests/minify_test.py b/tests/minify_test.py index 98307059..e990fb90 100644 --- a/tests/minify_test.py +++ b/tests/minify_test.py @@ -16,7 +16,7 @@ def read_sample(fname): return sample -class MinimizeSourceTest(unittest2.TestCase): +class MinimizeSourceTest(testlib.TestCase): func = staticmethod(mitogen.minify.minimize_source) def test_class(self): @@ -55,7 +55,7 @@ class MinimizeSourceTest(unittest2.TestCase): self.assertEqual(expected, self.func(original)) -class MitogenCoreTest(unittest2.TestCase): +class MitogenCoreTest(testlib.TestCase): # Verify minimize_source() succeeds for all built-in modules. func = staticmethod(mitogen.minify.minimize_source) diff --git a/tests/parent_test.py b/tests/parent_test.py index 9d540ccc..e6a93deb 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -153,7 +153,7 @@ class StreamErrorTest(testlib.RouterMixin, testlib.TestCase): self.assertTrue(s in e.args[0]) -class ContextTest(testlib.RouterMixin, unittest2.TestCase): +class ContextTest(testlib.RouterMixin, testlib.TestCase): def test_context_shutdown(self): local = self.router.local() pid = local.call(os.getpid) @@ -181,7 +181,7 @@ class OpenPtyTest(testlib.TestCase): self.assertEquals(e.args[0], msg) -class TtyCreateChildTest(unittest2.TestCase): +class TtyCreateChildTest(testlib.TestCase): func = staticmethod(mitogen.parent.tty_create_child) def test_dev_tty_open_succeeds(self): @@ -211,7 +211,7 @@ class TtyCreateChildTest(unittest2.TestCase): tf.close() -class IterReadTest(unittest2.TestCase): +class IterReadTest(testlib.TestCase): func = staticmethod(mitogen.parent.iter_read) def make_proc(self): @@ -263,7 +263,7 @@ class IterReadTest(unittest2.TestCase): proc.terminate() -class WriteAllTest(unittest2.TestCase): +class WriteAllTest(testlib.TestCase): func = staticmethod(mitogen.parent.write_all) def make_proc(self): diff --git a/tests/responder_test.py b/tests/responder_test.py index 46400fce..888302c0 100644 --- a/tests/responder_test.py +++ b/tests/responder_test.py @@ -13,7 +13,7 @@ import plain_old_module import simple_pkg.a -class NeutralizeMainTest(testlib.RouterMixin, unittest2.TestCase): +class NeutralizeMainTest(testlib.RouterMixin, testlib.TestCase): klass = mitogen.master.ModuleResponder def call(self, *args, **kwargs): @@ -67,7 +67,7 @@ class NeutralizeMainTest(testlib.RouterMixin, unittest2.TestCase): -class GoodModulesTest(testlib.RouterMixin, unittest2.TestCase): +class GoodModulesTest(testlib.RouterMixin, testlib.TestCase): def test_plain_old_module(self): # The simplest case: a top-level module with no interesting imports or # package machinery damage. @@ -89,7 +89,7 @@ class GoodModulesTest(testlib.RouterMixin, unittest2.TestCase): self.assertEquals(output, "['__main__', 50]\n") -class BrokenModulesTest(unittest2.TestCase): +class BrokenModulesTest(testlib.TestCase): def test_obviously_missing(self): # Ensure we don't crash in the case of a module legitimately being # unavailable. Should never happen in the real world. @@ -144,7 +144,7 @@ class BrokenModulesTest(unittest2.TestCase): self.assertIsInstance(msg.unpickle(), tuple) -class BlacklistTest(unittest2.TestCase): +class BlacklistTest(testlib.TestCase): @unittest2.skip('implement me') def test_whitelist_no_blacklist(self): assert 0 diff --git a/tests/router_test.py b/tests/router_test.py index 7b7e2896..b0add6d3 100644 --- a/tests/router_test.py +++ b/tests/router_test.py @@ -36,7 +36,7 @@ def send_n_sized_reply(sender, n): return 123 -class SourceVerifyTest(testlib.RouterMixin, unittest2.TestCase): +class SourceVerifyTest(testlib.RouterMixin, testlib.TestCase): def setUp(self): super(SourceVerifyTest, self).setUp() # Create some children, ping them, and store what their messages look @@ -149,7 +149,7 @@ class PolicyTest(testlib.RouterMixin, testlib.TestCase): self.assertEquals(e.args[0], self.router.refused_msg) -class CrashTest(testlib.BrokerMixin, unittest2.TestCase): +class CrashTest(testlib.BrokerMixin, testlib.TestCase): # This is testing both Broker's ability to crash nicely, and Router's # ability to respond to the crash event. klass = mitogen.master.Router @@ -178,8 +178,7 @@ class CrashTest(testlib.BrokerMixin, unittest2.TestCase): self.assertTrue(expect in log.stop()) - -class AddHandlerTest(unittest2.TestCase): +class AddHandlerTest(testlib.TestCase): klass = mitogen.master.Router def test_invoked_at_shutdown(self): diff --git a/tests/serialization_test.py b/tests/serialization_test.py index f108ff37..d8c54c59 100644 --- a/tests/serialization_test.py +++ b/tests/serialization_test.py @@ -20,7 +20,7 @@ def roundtrip(v): return mitogen.core.Message(data=msg.data).unpickle() -class BlobTest(unittest2.TestCase): +class BlobTest(testlib.TestCase): klass = mitogen.core.Blob # Python 3 pickle protocol 2 does weird stuff depending on whether an empty @@ -36,7 +36,7 @@ class BlobTest(unittest2.TestCase): self.assertEquals(b(''), roundtrip(v)) -class ContextTest(testlib.RouterMixin, unittest2.TestCase): +class ContextTest(testlib.RouterMixin, testlib.TestCase): klass = mitogen.core.Context # Ensure Context can be round-tripped by regular pickle in addition to diff --git a/tests/ssh_test.py b/tests/ssh_test.py index 36359a66..661ff5ed 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -29,7 +29,7 @@ class StubSshMixin(testlib.RouterMixin): del os.environ['STUBSSH_MODE'] -class ConstructorTest(testlib.RouterMixin, unittest2.TestCase): +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): def test_okay(self): context = self.router.ssh( hostname='hostname', @@ -165,7 +165,7 @@ class SshTest(testlib.DockerMixin, testlib.TestCase): fp.close() -class BannerTest(testlib.DockerMixin, unittest2.TestCase): +class BannerTest(testlib.DockerMixin, testlib.TestCase): # Verify the ability to disambiguate random spam appearing in the SSHd's # login banner from a legitimate password prompt. stream_class = mitogen.ssh.Stream diff --git a/tests/testlib.py b/tests/testlib.py index 8f11337d..605254d3 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -8,9 +8,11 @@ import subprocess import sys import time +import psutil import unittest2 import mitogen.core +import mitogen.fork import mitogen.master import mitogen.utils @@ -41,6 +43,13 @@ if faulthandler is not None: faulthandler.enable() +def get_fd_count(): + """ + Return the number of FDs open by this process. + """ + return psutil.Process().num_fds() + + def data_path(suffix): path = os.path.join(DATA_DIR, suffix) if path.endswith('.key'): @@ -211,6 +220,23 @@ class LogCapturer(object): class TestCase(unittest2.TestCase): + @classmethod + def setUpClass(cls): + # This is done in setUpClass() so we have a chance to run before any + # Broker() instantiations in setUp() etc. + mitogen.fork.on_fork() + cls._fd_count_before = get_fd_count() + super(TestCase, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + super(TestCase, cls).tearDownClass() + mitogen.fork.on_fork() + assert get_fd_count() == cls._fd_count_before, \ + "%s leaked FDs. Count before: %s, after: %s" % ( + cls, cls._fd_count_before, get_fd_count(), + ) + def assertRaises(self, exc, func, *args, **kwargs): """Like regular assertRaises, except return the exception that was raised. Can't use context manager because tests must run on Python2.4""" diff --git a/tests/types_test.py b/tests/types_test.py index 4f80e076..f929c098 100644 --- a/tests/types_test.py +++ b/tests/types_test.py @@ -11,8 +11,10 @@ import unittest2 import mitogen.core from mitogen.core import b +import testlib -class BlobTest(unittest2.TestCase): + +class BlobTest(testlib.TestCase): klass = mitogen.core.Blob def make(self): @@ -43,7 +45,7 @@ class BlobTest(unittest2.TestCase): mitogen.core.BytesType(blob2)) -class SecretTest(unittest2.TestCase): +class SecretTest(testlib.TestCase): klass = mitogen.core.Secret def make(self): diff --git a/tests/unix_test.py b/tests/unix_test.py index 67265c81..f837c6f0 100644 --- a/tests/unix_test.py +++ b/tests/unix_test.py @@ -30,7 +30,7 @@ class MyService(mitogen.service.Service): } -class IsPathDeadTest(unittest2.TestCase): +class IsPathDeadTest(testlib.TestCase): func = staticmethod(mitogen.unix.is_path_dead) path = '/tmp/stale-socket' @@ -57,7 +57,7 @@ class IsPathDeadTest(unittest2.TestCase): os.unlink(self.path) -class ListenerTest(testlib.RouterMixin, unittest2.TestCase): +class ListenerTest(testlib.RouterMixin, testlib.TestCase): klass = mitogen.unix.Listener def test_constructor_basic(self): @@ -66,7 +66,7 @@ class ListenerTest(testlib.RouterMixin, unittest2.TestCase): os.unlink(listener.path) -class ClientTest(unittest2.TestCase): +class ClientTest(testlib.TestCase): klass = mitogen.unix.Listener def _try_connect(self, path): diff --git a/tests/utils_test.py b/tests/utils_test.py index b2e0aa9e..5b81289e 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -6,6 +6,8 @@ import mitogen.core import mitogen.master import mitogen.utils +import testlib + def func0(router): return router @@ -16,7 +18,7 @@ def func(router): return router -class RunWithRouterTest(unittest2.TestCase): +class RunWithRouterTest(testlib.TestCase): # test_shutdown_on_exception # test_shutdown_on_success @@ -26,7 +28,7 @@ class RunWithRouterTest(unittest2.TestCase): self.assertFalse(router.broker._thread.isAlive()) -class WithRouterTest(unittest2.TestCase): +class WithRouterTest(testlib.TestCase): def test_with_broker(self): router = func() self.assertIsInstance(router, mitogen.master.Router) @@ -40,7 +42,7 @@ class Unicode(mitogen.core.UnicodeType): pass class Bytes(mitogen.core.BytesType): pass -class CastTest(unittest2.TestCase): +class CastTest(testlib.TestCase): def test_dict(self): self.assertEqual(type(mitogen.utils.cast({})), dict) self.assertEqual(type(mitogen.utils.cast(Dict())), dict) From 9b3cb55a8bf620a90470caeb001530557e5ff26c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 15:01:45 +0000 Subject: [PATCH 128/662] issue #4096: import log_fd_calls() helper. --- tests/testlib.py | 49 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/tests/testlib.py b/tests/testlib.py index 605254d3..7c376135 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -6,7 +6,9 @@ import re import socket import subprocess import sys +import threading import time +import traceback import psutil import unittest2 @@ -175,6 +177,46 @@ def sync_with_broker(broker, timeout=10.0): sem.get(timeout=10.0) +def log_fd_calls(): + mypid = os.getpid() + l = threading.Lock() + real_pipe = os.pipe + def pipe(): + with l: + rv = real_pipe() + if mypid == os.getpid(): + print + print rv + traceback.print_stack(limit=3) + print + return rv + + real_dup2 = os.dup2 + def dup2(*args): + with l: + real_dup2(*args) + if mypid == os.getpid(): + print + print '--', args + traceback.print_stack(limit=3) + print + + real_dup = os.dup + def dup(*args): + with l: + rc = real_dup(*args) + if mypid == os.getpid(): + print + print '--', args, '->', rv + traceback.print_stack(limit=3) + print + return rv + + os.pipe = pipe + os.dup = dup + os.dup2 = dup2 + + class CaptureStreamHandler(logging.StreamHandler): def __init__(self, *args, **kwargs): logging.StreamHandler.__init__(self, *args, **kwargs) @@ -232,10 +274,11 @@ class TestCase(unittest2.TestCase): def tearDownClass(cls): super(TestCase, cls).tearDownClass() mitogen.fork.on_fork() - assert get_fd_count() == cls._fd_count_before, \ - "%s leaked FDs. Count before: %s, after: %s" % ( + if get_fd_count() != cls._fd_count_before: + import os; os.system('lsof -p %s' % (os.getpid(),)) + assert 0, "%s leaked FDs. Count before: %s, after: %s" % ( cls, cls._fd_count_before, get_fd_count(), - ) + ) def assertRaises(self, exc, func, *args, **kwargs): """Like regular assertRaises, except return the exception that was From 70c550f50c7c0918a70c9baf080573e9d0a1b232 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 15:02:13 +0000 Subject: [PATCH 129/662] issue #406: close stdout pipes in parent_test --- tests/parent_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/parent_test.py b/tests/parent_test.py index e6a93deb..c4921c1f 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -230,6 +230,7 @@ class IterReadTest(testlib.TestCase): break finally: proc.terminate() + proc.stdout.close() def test_deadline_exceeded_before_call(self): proc = self.make_proc() @@ -244,6 +245,7 @@ class IterReadTest(testlib.TestCase): self.assertEqual(len(got), 0) finally: proc.terminate() + proc.stdout.close() def test_deadline_exceeded_during_call(self): proc = self.make_proc() @@ -261,6 +263,7 @@ class IterReadTest(testlib.TestCase): self.assertLess(len(got), 5) finally: proc.terminate() + proc.stdout.close() class WriteAllTest(testlib.TestCase): @@ -280,6 +283,7 @@ class WriteAllTest(testlib.TestCase): self.func(proc.stdin.fileno(), self.ten_ms_chunk) finally: proc.terminate() + proc.stdout.close() def test_deadline_exceeded_before_call(self): proc = self.make_proc() @@ -289,6 +293,7 @@ class WriteAllTest(testlib.TestCase): )) finally: proc.terminate() + proc.stdout.close() def test_deadline_exceeded_during_call(self): proc = self.make_proc() @@ -301,6 +306,7 @@ class WriteAllTest(testlib.TestCase): )) finally: proc.terminate() + proc.stdout.close() class DisconnectTest(testlib.RouterMixin, testlib.TestCase): From 6ff1e001da9d829db518c1cecd1b6604433254b4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 15:48:31 +0000 Subject: [PATCH 130/662] issue #406: log socketpair calls too. --- tests/testlib.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/testlib.py b/tests/testlib.py index 7c376135..8d2bd8d9 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -190,6 +190,19 @@ def log_fd_calls(): traceback.print_stack(limit=3) print return rv + os.pipe = pipe + + real_socketpair = socket.socketpair + def socketpair(*args): + with l: + rv = real_socketpair(*args) + if mypid == os.getpid(): + print + print '--', args, '->', rv + traceback.print_stack(limit=3) + print + return rv + socket.socketpair = socketpair real_dup2 = os.dup2 def dup2(*args): @@ -200,6 +213,7 @@ def log_fd_calls(): print '--', args traceback.print_stack(limit=3) print + os.dup2 = dup2 real_dup = os.dup def dup(*args): @@ -211,10 +225,7 @@ def log_fd_calls(): traceback.print_stack(limit=3) print return rv - - os.pipe = pipe os.dup = dup - os.dup2 = dup2 class CaptureStreamHandler(logging.StreamHandler): From 14b389cb467d4c3eb5715b0c8be98922c5a027cc Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 15:49:52 +0000 Subject: [PATCH 131/662] issue #406: don't leak FDs on failed child start. --- docs/changelog.rst | 3 +++ mitogen/parent.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d9746375..781c42b9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -210,6 +210,9 @@ Core Library :class:`mitogen.core.Broker` did not call :meth:`mitogen.core.Poller.close` during shutdown, leaking the underlying poller FD in masters and parents. +* `#406 `_: connections could leak + FDs when a child process failed to start. + * `#411 `_: the SSH method typed "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` was configured. This would lead to connection timeouts due to the hung diff --git a/mitogen/parent.py b/mitogen/parent.py index 780cecd7..3e30f475 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -211,7 +211,7 @@ def create_socketpair(): return parentfp, childfp -def detach_popen(*args, **kwargs): +def detach_popen(close_on_error=None, **kwargs): """ Use :class:`subprocess.Popen` to construct a child process, then hack the Popen so that it forgets the child it created, allowing it to survive a @@ -223,6 +223,8 @@ def detach_popen(*args, **kwargs): delivered to this process, causing later 'legitimate' calls to fail with ECHILD. + :param list close_on_error: + Array of integer file descriptors to close on exception. :returns: Process ID of the new child. """ @@ -230,7 +232,13 @@ def detach_popen(*args, **kwargs): # handling, without tying the surrounding code into managing a Popen # object, which isn't possible for at least :mod:`mitogen.fork`. This # should be replaced by a swappable helper class in a future version. - proc = subprocess.Popen(*args, **kwargs) + try: + proc = subprocess.Popen(**kwargs) + except Exception: + for fd in close_on_error or (): + os.close(fd) + raise + proc._child_created = False return proc.pid @@ -277,6 +285,7 @@ def create_child(args, merge_stdio=False, stderr_pipe=False, preexec_fn=None): stdout=childfp, close_fds=True, preexec_fn=preexec_fn, + close_on_error=[parentfp.fileno(), childfp.fileno()], **extra ) if stderr_pipe: @@ -344,6 +353,7 @@ def tty_create_child(args): stdout=slave_fd, stderr=slave_fd, preexec_fn=_acquire_controlling_tty, + close_on_error=[master_fd, slave_fd], close_fds=True, ) @@ -372,6 +382,7 @@ def hybrid_tty_create_child(args): mitogen.core.set_block(childfp) disable_echo(master_fd) disable_echo(slave_fd) + pid = detach_popen( args=args, stdin=childfp, @@ -379,6 +390,7 @@ def hybrid_tty_create_child(args): stderr=slave_fd, preexec_fn=_acquire_controlling_tty, close_fds=True, + close_on_error=[master_fd, slave_fd, parentfp.fileno(), childfp.fileno()], ) os.close(slave_fd) From 375182b71b7705d85290faf24d011242c427fc2a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 19:48:36 +0000 Subject: [PATCH 132/662] issue #406: don't leak side FDs on bootstrap failure. --- mitogen/parent.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mitogen/parent.py b/mitogen/parent.py index 3e30f475..019ee917 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1152,10 +1152,14 @@ class Stream(mitogen.core.Stream): try: self._connect_bootstrap(extra_fd) except EofError: + self.receive_side.close() + self.transmit_side.close() e = sys.exc_info()[1] self._adorn_eof_error(e) raise except Exception: + self.receive_side.close() + self.transmit_side.close() self._reap_child() raise From b0dd628f07f0b96a2b2b29d5b5e9505cc3874d80 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 19:49:13 +0000 Subject: [PATCH 133/662] issue #406: parent_test fixes, NameError in log_fd_calls(). --- tests/parent_test.py | 7 ++++--- tests/testlib.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/parent_test.py b/tests/parent_test.py index c4921c1f..e83d6f1a 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -207,6 +207,7 @@ class TtyCreateChildTest(testlib.TestCase): self.assertEquals(pid, waited_pid) self.assertEquals(0, status) self.assertEquals(mitogen.core.b(''), tf.read()) + os.close(fd) finally: tf.close() @@ -283,7 +284,7 @@ class WriteAllTest(testlib.TestCase): self.func(proc.stdin.fileno(), self.ten_ms_chunk) finally: proc.terminate() - proc.stdout.close() + proc.stdin.close() def test_deadline_exceeded_before_call(self): proc = self.make_proc() @@ -293,7 +294,7 @@ class WriteAllTest(testlib.TestCase): )) finally: proc.terminate() - proc.stdout.close() + proc.stdin.close() def test_deadline_exceeded_during_call(self): proc = self.make_proc() @@ -306,7 +307,7 @@ class WriteAllTest(testlib.TestCase): )) finally: proc.terminate() - proc.stdout.close() + proc.stdin.close() class DisconnectTest(testlib.RouterMixin, testlib.TestCase): diff --git a/tests/testlib.py b/tests/testlib.py index 8d2bd8d9..0f9a405a 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -218,7 +218,7 @@ def log_fd_calls(): real_dup = os.dup def dup(*args): with l: - rc = real_dup(*args) + rv = real_dup(*args) if mypid == os.getpid(): print print '--', args, '->', rv From 3da4b1a4203f330942fe0fde8f60b10287f64c16 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 20:13:11 +0000 Subject: [PATCH 134/662] tests: verify only main/watcher threads exist at teardown --- tests/testlib.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/testlib.py b/tests/testlib.py index 0f9a405a..3846f470 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -281,16 +281,38 @@ class TestCase(unittest2.TestCase): cls._fd_count_before = get_fd_count() super(TestCase, cls).setUpClass() + ALLOWED_THREADS = set([ + 'MainThread', + 'mitogen.master.join_thread_async' + ]) + @classmethod - def tearDownClass(cls): - super(TestCase, cls).tearDownClass() - mitogen.fork.on_fork() + def _teardown_check_threads(cls): + counts = {} + for thread in threading.enumerate(): + assert thread.name in cls.ALLOWED_THREADS, \ + 'Found thread %r still running after tests.' % (thread.name,) + counts[thread.name] = counts.get(thread.name, 0) + 1 + + for name in counts: + assert counts[name] == 1, \ + 'Found %d copies of thread %r running after tests.' % (name,) + + @classmethod + def _teardown_check_fds(cls): + mitogen.core.Latch._on_fork() if get_fd_count() != cls._fd_count_before: import os; os.system('lsof -p %s' % (os.getpid(),)) assert 0, "%s leaked FDs. Count before: %s, after: %s" % ( cls, cls._fd_count_before, get_fd_count(), ) + @classmethod + def tearDownClass(cls): + super(TestCase, cls).tearDownClass() + cls._teardown_check_threads() + cls._teardown_check_fds() + def assertRaises(self, exc, func, *args, **kwargs): """Like regular assertRaises, except return the exception that was raised. Can't use context manager because tests must run on Python2.4""" From 175fc377d267cd8308cebfbb2b6e02985bd51f8e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 20:38:39 +0000 Subject: [PATCH 135/662] tests: remove hard-wired SSL paths from fork_test. --- tests/fork_test.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/tests/fork_test.py b/tests/fork_test.py index 5e457c97..dd214bd1 100644 --- a/tests/fork_test.py +++ b/tests/fork_test.py @@ -1,4 +1,5 @@ +import _ssl import ctypes import os import random @@ -13,21 +14,29 @@ import testlib import plain_old_module -IS_64BIT = struct.calcsize('P') == 8 -PLATFORM_TO_PATH = { - ('darwin', False): '/usr/lib/libssl.dylib', - ('darwin', True): '/usr/lib/libssl.dylib', - ('linux2', False): '/usr/lib/libssl.so', - ('linux2', True): '/usr/lib/x86_64-linux-gnu/libssl.so', - # Python 2.6 - ('linux3', False): '/usr/lib/libssl.so', - ('linux3', True): '/usr/lib/x86_64-linux-gnu/libssl.so', - # Python 3 - ('linux', False): '/usr/lib/libssl.so', - ('linux', True): '/usr/lib/x86_64-linux-gnu/libssl.so', -} - -c_ssl = ctypes.CDLL(PLATFORM_TO_PATH[sys.platform, IS_64BIT]) +def _find_ssl_linux(): + s = testlib.subprocess__check_output(['ldd', _ssl.__file__]) + for line in s.splitlines(): + bits = line.split() + if bits[0].startswith('libssl'): + return bits[2] + +def _find_ssl_darwin(): + s = testlib.subprocess__check_output(['otool', '-l', _ssl.__file__]) + for line in s.splitlines(): + bits = line.split() + if bits[0] == 'name' and 'libssl' in bits[1]: + return bits[1] + + +if sys.platform.startswith('linux'): + LIBSSL_PATH = _find_ssl_linux() +elif sys.platform == 'darwin': + LIBSSL_PATH = _find_ssl_darwin() +else: + assert 0, "Don't know how to find libssl on this platform" + +c_ssl = ctypes.CDLL(LIBSSL_PATH) c_ssl.RAND_pseudo_bytes.argtypes = [ctypes.c_char_p, ctypes.c_int] c_ssl.RAND_pseudo_bytes.restype = ctypes.c_int From 10af266678f16e7404e025b727934a975b664863 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 20:54:05 +0000 Subject: [PATCH 136/662] issue #406: attempt Broker cleanup in case of a crash. --- mitogen/core.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index df4e54eb..5b1d5298 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2473,6 +2473,13 @@ class Broker(object): for (side, func) in self.poller.poll(timeout): self._call(side.stream, func) + def _broker_exit(self): + for _, (side, _) in self.poller.readers + self.poller.writers: + LOG.error('_broker_main() force disconnecting %r', side) + side.stream.on_disconnect(self) + + self.poller.close() + def _broker_shutdown(self): for _, (side, _) in self.poller.readers + self.poller.writers: self._call(side.stream, side.stream.on_shutdown) @@ -2487,12 +2494,6 @@ class Broker(object): 'more child processes still connected to ' 'our stdout/stderr pipes.', self) - for _, (side, _) in self.poller.readers + self.poller.writers: - LOG.error('_broker_main() force disconnecting %r', side) - side.stream.on_disconnect(self) - - self.poller.close() - def _broker_main(self): """ Handle events until :meth:`shutdown`. On shutdown, invoke @@ -2509,6 +2510,7 @@ class Broker(object): except Exception: LOG.exception('_broker_main() crashed') + self._broker_exit() fire(self, 'exit') def shutdown(self): From 802efa6ea656cbe9f762dc80ada37f09332862f6 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sat, 3 Nov 2018 20:55:30 +0000 Subject: [PATCH 137/662] issue #406: ensure broker_test waits for broker exit. --- tests/broker_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/broker_test.py b/tests/broker_test.py index c35e6161..7890b0f3 100644 --- a/tests/broker_test.py +++ b/tests/broker_test.py @@ -32,6 +32,7 @@ class DeferSyncTest(testlib.TestCase): self.assertEquals(th, broker._thread) finally: broker.shutdown() + broker.join() def test_exception(self): broker = self.klass() @@ -40,6 +41,7 @@ class DeferSyncTest(testlib.TestCase): broker.defer_sync, lambda: int('dave')) finally: broker.shutdown() + broker.join() if __name__ == '__main__': From eae1bdba4e929bca59a8cd934d3e054d44feba6f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 01:48:04 +0000 Subject: [PATCH 138/662] tests: make minify_test print something useful on failure --- tests/minify_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/minify_test.py b/tests/minify_test.py index e990fb90..d1161c90 100644 --- a/tests/minify_test.py +++ b/tests/minify_test.py @@ -95,7 +95,11 @@ class MitogenCoreTest(testlib.TestCase): def test_minify_all(self): for name in glob.glob('mitogen/*.py'): original = self.read_source(name) - minified = self.func(original) + try: + minified = self.func(original) + except Exception: + print('file was: ' + name) + raise self._test_syntax_valid(minified, name) self._test_line_counts_match(original, minified) From b3841317dd89ffb8d015929318c484a8e2ef7f41 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 01:49:10 +0000 Subject: [PATCH 139/662] issue #406: clean up FDs on failure explicitly The previous approach was crap since it left e.g. socketpair instances lying around for GC with their underlying FD already closed, coupled with FD number reuse, led to random madness when GC finally runs. --- mitogen/parent.py | 79 ++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/mitogen/parent.py b/mitogen/parent.py index 019ee917..e36167d4 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -211,7 +211,7 @@ def create_socketpair(): return parentfp, childfp -def detach_popen(close_on_error=None, **kwargs): +def detach_popen(**kwargs): """ Use :class:`subprocess.Popen` to construct a child process, then hack the Popen so that it forgets the child it created, allowing it to survive a @@ -232,13 +232,7 @@ def detach_popen(close_on_error=None, **kwargs): # handling, without tying the surrounding code into managing a Popen # object, which isn't possible for at least :mod:`mitogen.fork`. This # should be replaced by a swappable helper class in a future version. - try: - proc = subprocess.Popen(**kwargs) - except Exception: - for fd in close_on_error or (): - os.close(fd) - raise - + proc = subprocess.Popen(**kwargs) proc._child_created = False return proc.pid @@ -279,15 +273,20 @@ def create_child(args, merge_stdio=False, stderr_pipe=False, preexec_fn=None): mitogen.core.set_cloexec(stderr_w) extra = {'stderr': stderr_w} - pid = detach_popen( - args=args, - stdin=childfp, - stdout=childfp, - close_fds=True, - preexec_fn=preexec_fn, - close_on_error=[parentfp.fileno(), childfp.fileno()], - **extra - ) + try: + pid = detach_popen( + args=args, + stdin=childfp, + stdout=childfp, + close_fds=True, + preexec_fn=preexec_fn, + **extra + ) + except Exception: + childfp.close() + parentfp.close() + raise + if stderr_pipe: os.close(stderr_w) childfp.close() @@ -347,15 +346,19 @@ def tty_create_child(args): disable_echo(master_fd) disable_echo(slave_fd) - pid = detach_popen( - args=args, - stdin=slave_fd, - stdout=slave_fd, - stderr=slave_fd, - preexec_fn=_acquire_controlling_tty, - close_on_error=[master_fd, slave_fd], - close_fds=True, - ) + try: + pid = detach_popen( + args=args, + stdin=slave_fd, + stdout=slave_fd, + stderr=slave_fd, + preexec_fn=_acquire_controlling_tty, + close_fds=True, + ) + except Exception: + os.close(master_fd) + os.close(slave_fd) + raise os.close(slave_fd) LOG.debug('tty_create_child() child %d fd %d, parent %d, cmd: %s', @@ -383,15 +386,21 @@ def hybrid_tty_create_child(args): disable_echo(master_fd) disable_echo(slave_fd) - pid = detach_popen( - args=args, - stdin=childfp, - stdout=childfp, - stderr=slave_fd, - preexec_fn=_acquire_controlling_tty, - close_fds=True, - close_on_error=[master_fd, slave_fd, parentfp.fileno(), childfp.fileno()], - ) + try: + pid = detach_popen( + args=args, + stdin=childfp, + stdout=childfp, + stderr=slave_fd, + preexec_fn=_acquire_controlling_tty, + close_fds=True, + ) + except Exception: + os.close(master_fd) + os.close(slave_fd) + parentfp.close() + childfp.close() + raise os.close(slave_fd) childfp.close() From 17631b0573092807d29bf7c4e4cdfa4c713c64cf Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 01:50:19 +0000 Subject: [PATCH 140/662] issue #406: parent: close extra_fd on failure too. --- mitogen/parent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mitogen/parent.py b/mitogen/parent.py index e36167d4..f2dacb09 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1169,6 +1169,8 @@ class Stream(mitogen.core.Stream): except Exception: self.receive_side.close() self.transmit_side.close() + if extra_fd is not None: + os.close(extra_fd) self._reap_child() raise From 003526ef7bd0b4ca65e35df35e0d725709f69981 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 01:54:50 +0000 Subject: [PATCH 141/662] issue #406: fix thread leaks in unix_test too. --- tests/unix_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unix_test.py b/tests/unix_test.py index f837c6f0..ee9499ba 100644 --- a/tests/unix_test.py +++ b/tests/unix_test.py @@ -87,6 +87,8 @@ class ClientTest(testlib.TestCase): resp = context.call_service(service_name=MyService, method_name='ping') self.assertEquals(mitogen.context_id, resp['src_id']) self.assertEquals(0, resp['auth_id']) + router.broker.shutdown() + router.broker.join() def _test_simple_server(self, path): router = mitogen.master.Router() @@ -102,7 +104,9 @@ class ClientTest(testlib.TestCase): time.sleep(0.1) finally: pool.shutdown() + pool.join() router.broker.shutdown() + router.broker.join() finally: os._exit(0) From dc3db49c5a24ed5b251d4a4a7a0d5e37549e8af3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 02:35:56 +0000 Subject: [PATCH 142/662] issue #406: more leaked FDs when create_child() fails. --- mitogen/parent.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mitogen/parent.py b/mitogen/parent.py index f2dacb09..556f38c6 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -285,6 +285,9 @@ def create_child(args, merge_stdio=False, stderr_pipe=False, preexec_fn=None): except Exception: childfp.close() parentfp.close() + if stderr_pipe: + os.close(stderr_r) + os.close(stderr_w) raise if stderr_pipe: From 411af6c1675330214b00baeef32614e366196621 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 11:25:22 +0000 Subject: [PATCH 143/662] issue #406: unix: don't leak already-closed socket object if Side.close() closes the socket (which it does), and it gets reused, GC will cause socketobject.__del__ to later delete some random FD. --- mitogen/unix.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mitogen/unix.py b/mitogen/unix.py index 417842bc..cc2d92ff 100644 --- a/mitogen/unix.py +++ b/mitogen/unix.py @@ -78,6 +78,11 @@ class Listener(mitogen.core.BasicStream): self.receive_side = mitogen.core.Side(self, self._sock.fileno()) router.broker.start_receive(self) + def on_shutdown(self, broker): + self._sock.close() + self.receive_side.closed = True + broker.stop_receive(self) + def _accept_client(self, sock): sock.setblocking(True) try: From 661e274556c874d5652c15ab58a4f1c6fa6e68d9 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 11:49:24 +0000 Subject: [PATCH 144/662] issue #406: ensure is_path_dead() socket is finalized. --- mitogen/unix.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mitogen/unix.py b/mitogen/unix.py index cc2d92ff..e691fe71 100644 --- a/mitogen/unix.py +++ b/mitogen/unix.py @@ -49,10 +49,13 @@ from mitogen.core import LOG def is_path_dead(path): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: - s.connect(path) - except socket.error: - e = sys.exc_info()[1] - return e.args[0] in (errno.ECONNREFUSED, errno.ENOENT) + try: + s.connect(path) + except socket.error: + e = sys.exc_info()[1] + return e.args[0] in (errno.ECONNREFUSED, errno.ENOENT) + finally: + s.close() return False From 586c6aca9a0685603c8e71c42b2e3f440cd43bd8 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 11:52:02 +0000 Subject: [PATCH 145/662] issue #406: unix: fix ordering of stop_receive/close. --- mitogen/unix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitogen/unix.py b/mitogen/unix.py index e691fe71..12182a28 100644 --- a/mitogen/unix.py +++ b/mitogen/unix.py @@ -82,9 +82,9 @@ class Listener(mitogen.core.BasicStream): router.broker.start_receive(self) def on_shutdown(self, broker): + broker.stop_receive(self) self._sock.close() self.receive_side.closed = True - broker.stop_receive(self) def _accept_client(self, sock): sock.setblocking(True) From e01c8f2891dc07e78d1c5d0fa8961c18cc23c598 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 11:57:50 +0000 Subject: [PATCH 146/662] issue #406: 3.x syntax fixes. --- tests/testlib.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/testlib.py b/tests/testlib.py index 3846f470..2f3c2b2e 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -10,7 +10,6 @@ import threading import time import traceback -import psutil import unittest2 import mitogen.core @@ -49,6 +48,7 @@ def get_fd_count(): """ Return the number of FDs open by this process. """ + import psutil return psutil.Process().num_fds() @@ -185,10 +185,9 @@ def log_fd_calls(): with l: rv = real_pipe() if mypid == os.getpid(): - print - print rv + sys.stdout.write('\n%s\n' % (rv,)) traceback.print_stack(limit=3) - print + sys.stdout.write('\n') return rv os.pipe = pipe @@ -197,10 +196,9 @@ def log_fd_calls(): with l: rv = real_socketpair(*args) if mypid == os.getpid(): - print - print '--', args, '->', rv + sys.stdout.write('\n%s -> %s\n' % (args, rv)) traceback.print_stack(limit=3) - print + sys.stdout.write('\n') return rv socket.socketpair = socketpair @@ -209,10 +207,9 @@ def log_fd_calls(): with l: real_dup2(*args) if mypid == os.getpid(): - print - print '--', args + sys.stdout.write('\n%s\n' % (args,)) traceback.print_stack(limit=3) - print + sys.stdout.write('\n') os.dup2 = dup2 real_dup = os.dup @@ -220,10 +217,9 @@ def log_fd_calls(): with l: rv = real_dup(*args) if mypid == os.getpid(): - print - print '--', args, '->', rv + sys.stdout.write('\n%s -> %s\n' % (args, rv)) traceback.print_stack(limit=3) - print + sys.stdout.write('\n') return rv os.dup = dup From 802de6a8d585fbc24434a993aa0e2bba02920ce1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 13:48:34 +0000 Subject: [PATCH 147/662] issue #406: clean up DiagLogStream handling and connect() failure. When Stream.connect() fails, have it just use on_disconnect(). Now there is a single disconnect cleanup path. Remove cutpasted DiagLogStream setup/destruction, and move it into the base class (temporarily), and only manage the lifetime of its underlying FD via Side.close(). This cures another EBADF failure. --- mitogen/doas.py | 18 ++------- mitogen/fork.py | 2 +- mitogen/parent.py | 70 ++++++++++++++++++++++------------- mitogen/ssh.py | 25 +++---------- mitogen/su.py | 5 +-- mitogen/sudo.py | 12 +++--- tests/data/stubs/stub-doas.py | 7 +++- tests/data/stubs/stub-sudo.py | 7 +++- 8 files changed, 73 insertions(+), 73 deletions(-) diff --git a/mitogen/doas.py b/mitogen/doas.py index cdcee0b0..09b2be9e 100644 --- a/mitogen/doas.py +++ b/mitogen/doas.py @@ -45,10 +45,6 @@ class Stream(mitogen.parent.Stream): create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) child_is_immediate_subprocess = False - #: Once connected, points to the corresponding DiagLogStream, allowing it - #: to be disconnected at the same time this stream is being torn down. - tty_stream = None - username = 'root' password = None doas_path = 'doas' @@ -75,10 +71,6 @@ class Stream(mitogen.parent.Stream): super(Stream, self).connect() self.name = u'doas.' + mitogen.core.to_text(self.username) - def on_disconnect(self, broker): - self.tty_stream.on_disconnect(broker) - super(Stream, self).on_disconnect(broker) - def get_boot_command(self): bits = [self.doas_path, '-u', self.username, '--'] bits = bits + super(Stream, self).get_boot_command() @@ -88,15 +80,13 @@ class Stream(mitogen.parent.Stream): password_incorrect_msg = 'doas password is incorrect' password_required_msg = 'doas password is required' - def _connect_bootstrap(self, extra_fd): - self.tty_stream = mitogen.parent.DiagLogStream(extra_fd, self) - - password_sent = False + def _connect_bootstrap(self): it = mitogen.parent.iter_read( - fds=[self.receive_side.fd, extra_fd], + fds=[self.receive_side.fd, self.diag_stream.receive_side.fd], deadline=self.connect_deadline, ) + password_sent = False for buf in it: LOG.debug('%r: received %r', self, buf) if buf.endswith(self.EC0_MARKER): @@ -111,7 +101,7 @@ class Stream(mitogen.parent.Stream): if password_sent: raise PasswordError(self.password_incorrect_msg) LOG.debug('sending password') - self.tty_stream.transmit_side.write( + self.diag_stream.transmit_side.write( mitogen.core.to_text(self.password + '\n').encode('utf-8') ) password_sent = True diff --git a/mitogen/fork.py b/mitogen/fork.py index cf769788..3e3a98a9 100644 --- a/mitogen/fork.py +++ b/mitogen/fork.py @@ -188,6 +188,6 @@ class Stream(mitogen.parent.Stream): # Don't trigger atexit handlers, they were copied from the parent. os._exit(0) - def _connect_bootstrap(self, extra_fd): + def _connect_bootstrap(self): # None required. pass diff --git a/mitogen/parent.py b/mitogen/parent.py index 556f38c6..c4e6f621 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -939,6 +939,33 @@ class Stream(mitogen.core.Stream): #: ExternalContext.main(). max_message_size = None + #: If :attr:`create_child` supplied a diag_fd, references the corresponding + #: :class:`DiagLogStream`, allowing it to be disconnected when this stream + #: is disconnected. Set to :data:`None` if no `diag_fd` was present. + diag_stream = None + + #: Function with the semantics of :func:`create_child` used to create the + #: child process. + create_child = staticmethod(create_child) + + #: Dictionary of extra kwargs passed to :attr:`create_child`. + create_child_args = {} + + #: :data:`True` if the remote has indicated that it intends to detach, and + #: should not be killed on disconnect. + detached = False + + #: If :data:`True`, indicates the child should not be killed during + #: graceful detachment, as it the actual process implementing the child + #: context. In all other cases, the subprocess is SSH, sudo, or a similar + #: tool that should be reminded to quit during disconnection. + child_is_immediate_subprocess = True + + #: Prefix given to default names generated by :meth:`connect`. + name_prefix = u'local' + + _reaped = False + def __init__(self, *args, **kwargs): super(Stream, self).__init__(*args, **kwargs) self.sent_modules = set(['mitogen', 'mitogen.core']) @@ -976,15 +1003,6 @@ class Stream(mitogen.core.Stream): ) ) - #: If :data:`True`, indicates the subprocess managed by us should not be - #: killed during graceful detachment, as it the actual process implementing - #: the child context. In all other cases, the subprocess is SSH, sudo, or a - #: similar tool that should be reminded to quit during disconnection. - child_is_immediate_subprocess = True - - detached = False - _reaped = False - def _reap_child(self): """ Reap the child process during disconnection. @@ -1024,8 +1042,10 @@ class Stream(mitogen.core.Stream): raise def on_disconnect(self, broker): - self._reap_child() super(Stream, self).on_disconnect(broker) + if self.diag_stream is not None: + self.diag_stream.on_disconnect(broker) + self._reap_child() # Minimised, gzipped, base64'd and passed to 'python -c'. It forks, dups # file descriptor 0 as 100, creates a pipe, then execs a new interpreter @@ -1129,10 +1149,6 @@ class Stream(mitogen.core.Stream): ) return zlib.compress(source.encode('utf-8'), 9) - create_child = staticmethod(create_child) - create_child_args = {} - name_prefix = u'local' - def start_child(self): args = self.get_boot_command() try: @@ -1154,26 +1170,28 @@ class Stream(mitogen.core.Stream): def connect(self): LOG.debug('%r.connect()', self) - self.pid, fd, extra_fd = self.start_child() + self.pid, fd, diag_fd = self.start_child() self.name = u'%s.%s' % (self.name_prefix, self.pid) self.receive_side = mitogen.core.Side(self, fd) self.transmit_side = mitogen.core.Side(self, os.dup(fd)) - LOG.debug('%r.connect(): child process stdin/stdout=%r', - self, self.receive_side.fd) + if diag_fd is not None: + self.diag_stream = DiagLogStream(diag_fd, self) + else: + self.diag_stream = None + + LOG.debug('%r.connect(): stdin=%r, stdout=%r, diag=%r', + self, self.receive_side.fd, self.transmit_side.fd, + self.diag_stream and self.diag_stream.receive_side.fd) try: - self._connect_bootstrap(extra_fd) + self._connect_bootstrap() except EofError: - self.receive_side.close() - self.transmit_side.close() + self.on_disconnect(self._router.broker) e = sys.exc_info()[1] self._adorn_eof_error(e) raise except Exception: - self.receive_side.close() - self.transmit_side.close() - if extra_fd is not None: - os.close(extra_fd) + self.on_disconnect(self._router.broker) self._reap_child() raise @@ -1188,8 +1206,10 @@ class Stream(mitogen.core.Stream): write_all(self.transmit_side.fd, self.get_preamble()) discard_until(self.receive_side.fd, self.EC1_MARKER, self.connect_deadline) + if self.diag_stream: + self._router.broker.start_receive(self.diag_stream) - def _connect_bootstrap(self, extra_fd): + def _connect_bootstrap(self): discard_until(self.receive_side.fd, self.EC0_MARKER, self.connect_deadline) self._ec0_received() diff --git a/mitogen/ssh.py b/mitogen/ssh.py index fba6e8f2..e3891f9c 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -127,10 +127,6 @@ class Stream(mitogen.parent.Stream): #: Number of -v invocations to pass on command line. ssh_debug_level = 0 - #: If batch_mode=False, points to the corresponding DiagLogStream, allowing - #: it to be disconnected at the same time this stream is being torn down. - tty_stream = None - #: The path to the SSH binary. ssh_path = 'ssh' @@ -195,11 +191,6 @@ class Stream(mitogen.parent.Stream): 'stderr_pipe': True, } - def on_disconnect(self, broker): - if self.tty_stream is not None: - self.tty_stream.on_disconnect(broker) - super(Stream, self).on_disconnect(broker) - def get_boot_command(self): bits = [self.ssh_path] if self.ssh_debug_level: @@ -265,7 +256,7 @@ class Stream(mitogen.parent.Stream): def _host_key_prompt(self): if self.check_host_keys == 'accept': LOG.debug('%r: accepting host key', self) - self.tty_stream.transmit_side.write(b('yes\n')) + self.diag_stream.transmit_side.write(b('yes\n')) return # _host_key_prompt() should never be reached with ignore or enforce @@ -273,16 +264,10 @@ class Stream(mitogen.parent.Stream): # with ours. raise HostKeyError(self.hostkey_config_msg) - def _ec0_received(self): - if self.tty_stream is not None: - self._router.broker.start_receive(self.tty_stream) - return super(Stream, self)._ec0_received() - - def _connect_bootstrap(self, extra_fd): + def _connect_bootstrap(self): fds = [self.receive_side.fd] - if extra_fd is not None: - self.tty_stream = mitogen.parent.DiagLogStream(extra_fd, self) - fds.append(extra_fd) + if self.diag_stream is not None: + fds.append(self.diag_stream.receive_side.fd) it = mitogen.parent.iter_read(fds=fds, deadline=self.connect_deadline) @@ -311,7 +296,7 @@ class Stream(mitogen.parent.Stream): if self.password is None: raise PasswordError(self.password_required_msg) LOG.debug('%r: sending password', self) - self.tty_stream.transmit_side.write( + self.diag_stream.transmit_side.write( (self.password + '\n').encode() ) password_sent = True diff --git a/mitogen/su.py b/mitogen/su.py index 7e2e5f08..9b0172c8 100644 --- a/mitogen/su.py +++ b/mitogen/su.py @@ -80,9 +80,6 @@ class Stream(mitogen.parent.Stream): super(Stream, self).connect() self.name = u'su.' + mitogen.core.to_text(self.username) - def on_disconnect(self, broker): - super(Stream, self).on_disconnect(broker) - def get_boot_command(self): argv = mitogen.parent.Argv(super(Stream, self).get_boot_command()) return [self.su_path, self.username, '-c', str(argv)] @@ -90,7 +87,7 @@ class Stream(mitogen.parent.Stream): password_incorrect_msg = 'su password is incorrect' password_required_msg = 'su password is required' - def _connect_bootstrap(self, extra_fd): + def _connect_bootstrap(self): password_sent = False it = mitogen.parent.iter_read( fds=[self.receive_side.fd], diff --git a/mitogen/sudo.py b/mitogen/sudo.py index 84b81ddc..b2eaabce 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -150,10 +150,6 @@ class Stream(mitogen.parent.Stream): super(Stream, self).connect() self.name = u'sudo.' + mitogen.core.to_text(self.username) - def on_disconnect(self, broker): - self.tty_stream.on_disconnect(broker) - super(Stream, self).on_disconnect(broker) - def get_boot_command(self): # Note: sudo did not introduce long-format option processing until July # 2013, so even though we parse long-format options, supply short-form @@ -177,12 +173,14 @@ class Stream(mitogen.parent.Stream): password_incorrect_msg = 'sudo password is incorrect' password_required_msg = 'sudo password is required' - def _connect_bootstrap(self, extra_fd): - self.tty_stream = mitogen.parent.DiagLogStream(extra_fd, self) + def _connect_bootstrap(self): + fds = [self.receive_side.fd] + if self.diag_stream is not None: + fds.append(self.diag_stream.receive_side.fd) password_sent = False it = mitogen.parent.iter_read( - fds=[self.receive_side.fd, extra_fd], + fds=fds, deadline=self.connect_deadline, ) diff --git a/tests/data/stubs/stub-doas.py b/tests/data/stubs/stub-doas.py index 08caf044..ca929bc0 100755 --- a/tests/data/stubs/stub-doas.py +++ b/tests/data/stubs/stub-doas.py @@ -2,8 +2,13 @@ import json import os +import subprocess import sys os.environ['ORIGINAL_ARGV'] = json.dumps(sys.argv) os.environ['THIS_IS_STUB_DOAS'] = '1' -os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) + +# This must be a child process and not exec() since Mitogen replaces its stderr +# descriptor, causing the last user of the slave PTY to close it, resulting in +# the master side indicating EIO. +subprocess.check_call(sys.argv[sys.argv.index('--') + 1:]) diff --git a/tests/data/stubs/stub-sudo.py b/tests/data/stubs/stub-sudo.py index ff88cd8e..a7f2704f 100755 --- a/tests/data/stubs/stub-sudo.py +++ b/tests/data/stubs/stub-sudo.py @@ -2,8 +2,13 @@ import json import os +import subprocess import sys os.environ['ORIGINAL_ARGV'] = json.dumps(sys.argv) os.environ['THIS_IS_STUB_SUDO'] = '1' -os.execv(sys.executable, sys.argv[sys.argv.index('--') + 1:]) + +# This must be a child process and not exec() since Mitogen replaces its stderr +# descriptor, causing the last user of the slave PTY to close it, resulting in +# the master side indicating EIO. +subprocess.check_call(sys.argv[sys.argv.index('--') + 1:]) From 01e65d78650ffebe9ab28cef06824f1bc34f97a5 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 13:53:43 +0000 Subject: [PATCH 148/662] docs: update Changelog; closes #406. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 781c42b9..e3885b79 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -213,6 +213,10 @@ Core Library * `#406 `_: connections could leak FDs when a child process failed to start. +* `#406 `_: connections could leave + FD wrapper objects that had not been closed lying around to be closed during + garbage collection, causing reused FD numbers to be closed at random moments. + * `#411 `_: the SSH method typed "``y``" rather than the requisite "``yes``" when `check_host_keys="accept"` was configured. This would lead to connection timeouts due to the hung From 4ac9cdce7c09b6eda5b180cc81c0a869e1cfdcd5 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 13:54:34 +0000 Subject: [PATCH 149/662] docs: update Changelog; closes #417. --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e3885b79..9dd75d1b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -213,7 +213,8 @@ Core Library * `#406 `_: connections could leak FDs when a child process failed to start. -* `#406 `_: connections could leave +* `#406 `_, + `#417 `_: connections could leave FD wrapper objects that had not been closed lying around to be closed during garbage collection, causing reused FD numbers to be closed at random moments. From c09780aeb013f08606357ea6caa9ec2ebc428426 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 14:36:44 +0000 Subject: [PATCH 150/662] core: fix add_handler(respondent=..) memory leak Closes #416. --- mitogen/core.py | 56 ++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 5b1d5298..3d2fc288 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2087,6 +2087,8 @@ class Router(object): self._last_handle = itertools.count(1000) #: handle -> (persistent?, func(msg)) self._handle_map = {} + #: Context -> set { handle, .. } + self._handles_by_respondent = {} self.add_handler(self._on_del_route, DEL_ROUTE) def __repr__(self): @@ -2117,7 +2119,7 @@ class Router(object): def _on_broker_exit(self): while self._handle_map: - _, (_, func, _) = self._handle_map.popitem() + _, (_, func, _, _) = self._handle_map.popitem() func(Message.dead()) def context_by_id(self, context_id, via_id=None, create=True, name=None): @@ -2151,8 +2153,8 @@ class Router(object): `dst_id`. If a specific route for `dst_id` is not known, a reference to the parent context's stream is returned. """ - return self._stream_by_id.get(dst_id, - self._stream_by_id.get(mitogen.parent_id)) + parent = self._stream_by_id.get(mitogen.parent_id) + return self._stream_by_id.get(dst_id, parent) def del_handler(self, handle): """ @@ -2161,7 +2163,9 @@ class Router(object): :raises KeyError: The handle wasn't registered. """ - del self._handle_map[handle] + _, _, _, respondent = self._handle_map.pop(handle) + if respondent: + self._handles_by_respondent[respondent].discard(handle) def add_handler(self, fn, handle=None, persist=True, policy=None, respondent=None): @@ -2216,19 +2220,22 @@ class Router(object): handle = handle or next(self._last_handle) _vv and IOLOG.debug('%r.add_handler(%r, %r, %r)', self, fn, handle, persist) + self._handle_map[handle] = persist, fn, policy, respondent if respondent: - assert policy is None - def policy(msg, _stream): - return msg.is_dead or msg.src_id == respondent.context_id - def on_disconnect(): - if handle in self._handle_map: - fn(Message.dead()) - del self._handle_map[handle] - listen(respondent, 'disconnect', on_disconnect) - - self._handle_map[handle] = persist, fn, policy + if respondent not in self._handles_by_respondent: + self._handles_by_respondent[respondent] = set() + listen(respondent, 'disconnect', + lambda: self._on_respondent_disconnect(respondent)) + self._handles_by_respondent[respondent].add(handle) + return handle + def _on_respondent_disconnect(self, context): + for handle in self._handles_by_respondent.pop(context, ()): + _, fn, _, _ = self._handle_map[handle] + fn(Message.dead()) + del self._handle_map[handle] + def on_shutdown(self, broker): """Called during :meth:`Broker.shutdown`, informs callbacks registered with :meth:`add_handle_cb` the connection is dead.""" @@ -2238,16 +2245,25 @@ class Router(object): _v and LOG.debug('%r.on_shutdown(): killing %r: %r', self, handle, fn) fn(Message.dead()) + def _maybe_send_dead(self, msg): + if msg.reply_to and not msg.is_dead: + msg.reply(Message.dead(), router=self) + refused_msg = 'Refused by policy.' def _invoke(self, msg, stream): # IOLOG.debug('%r._invoke(%r)', self, msg) try: - persist, fn, policy = self._handle_map[msg.handle] + persist, fn, policy, respondent = self._handle_map[msg.handle] except KeyError: LOG.error('%r: invalid handle: %r', self, msg) - if msg.reply_to and not msg.is_dead: - msg.reply(Message.dead()) + self._maybe_send_dead(msg) + return + + if respondent and not (msg.is_dead or + msg.src_id == respondent.context_id): + LOG.error('%r: reply from unexpected context: %r', self, msg) + self._maybe_send_dead(msg) return if policy and not policy(msg, stream): @@ -2261,17 +2277,13 @@ class Router(object): return if not persist: - del self._handle_map[msg.handle] + self.del_handler(msg.handle) try: fn(msg) except Exception: LOG.exception('%r._invoke(%r): %r crashed', self, msg, fn) - def _maybe_send_dead(self, msg): - if msg.reply_to and not msg.is_dead: - msg.reply(Message.dead(), router=self) - def _async_route(self, msg, in_stream=None): """ Arrange for `msg` to be forwarded towards its destination. If its From 176fe55bbd8c3771ef193f1eb53b302567d17892 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 14:38:24 +0000 Subject: [PATCH 151/662] issue #416: update Changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9dd75d1b..33d94e5b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -223,6 +223,10 @@ Core Library was configured. This would lead to connection timeouts due to the hung response. +* `#416 `_: around 1.4KiB of memory + was leaked on every RPC, due to a list of strong references keeping alive any + handler ever registered for disconnect notification. + * `16ca111e `_: handle OpenSSH 7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present. From cf97932fad3d117f91af95ffb5fe0c8c6ddaadf2 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 15:26:25 +0000 Subject: [PATCH 152/662] core: dead messages have optional body, use it everywhere; closes #387. --- mitogen/core.py | 81 +++++++++++++++++++++---------------- tests/call_function_test.py | 4 +- tests/parent_test.py | 16 ++++---- tests/router_test.py | 43 +++++++++++++------- 4 files changed, 84 insertions(+), 60 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 3d2fc288..058d1d3a 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -594,10 +594,11 @@ class Message(object): return self.reply_to == IS_DEAD @classmethod - def dead(cls, **kwargs): + def dead(cls, reason=None, **kwargs): """ Syntax helper to construct a dead message. """ + kwargs['data'] = (reason or u'').encode() return cls(reply_to=IS_DEAD, **kwargs) @classmethod @@ -645,6 +646,14 @@ class Message(object): else: UNPICKLER_KWARGS = {} + def _throw_dead(self): + if len(self.data): + raise ChannelError(self.data.decode(errors='replace')) + elif self.src_id == mitogen.context_id: + raise ChannelError(ChannelError.local_msg) + else: + raise ChannelError(ChannelError.remote_msg) + def unpickle(self, throw=True, throw_dead=True): """ Unpickle :attr:`data`, optionally raising any exceptions present. @@ -660,7 +669,7 @@ class Message(object): """ _vv and IOLOG.debug('%r.unpickle()', self) if throw_dead and self.is_dead: - raise ChannelError(ChannelError.remote_msg) + self._throw_dead() obj = self._unpickled if obj is Message._unpickled: @@ -811,6 +820,8 @@ class Receiver(object): if self.notify: self.notify(self) + closed_msg = 'the Receiver has been closed' + def close(self): """ Unregister the receiver's handle from its associated router, and cause @@ -820,7 +831,7 @@ class Receiver(object): if self.handle: self.router.del_handler(self.handle) self.handle = None - self._latch.put(Message.dead()) + self._latch.put(Message.dead(self.closed_msg)) def empty(self): """ @@ -853,10 +864,7 @@ class Receiver(object): _vv and IOLOG.debug('%r.get(timeout=%r, block=%r)', self, timeout, block) msg = self._latch.get(timeout=timeout, block=block) if msg.is_dead and throw_dead: - if msg.src_id == mitogen.context_id: - raise ChannelError(ChannelError.local_msg) - else: - raise ChannelError(ChannelError.remote_msg) + msg._throw_dead() return msg def __iter__(self): @@ -2117,10 +2125,12 @@ class Router(object): del self._stream_by_id[context.context_id] context.on_disconnect() + broker_exit_msg = 'Broker has exitted' + def _on_broker_exit(self): while self._handle_map: _, (_, func, _, _) = self._handle_map.popitem() - func(Message.dead()) + func(Message.dead(self.broker_exit_msg)) def context_by_id(self, context_id, via_id=None, create=True, name=None): """ @@ -2230,10 +2240,21 @@ class Router(object): return handle + refused_msg = 'refused by policy' + invalid_handle_msg = 'invalid handle' + too_large_msg = 'message too large (max %d bytes)' + respondent_disconnect_msg = 'the respondent Context has disconnected' + broker_shutdown_msg = 'Broker is shutting down' + no_route_msg = 'no route to %r, my ID is %r' + unidirectional_msg = ( + 'routing mode prevents forward of message from context %d via ' + 'context %d' + ) + def _on_respondent_disconnect(self, context): for handle in self._handles_by_respondent.pop(context, ()): _, fn, _, _ = self._handle_map[handle] - fn(Message.dead()) + fn(Message.dead(self.respondent_disconnect_msg)) del self._handle_map[handle] def on_shutdown(self, broker): @@ -2243,37 +2264,30 @@ class Router(object): fire(self, 'shutdown') for handle, (persist, fn) in self._handle_map.iteritems(): _v and LOG.debug('%r.on_shutdown(): killing %r: %r', self, handle, fn) - fn(Message.dead()) + fn(Message.dead(self.broker_shutdown_msg)) - def _maybe_send_dead(self, msg): + def _maybe_send_dead(self, msg, reason, *args): + if args: + reason %= args + LOG.debug('%r: %r is dead: %r', self, msg, reason) if msg.reply_to and not msg.is_dead: - msg.reply(Message.dead(), router=self) - - refused_msg = 'Refused by policy.' + msg.reply(Message.dead(reason=reason), router=self) def _invoke(self, msg, stream): # IOLOG.debug('%r._invoke(%r)', self, msg) try: persist, fn, policy, respondent = self._handle_map[msg.handle] except KeyError: - LOG.error('%r: invalid handle: %r', self, msg) - self._maybe_send_dead(msg) + self._maybe_send_dead(msg, reason=self.invalid_handle_msg) return if respondent and not (msg.is_dead or msg.src_id == respondent.context_id): - LOG.error('%r: reply from unexpected context: %r', self, msg) - self._maybe_send_dead(msg) + self._maybe_send_dead(msg, 'reply from unexpected context') return if policy and not policy(msg, stream): - LOG.error('%r: policy refused message: %r', self, msg) - if msg.reply_to: - self.route(Message.pickled( - CallError(self.refused_msg), - dst_id=msg.src_id, - handle=msg.reply_to - )) + self._maybe_send_dead(msg, self.refused_msg) return if not persist: @@ -2301,9 +2315,9 @@ class Router(object): _vv and IOLOG.debug('%r._async_route(%r, %r)', self, msg, in_stream) if len(msg.data) > self.max_message_size: - LOG.error('message too large (max %d bytes): %r', - self.max_message_size, msg) - self._maybe_send_dead(msg) + self._maybe_send_dead(msg, self.too_large_msg % ( + self.max_message_size, + )) return # Perform source verification. @@ -2336,17 +2350,14 @@ class Router(object): out_stream = self._stream_by_id.get(mitogen.parent_id) if out_stream is None: - if msg.reply_to not in (0, IS_DEAD): - LOG.error('%r: no route for %r, my ID is %r', - self, msg, mitogen.context_id) - self._maybe_send_dead(msg) + self._maybe_send_dead(msg, self.no_route_msg, + msg.dst_id, mitogen.context_id) return if in_stream and self.unidirectional and not \ (in_stream.is_privileged or out_stream.is_privileged): - LOG.error('routing mode prevents forward of %r from %r -> %r', - msg, in_stream, out_stream) - self._maybe_send_dead(msg) + self._maybe_send_dead(msg, self.unidirectional_msg, + in_stream.remote_id, out_stream.remote_id) return out_stream._send(msg) diff --git a/tests/call_function_test.py b/tests/call_function_test.py index 72991d62..5a0facc4 100644 --- a/tests/call_function_test.py +++ b/tests/call_function_test.py @@ -90,7 +90,7 @@ class CallFunctionTest(testlib.RouterMixin, testlib.TestCase): self.broker.defer(stream.on_disconnect, self.broker) exc = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(exc.args[0], mitogen.core.ChannelError.local_msg) + self.assertEquals(exc.args[0], self.router.respondent_disconnect_msg) def test_aborted_on_local_broker_shutdown(self): stream = self.router._stream_by_id[self.local.context_id] @@ -99,7 +99,7 @@ class CallFunctionTest(testlib.RouterMixin, testlib.TestCase): self.broker.shutdown() exc = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(exc.args[0], mitogen.core.ChannelError.local_msg) + self.assertEquals(exc.args[0], self.router.respondent_disconnect_msg) def test_accepts_returns_context(self): context = self.local.call(func_returns_arg, self.local) diff --git a/tests/parent_test.py b/tests/parent_test.py index e83d6f1a..797845df 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -319,7 +319,7 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): c1.shutdown(wait=True) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) + self.assertEquals(e.args[0], self.router.respondent_disconnect_msg) def test_indirect_child_disconnected(self): # Achievement unlocked: process notices an indirectly connected child @@ -330,7 +330,7 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): c2.shutdown(wait=True) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) + self.assertEquals(e.args[0], self.router.respondent_disconnect_msg) def test_indirect_child_intermediary_disconnected(self): # Battlefield promotion: process notices indirect child disconnected @@ -341,7 +341,7 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): c1.shutdown(wait=True) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) + 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 @@ -357,9 +357,8 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): c2.shutdown(wait=True) e = self.assertRaises(mitogen.core.CallError, lambda: recv.get().unpickle()) - self.assertTrue(e.args[0].startswith( - 'mitogen.core.ChannelError: Channel closed by local end.' - )) + s = 'mitogen.core.ChannelError: ' + self.router.respondent_disconnect_msg + self.assertTrue(e.args[0].startswith(s)) def test_far_sibling_disconnected(self): # God mode: child of child notices child of child of parent has @@ -378,9 +377,8 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): c22.shutdown(wait=True) e = self.assertRaises(mitogen.core.CallError, lambda: recv.get().unpickle()) - self.assertTrue(e.args[0].startswith( - 'mitogen.core.ChannelError: Channel closed by local end.' - )) + s = 'mitogen.core.ChannelError: ' + self.router.respondent_disconnect_msg + self.assertTrue(e.args[0].startswith(s)) if __name__ == '__main__': diff --git a/tests/router_test.py b/tests/router_test.py index b0add6d3..ebbf20ff 100644 --- a/tests/router_test.py +++ b/tests/router_test.py @@ -137,14 +137,13 @@ class PolicyTest(testlib.RouterMixin, testlib.TestCase): self.sync_with_broker() # Verify log. - expect = '%r: policy refused message: ' % (self.router,) - self.assertTrue(expect in log.stop()) + self.assertTrue(self.router.refused_msg in log.stop()) # Verify message was not delivered. self.assertTrue(recv.empty()) # Verify CallError received by reply_to target. - e = self.assertRaises(mitogen.core.CallError, + e = self.assertRaises(mitogen.core.ChannelError, lambda: reply_target.get().unpickle()) self.assertEquals(e.args[0], self.router.refused_msg) @@ -212,14 +211,15 @@ class MessageSizeTest(testlib.BrokerMixin, testlib.TestCase): logs = testlib.LogCapturer() logs.start() + expect = router.too_large_msg % (4096,) + # Try function call. Receiver should be woken by a dead message sent by # router due to message size exceeded. child = router.fork() e = self.assertRaises(mitogen.core.ChannelError, lambda: child.call(zlib.crc32, ' '*8192)) - self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) + self.assertEquals(e.args[0], expect) - expect = 'message too large (max 4096 bytes)' self.assertTrue(expect in logs.stop()) def test_remote_configured(self): @@ -252,11 +252,12 @@ class NoRouteTest(testlib.RouterMixin, testlib.TestCase): msg = recv.get(throw_dead=False) self.assertEquals(msg.is_dead, True) self.assertEquals(msg.src_id, l1.context_id) + self.assertEquals(msg.data, self.router.invalid_handle_msg.encode()) recv = l1.send_async(mitogen.core.Message(handle=999)) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(e.args[0], mitogen.core.ChannelError.remote_msg) + self.assertEquals(e.args[0], self.router.invalid_handle_msg) def test_totally_invalid_context_returns_dead(self): recv = mitogen.core.Receiver(self.router) @@ -269,11 +270,18 @@ class NoRouteTest(testlib.RouterMixin, testlib.TestCase): rmsg = recv.get(throw_dead=False) self.assertEquals(rmsg.is_dead, True) self.assertEquals(rmsg.src_id, mitogen.context_id) + self.assertEquals(rmsg.data, (self.router.no_route_msg % ( + 1234, + mitogen.context_id, + )).encode()) self.router.route(msg) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) + self.assertEquals(e.args[0], (self.router.no_route_msg % ( + 1234, + mitogen.context_id, + ))) def test_previously_alive_context_returns_dead(self): l1 = self.router.fork() @@ -288,11 +296,18 @@ class NoRouteTest(testlib.RouterMixin, testlib.TestCase): rmsg = recv.get(throw_dead=False) self.assertEquals(rmsg.is_dead, True) self.assertEquals(rmsg.src_id, mitogen.context_id) + self.assertEquals(rmsg.data, (self.router.no_route_msg % ( + l1.context_id, + mitogen.context_id, + )).encode()) self.router.route(msg) e = self.assertRaises(mitogen.core.ChannelError, lambda: recv.get()) - self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) + self.assertEquals(e.args[0], self.router.no_route_msg % ( + l1.context_id, + mitogen.context_id, + )) class UnidirectionalTest(testlib.RouterMixin, testlib.TestCase): @@ -305,7 +320,10 @@ class UnidirectionalTest(testlib.RouterMixin, testlib.TestCase): e = self.assertRaises(mitogen.core.CallError, lambda: l2.call(ping_context, l1)) - msg = 'mitogen.core.ChannelError: Channel closed by remote end.' + msg = self.router.unidirectional_msg % ( + l2.context_id, + l1.context_id, + ) self.assertTrue(msg in str(e)) self.assertTrue('routing mode prevents forward of ' in logs.stop()) @@ -319,14 +337,11 @@ class UnidirectionalTest(testlib.RouterMixin, testlib.TestCase): l1s.is_privileged = True l2 = self.router.fork() - logs = testlib.LogCapturer() - logs.start() e = self.assertRaises(mitogen.core.CallError, lambda: l2.call(ping_context, l1)) - msg = 'mitogen.core.CallError: Refused by policy.' - self.assertTrue(msg in str(e)) - self.assertTrue('policy refused message: ' in logs.stop()) + msg = 'mitogen.core.ChannelError: %s' % (self.router.refused_msg,) + self.assertTrue(str(e).startswith(msg)) class EgressIdsTest(testlib.RouterMixin, testlib.TestCase): From 33011af9a5641b620fd8b4593b511ebfc97dac91 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 15:29:06 +0000 Subject: [PATCH 153/662] issue #387: update Changelog. --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 33d94e5b..0ae70a39 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -201,6 +201,12 @@ Core Library receivers to wake with :class:`mitogen.core.ChannelError`, even when one participant is not a parent of the other. +* `#387 `_: dead messages include an + optional reason in their body. This is used to cause + :class:`mitogen.core.ChannelError` to report far more useful diagnostics at + the point the error occurs that previously would have been buried in debug + log output from an unrelated context. + * `#405 `_: if an oversized message is rejected, and it has a ``reply_to`` set, a dead message is returned to the sender. This ensures function calls exceeding the configured maximum size From fea0fb41fc7e508ae2417bbf3509be89764cc70c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 15:42:51 +0000 Subject: [PATCH 154/662] docs: update Changelog; closes #288 --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0ae70a39..3c081d3e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -219,7 +219,8 @@ Core Library * `#406 `_: connections could leak FDs when a child process failed to start. -* `#406 `_, +* `#288 `_, + `#406 `_, `#417 `_: connections could leave FD wrapper objects that had not been closed lying around to be closed during garbage collection, causing reused FD numbers to be closed at random moments. From 76ec4f201c6bd56d116b950888ae150301a8570b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 17:49:45 +0000 Subject: [PATCH 155/662] issue #413: paper over harmless duplicate del_route() Ideally it would only be called once, and in future maybe it can, but right now we need to cope with these cases: * Downstream parent notifies us of disconnection (DEL_ROUTE) * We notify ourself of disconnection * We notify ourself and so does downstream parent It's case 3 that causes the error. --- ansible_mitogen/services.py | 5 +---- mitogen/core.py | 4 ++-- mitogen/parent.py | 17 ++++++++++++----- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index c97d71bb..adfd828b 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -303,12 +303,9 @@ class ContextService(mitogen.service.Service): longer reachable context. This method runs in the Broker thread and must not to block. """ - # TODO: there is a race between creation of a context and disconnection - # of its related stream. An error reply should be sent to any message - # in _latches_by_key below. self._lock.acquire() try: - LOG.info('Forgetting %r due to stream disconnect', context) + LOG.info('%r: Forgetting %r due to stream disconnect', self, context) self._forget_context_unlocked(context) finally: self._lock.release() diff --git a/mitogen/core.py b/mitogen/core.py index 058d1d3a..58289553 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2104,8 +2104,8 @@ class Router(object): def _on_del_route(self, msg): """ - Stub DEL_ROUTE handler; fires 'disconnect' events on the corresponding - member of :attr:`_context_by_id`. This handler is replaced by + Stub :data:`DEL_ROUTE` handler; fires 'disconnect' events on the + corresponding :attr:`_context_by_id` member. This is replaced by :class:`mitogen.parent.RouteMonitor` in an upgraded context. """ LOG.error('%r._on_del_route() %r', self, msg) diff --git a/mitogen/parent.py b/mitogen/parent.py index c4e6f621..04f7784e 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1558,6 +1558,9 @@ class RouteMonitor(object): #: stream; used to cleanup routes during disconnection. self._routes_by_stream = {} + def __repr__(self): + return 'RouteMonitor()' + def _send_one(self, stream, handle, target_id, name): """ Compose and send an update message on a stream. @@ -1644,7 +1647,8 @@ class RouteMonitor(object): Respond to disconnection of a local stream by """ routes = self._routes_by_stream.pop(stream) - LOG.debug('%r is gone; propagating DEL_ROUTE for %r', stream, routes) + LOG.debug('%r: %r is gone; propagating DEL_ROUTE for %r', + self, stream, routes) for target_id in routes: self.router.del_route(target_id) self._propagate_up(mitogen.core.DEL_ROUTE, target_id) @@ -1692,18 +1696,21 @@ class RouteMonitor(object): target_id = int(msg.data) registered_stream = self.router.stream_by_id(target_id) + if registered_stream is None: + return + stream = self.router.stream_by_id(msg.auth_id) if registered_stream != stream: - LOG.error('Received DEL_ROUTE for %d from %r, expected %r', - target_id, stream, registered_stream) + LOG.error('%r: received DEL_ROUTE for %d from %r, expected %r', + self, target_id, stream, registered_stream) return context = self.router.context_by_id(target_id, create=False) if context: - LOG.debug('%r: Firing local disconnect for %r', self, context) + LOG.debug('%r: firing local disconnect for %r', self, context) mitogen.core.fire(context, 'disconnect') - LOG.debug('Deleting route to %d via %r', target_id, stream) + LOG.debug('%r: deleting route to %d via %r', self, target_id, stream) routes = self._routes_by_stream.get(stream) if routes: routes.discard(target_id) From 7a1dfa388ac04d786f8d8ad8dfb3a3dff7c857c3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 17:51:51 +0000 Subject: [PATCH 156/662] docs: update Changelog; closes #413. --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3c081d3e..93c21370 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -201,7 +201,8 @@ Core Library receivers to wake with :class:`mitogen.core.ChannelError`, even when one participant is not a parent of the other. -* `#387 `_: dead messages include an +* `#387 `_, + `#413 `_: dead messages include an optional reason in their body. This is used to cause :class:`mitogen.core.ChannelError` to report far more useful diagnostics at the point the error occurs that previously would have been buried in debug From d1b7c232bf76096b2f20bd86a4446b0e0ba164fd Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 18:56:54 +0000 Subject: [PATCH 157/662] tests: image_prep needs sudo --- tests/image_prep/_container_setup.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/image_prep/_container_setup.yml b/tests/image_prep/_container_setup.yml index db0d3789..f62c5955 100644 --- a/tests/image_prep/_container_setup.yml +++ b/tests/image_prep/_container_setup.yml @@ -2,6 +2,7 @@ - hosts: all strategy: linear gather_facts: false + become: true tasks: - raw: > if ! python -c ''; then From 3836c6a22023011fa6028aeef6607cab4f7de3f4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 19:43:52 +0000 Subject: [PATCH 158/662] tests/bench: run roundtrip.py a ton more to reduce variance --- tests/bench/roundtrip.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/bench/roundtrip.py b/tests/bench/roundtrip.py index 7c5a9252..33b3c5b8 100644 --- a/tests/bench/roundtrip.py +++ b/tests/bench/roundtrip.py @@ -5,13 +5,22 @@ Measure latency of local RPC. import mitogen import time +import ansible_mitogen.process +ansible_mitogen.process.setup_gil() + +try: + xrange +except NameError: + xrange = range + def do_nothing(): pass @mitogen.main() def main(router): f = router.fork() + f.call(do_nothing) t0 = time.time() - for x in range(1000): + for x in xrange(20000): f.call(do_nothing) - print '++', int(1e6 * ((time.time() - t0) / (1.0+x))), 'usec' + print('++', int(1e6 * ((time.time() - t0) / (1.0+x))), 'usec') From 6e1f9e25960219c94fc6c001d8eed624b456fd3f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 19:47:25 +0000 Subject: [PATCH 159/662] core: 2.6 str.decode() compat fix. --- mitogen/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitogen/core.py b/mitogen/core.py index 58289553..370fb742 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -648,7 +648,7 @@ class Message(object): def _throw_dead(self): if len(self.data): - raise ChannelError(self.data.decode(errors='replace')) + raise ChannelError(self.data.decode('utf-8', 'replace')) elif self.src_id == mitogen.context_id: raise ChannelError(ChannelError.local_msg) else: From 5482b4d528a7e79e0291b27f5db40e5f1954d060 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 19:48:42 +0000 Subject: [PATCH 160/662] tests: poller_test 3.x fix. --- tests/poller_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/poller_test.py b/tests/poller_test.py index a6190821..c214f367 100644 --- a/tests/poller_test.py +++ b/tests/poller_test.py @@ -43,7 +43,7 @@ class SockMixin(object): """Make `fd` unwriteable.""" while True: try: - os.write(fd, 'x'*4096) + os.write(fd, mitogen.core.b('x')*4096) except OSError: e = sys.exc_info()[1] if e.args[0] == errno.EAGAIN: From 27a4001f4f5b5e682ac103ce6069515bd6a4125d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 19:49:17 +0000 Subject: [PATCH 161/662] tests: handle NameError when faulthandler is not installed. --- tests/testlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testlib.py b/tests/testlib.py index 2f3c2b2e..1406bf2d 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -20,7 +20,7 @@ import mitogen.utils try: import faulthandler except ImportError: - pass + faulthandler = None try: import urlparse From 3f46c9569c260a972c4790e2510510b9d9d898c5 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 20:16:44 +0000 Subject: [PATCH 162/662] tests: 3.x syntax compat for tests/data/stubs/ --- tests/data/stubs/stub-lxc-info.py | 2 +- tests/data/stubs/stub-lxc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/stubs/stub-lxc-info.py b/tests/data/stubs/stub-lxc-info.py index bcbc36a5..480bf266 100755 --- a/tests/data/stubs/stub-lxc-info.py +++ b/tests/data/stubs/stub-lxc-info.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # Mainly for use in stubconnections/kubectl.yml -print 'PID: 1' +print('PID: 1') diff --git a/tests/data/stubs/stub-lxc.py b/tests/data/stubs/stub-lxc.py index 03572cac..9d14090a 100755 --- a/tests/data/stubs/stub-lxc.py +++ b/tests/data/stubs/stub-lxc.py @@ -5,7 +5,7 @@ import os # setns.py fetching leader PID. if sys.argv[1] == 'info': - print 'Pid: 1' + print('Pid: 1') sys.exit(0) os.environ['ORIGINAL_ARGV'] = repr(sys.argv) From 045db6f6890e54540827ddebc8eecdd6a22e2413 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 20:18:23 +0000 Subject: [PATCH 163/662] Fix iter_read() FD leaks on 3.x; closes #418. --- mitogen/doas.py | 17 +++++++++++------ mitogen/parent.py | 16 ++++++++++------ mitogen/ssh.py | 19 ++++++++++++------- mitogen/su.py | 19 ++++++++++++------- mitogen/sudo.py | 28 +++++++++++++++++----------- 5 files changed, 62 insertions(+), 37 deletions(-) diff --git a/mitogen/doas.py b/mitogen/doas.py index 09b2be9e..3a6f881d 100644 --- a/mitogen/doas.py +++ b/mitogen/doas.py @@ -80,12 +80,7 @@ class Stream(mitogen.parent.Stream): password_incorrect_msg = 'doas password is incorrect' password_required_msg = 'doas password is required' - def _connect_bootstrap(self): - it = mitogen.parent.iter_read( - fds=[self.receive_side.fd, self.diag_stream.receive_side.fd], - deadline=self.connect_deadline, - ) - + def _connect_input_loop(self, it): password_sent = False for buf in it: LOG.debug('%r: received %r', self, buf) @@ -106,3 +101,13 @@ class Stream(mitogen.parent.Stream): ) password_sent = True raise mitogen.core.StreamError('bootstrap failed') + + def _connect_bootstrap(self): + it = mitogen.parent.iter_read( + fds=[self.receive_side.fd, self.diag_stream.receive_side.fd], + deadline=self.connect_deadline, + ) + try: + self._connect_input_loop(it) + finally: + it.close() diff --git a/mitogen/parent.py b/mitogen/parent.py index 04f7784e..865d8fd2 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -532,12 +532,16 @@ def discard_until(fd, s, deadline): :raises mitogen.core.StreamError: Attempt to read past end of file. """ - for buf in iter_read([fd], deadline): - if IOLOG.level == logging.DEBUG: - for line in buf.splitlines(): - IOLOG.debug('discard_until: discarding %r', line) - if buf.endswith(s): - return + it = iter_read([fd], deadline) + try: + for buf in it: + if IOLOG.level == logging.DEBUG: + for line in buf.splitlines(): + IOLOG.debug('discard_until: discarding %r', line) + if buf.endswith(s): + return + finally: + it.close() # ensure Poller.close() is called. def _upgrade_broker(broker): diff --git a/mitogen/ssh.py b/mitogen/ssh.py index e3891f9c..106dfd56 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -264,13 +264,7 @@ class Stream(mitogen.parent.Stream): # with ours. raise HostKeyError(self.hostkey_config_msg) - def _connect_bootstrap(self): - fds = [self.receive_side.fd] - if self.diag_stream is not None: - fds.append(self.diag_stream.receive_side.fd) - - it = mitogen.parent.iter_read(fds=fds, deadline=self.connect_deadline) - + def _connect_input_loop(self, it): password_sent = False for buf, partial in filter_debug(self, it): LOG.debug('%r: received %r', self, buf) @@ -302,3 +296,14 @@ class Stream(mitogen.parent.Stream): password_sent = True raise mitogen.core.StreamError('bootstrap failed') + + def _connect_bootstrap(self): + fds = [self.receive_side.fd] + if self.diag_stream is not None: + fds.append(self.diag_stream.receive_side.fd) + + it = mitogen.parent.iter_read(fds=fds, deadline=self.connect_deadline) + try: + self._connect_input_loop(it) + finally: + it.close() diff --git a/mitogen/su.py b/mitogen/su.py index 9b0172c8..65357a3e 100644 --- a/mitogen/su.py +++ b/mitogen/su.py @@ -87,13 +87,7 @@ class Stream(mitogen.parent.Stream): password_incorrect_msg = 'su password is incorrect' password_required_msg = 'su password is required' - def _connect_bootstrap(self): - password_sent = False - it = mitogen.parent.iter_read( - fds=[self.receive_side.fd], - deadline=self.connect_deadline, - ) - + def _connect_input_loop(self, it): for buf in it: LOG.debug('%r: received %r', self, buf) if buf.endswith(self.EC0_MARKER): @@ -113,3 +107,14 @@ class Stream(mitogen.parent.Stream): ) password_sent = True raise mitogen.core.StreamError('bootstrap failed') + + def _connect_bootstrap(self): + password_sent = False + it = mitogen.parent.iter_read( + fds=[self.receive_side.fd], + deadline=self.connect_deadline, + ) + try: + self._connect_input_loop(it) + finally: + it.close() diff --git a/mitogen/sudo.py b/mitogen/sudo.py index b2eaabce..68b27fec 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -173,17 +173,7 @@ class Stream(mitogen.parent.Stream): password_incorrect_msg = 'sudo password is incorrect' password_required_msg = 'sudo password is required' - def _connect_bootstrap(self): - fds = [self.receive_side.fd] - if self.diag_stream is not None: - fds.append(self.diag_stream.receive_side.fd) - - password_sent = False - it = mitogen.parent.iter_read( - fds=fds, - deadline=self.connect_deadline, - ) - + def _connect_input_loop(self, it): for buf in it: LOG.debug('%r: received %r', self, buf) if buf.endswith(self.EC0_MARKER): @@ -199,3 +189,19 @@ class Stream(mitogen.parent.Stream): ) password_sent = True raise mitogen.core.StreamError('bootstrap failed') + + def _connect_bootstrap(self): + fds = [self.receive_side.fd] + if self.diag_stream is not None: + fds.append(self.diag_stream.receive_side.fd) + + password_sent = False + it = mitogen.parent.iter_read( + fds=fds, + deadline=self.connect_deadline, + ) + + try: + self._connect_input_loop(it) + finally: + it.close() From a7eca5b55e66a13825175800ede8a65b41d45c5d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 20:20:41 +0000 Subject: [PATCH 164/662] docs: update Changelog. --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 93c21370..bb1974a8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -235,6 +235,11 @@ Core Library was leaked on every RPC, due to a list of strong references keeping alive any handler ever registered for disconnect notification. +* `#418 `_: the + :func:`mitogen.parent.iter_read` helper would leak poller FDs, because + execution of its :keyword:`finally` block was delayed on Python 3. Now + callers explicitly close the generator when finished. + * `16ca111e `_: handle OpenSSH 7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present. From e180d310b5eb0151435fbea64a4e372f0bede128 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 20:27:52 +0000 Subject: [PATCH 165/662] tests: fix fork_test compat on 3.x. --- tests/fork_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fork_test.py b/tests/fork_test.py index dd214bd1..39f5352e 100644 --- a/tests/fork_test.py +++ b/tests/fork_test.py @@ -16,14 +16,14 @@ import plain_old_module def _find_ssl_linux(): s = testlib.subprocess__check_output(['ldd', _ssl.__file__]) - for line in s.splitlines(): + for line in s.decode().splitlines(): bits = line.split() if bits[0].startswith('libssl'): return bits[2] def _find_ssl_darwin(): s = testlib.subprocess__check_output(['otool', '-l', _ssl.__file__]) - for line in s.splitlines(): + for line in s.decode().splitlines(): bits = line.split() if bits[0] == 'name' and 'libssl' in bits[1]: return bits[1] From 6d5facec4c5728f71f345f6e4e7b6738ea66888b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 20:43:29 +0000 Subject: [PATCH 166/662] su/sudo: fallout from previous commits issue #418 and FD cleanup work. --- mitogen/su.py | 4 +++- mitogen/sudo.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mitogen/su.py b/mitogen/su.py index 65357a3e..9faee2d4 100644 --- a/mitogen/su.py +++ b/mitogen/su.py @@ -88,6 +88,8 @@ class Stream(mitogen.parent.Stream): password_required_msg = 'su password is required' def _connect_input_loop(self, it): + password_sent = False + for buf in it: LOG.debug('%r: received %r', self, buf) if buf.endswith(self.EC0_MARKER): @@ -106,10 +108,10 @@ class Stream(mitogen.parent.Stream): mitogen.core.to_text(self.password + '\n').encode('utf-8') ) password_sent = True + raise mitogen.core.StreamError('bootstrap failed') def _connect_bootstrap(self): - password_sent = False it = mitogen.parent.iter_read( fds=[self.receive_side.fd], deadline=self.connect_deadline, diff --git a/mitogen/sudo.py b/mitogen/sudo.py index 68b27fec..cf8a44d9 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -116,10 +116,6 @@ class Stream(mitogen.parent.Stream): create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) child_is_immediate_subprocess = False - #: Once connected, points to the corresponding DiagLogStream, allowing it to - #: be disconnected at the same time this stream is being torn down. - tty_stream = None - sudo_path = 'sudo' username = 'root' password = None @@ -174,6 +170,8 @@ class Stream(mitogen.parent.Stream): password_required_msg = 'sudo password is required' def _connect_input_loop(self, it): + password_sent = False + for buf in it: LOG.debug('%r: received %r', self, buf) if buf.endswith(self.EC0_MARKER): @@ -184,10 +182,11 @@ class Stream(mitogen.parent.Stream): raise PasswordError(self.password_required_msg) if password_sent: raise PasswordError(self.password_incorrect_msg) - self.tty_stream.transmit_side.write( + self.diag_stream.transmit_side.write( mitogen.core.to_text(self.password + '\n').encode('utf-8') ) password_sent = True + raise mitogen.core.StreamError('bootstrap failed') def _connect_bootstrap(self): @@ -195,7 +194,6 @@ class Stream(mitogen.parent.Stream): if self.diag_stream is not None: fds.append(self.diag_stream.receive_side.fd) - password_sent = False it = mitogen.parent.iter_read( fds=fds, deadline=self.connect_deadline, From 1c24a13560f3181516b724a14431f0109926d788 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 20:54:34 +0000 Subject: [PATCH 167/662] tests: add Ansible back to requirements Needed for Tox --- dev_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_requirements.txt b/dev_requirements.txt index c536c154..eb83f8be 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,5 @@ -r docs/docs-requirements.txt +-r tests/ansible/requirements.txt psutil==5.4.8 coverage==4.5.1 Django==1.6.11 # Last version supporting 2.6. From cd6486b0e9211412b32d9effab4dfa9a03b65177 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 21:16:44 +0000 Subject: [PATCH 168/662] tests: fix more DisconnectTest raciness. --- tests/parent_test.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/parent_test.py b/tests/parent_test.py index 797845df..0bd8079c 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -31,8 +31,25 @@ def wait_for_child(pid, timeout=1.0): @mitogen.core.takes_econtext -def call_func_in_sibling(ctx, econtext): - ctx.call(time.sleep, 99999) +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.pending_bytes()) == 0: + return + time.sleep(0.1) class GetDefaultRemoteNameTest(testlib.TestCase): @@ -353,12 +370,17 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): self.router.stream_by_id(c1.context_id).auth_id = mitogen.context_id c1.call(mitogen.parent.upgrade_router) - recv = c1.call_async(call_func_in_sibling, c2) + 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)) + 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 @@ -373,8 +395,13 @@ class DisconnectTest(testlib.RouterMixin, testlib.TestCase): self.router.stream_by_id(c1.context_id).auth_id = mitogen.context_id c11.call(mitogen.parent.upgrade_router) - recv = c11.call_async(call_func_in_sibling, c22) + 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 From c286f4f107248641dec58b71465abe4f3c79f117 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Sun, 4 Nov 2018 21:17:09 +0000 Subject: [PATCH 169/662] Add tests/ansible/requirements.txt to Tox. --- tests/ansible/requirements.txt | 2 ++ tox.ini | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/ansible/requirements.txt b/tests/ansible/requirements.txt index fdabb0f6..551af999 100644 --- a/tests/ansible/requirements.txt +++ b/tests/ansible/requirements.txt @@ -1,2 +1,4 @@ +ansible; python_version >= '2.7' +ansible<2.7; python_version < '2.7' paramiko==2.3.2 # Last 2.6-compat version. google-api-python-client==1.6.5 diff --git a/tox.ini b/tox.ini index 6bf8bb53..9bff333e 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist = [testenv] deps = -r{toxinidir}/dev_requirements.txt + -r{toxinidir}/tests/ansible/requirements.txt commands = {posargs:bash run_tests} From 574fc27a9ca5340e4f7ea57daf67e6d5746f6ad0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 00:22:47 +0000 Subject: [PATCH 170/662] issue #414: import test / reproduction. --- tests/ansible/integration/async/all.yml | 3 +- .../integration/async/multiple_items_loop.yml | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/ansible/integration/async/multiple_items_loop.yml diff --git a/tests/ansible/integration/async/all.yml b/tests/ansible/integration/async/all.yml index 17969ead..61d2d35c 100644 --- a/tests/ansible/integration/async/all.yml +++ b/tests/ansible/integration/async/all.yml @@ -1,8 +1,9 @@ +- import_playbook: multiple_items_loop.yml - import_playbook: result_binary_producing_json.yml - import_playbook: result_binary_producing_junk.yml - import_playbook: result_shell_echo_hi.yml - import_playbook: runner_new_process.yml - import_playbook: runner_one_job.yml - import_playbook: runner_timeout_then_polling.yml -- import_playbook: runner_with_polling_and_timeout.yml - import_playbook: runner_two_simultaneous_jobs.yml +- import_playbook: runner_with_polling_and_timeout.yml diff --git a/tests/ansible/integration/async/multiple_items_loop.yml b/tests/ansible/integration/async/multiple_items_loop.yml new file mode 100644 index 00000000..9a9b1192 --- /dev/null +++ b/tests/ansible/integration/async/multiple_items_loop.yml @@ -0,0 +1,36 @@ +# issue #414: verify behaviour of async tasks created in a loop. + +- name: integration/async/multiple_items_loop.yml + hosts: test-targets + any_errors_fatal: true + tasks: + + - name: start long running ops + become: true + shell: "{{item}}" + async: 15 + poll: 0 + register: jobs + with_items: + - "sleep 3; echo hi-from-job-1" + - "sleep 5; echo hi-from-job-2" + + - name: Ensure static files are collected and compressed + async_status: + jid: "{{ item.ansible_job_id }}" + become: yes + register: out + until: out.finished + retries: 30 + with_items: + - "{{ jobs.results }}" + + - assert: + that: + - out.results[0].stdout == 'hi-from-job-1' + - out.results[0].rc == 0 + - out.results[0].delta > '0:00:03' + + - out.results[1].stdout == 'hi-from-job-2' + - out.results[1].rc == 0 + - out.results[1].delta > '0:00:05' From 174b685d16121c4f5760020f7db6ab731f8aa70a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 12:40:48 +0000 Subject: [PATCH 171/662] tests: CentOS 6 lacks %wheel in sudo by default. --- tests/image_prep/_container_setup.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/image_prep/_container_setup.yml b/tests/image_prep/_container_setup.yml index f62c5955..39ab2dd8 100644 --- a/tests/image_prep/_container_setup.yml +++ b/tests/image_prep/_container_setup.yml @@ -2,7 +2,6 @@ - hosts: all strategy: linear gather_facts: false - become: true tasks: - raw: > if ! python -c ''; then @@ -96,6 +95,11 @@ dest: /etc/sudoers.d/001-mitogen src: ../data/docker/001-mitogen.sudo + - lineinfile: + path: /etc/sudoers + line: "%wheel ALL=(ALL) ALL" + when: distro == "CentOS" + - lineinfile: path: /etc/ssh/sshd_config line: Banner /etc/ssh/banner.txt From 91513f5b7ee32f316f61f64fc9b6b01cbaac8858 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 12:39:39 +0000 Subject: [PATCH 172/662] tests: properly close 'cat' child process on exit. --- .travis/ci_lib.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.travis/ci_lib.py b/.travis/ci_lib.py index e92564b6..d8d6827d 100644 --- a/.travis/ci_lib.py +++ b/.travis/ci_lib.py @@ -37,11 +37,20 @@ if not hasattr(subprocess, 'check_output'): # Force stdout FD 1 to be a pipe, so tools like pip don't spam progress bars. -sys.stdout = os.popen('stdbuf -oL cat', 'w', 1) -os.dup2(sys.stdout.fileno(), 1) +proc = subprocess.Popen( + args=['stdbuf', '-oL', 'cat'], + stdin=subprocess.PIPE +) + +os.dup2(proc.stdin.fileno(), 1) +os.dup2(proc.stdin.fileno(), 2) + +def cleanup_travis_junk(): + sys.stdout.close() + sys.stderr.close() + proc.terminate() -sys.stderr = sys.stdout -os.dup2(sys.stderr.fileno(), 2) +atexit.register(cleanup_travis_junk) # ----------------- @@ -54,7 +63,9 @@ def _argv(s, *args): def run(s, *args, **kwargs): argv = _argv(s, *args) print('Running: %s' % (argv,)) - return subprocess.check_call(argv, **kwargs) + ret = subprocess.check_call(argv, **kwargs) + print('Finished running: %s' % (argv,)) + return ret def get_output(s, *args, **kwargs): From 816da64df5f974bfd97f88c64133e867949eead7 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 12:49:19 +0000 Subject: [PATCH 173/662] tests: show task args in image_prep --- tests/image_prep/ansible.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/image_prep/ansible.cfg b/tests/image_prep/ansible.cfg index a3937825..8a8c47fa 100644 --- a/tests/image_prep/ansible.cfg +++ b/tests/image_prep/ansible.cfg @@ -2,3 +2,5 @@ [defaults] strategy_plugins = ../../ansible_mitogen/plugins/strategy retry_files_enabled = false +display_args_to_stdout = True +no_target_syslog = True From f87553b16545da7918300c00dc115880021f4597 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 12:49:47 +0000 Subject: [PATCH 174/662] tests: must set ansible_become_pass in synchronize.yml. --- tests/ansible/integration/action/synchronize.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ansible/integration/action/synchronize.yml b/tests/ansible/integration/action/synchronize.yml index 25649fbf..43bbb265 100644 --- a/tests/ansible/integration/action/synchronize.yml +++ b/tests/ansible/integration/action/synchronize.yml @@ -5,6 +5,7 @@ any_errors_fatal: true vars: ansible_user: mitogen__has_sudo_pubkey + ansible_become_pass: has_sudo_pubkey_password ansible_ssh_private_key_file: /tmp/synchronize-action-key tasks: # must copy git file to set proper file mode. From b29c8eaf2a2f41f96a4083e6555ca14e29061775 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 12:59:21 +0000 Subject: [PATCH 175/662] tests: allow passing -vvv to build_docker_images. --- tests/image_prep/build_docker_images.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/image_prep/build_docker_images.py b/tests/image_prep/build_docker_images.py index 94a17104..0ab722f4 100755 --- a/tests/image_prep/build_docker_images.py +++ b/tests/image_prep/build_docker_images.py @@ -6,9 +6,10 @@ Build the Docker images used for testing. import commands import os -import tempfile import shlex import subprocess +import sys +import tempfile BASEDIR = os.path.dirname(os.path.abspath(__file__)) @@ -42,7 +43,7 @@ with tempfile.NamedTemporaryFile() as fp: try: subprocess.check_call( cwd=BASEDIR, - args=sh('ansible-playbook -i %s -c docker setup.yml', fp.name), + args=sh('ansible-playbook -i %s -c docker setup.yml', fp.name) + sys.argv[1:], ) for container_id, label in label_by_id.items(): From 9ad022107e3b2dff8e85409532c87d2859e1c2cd Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 13:00:05 +0000 Subject: [PATCH 176/662] issue #414: disable test until rest of CI is healthy --- tests/ansible/integration/async/all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ansible/integration/async/all.yml b/tests/ansible/integration/async/all.yml index 61d2d35c..1a5b20ef 100644 --- a/tests/ansible/integration/async/all.yml +++ b/tests/ansible/integration/async/all.yml @@ -1,4 +1,4 @@ -- import_playbook: multiple_items_loop.yml +# - import_playbook: multiple_items_loop.yml - import_playbook: result_binary_producing_json.yml - import_playbook: result_binary_producing_junk.yml - import_playbook: result_shell_echo_hi.yml From 6bae5869239ac58371936741a3673e801d9bf0f9 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 13:06:01 +0000 Subject: [PATCH 177/662] tests: fix up Travis bodge for Python 2.6. --- .travis/ci_lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis/ci_lib.py b/.travis/ci_lib.py index d8d6827d..f5778161 100644 --- a/.travis/ci_lib.py +++ b/.travis/ci_lib.py @@ -45,9 +45,9 @@ proc = subprocess.Popen( os.dup2(proc.stdin.fileno(), 1) os.dup2(proc.stdin.fileno(), 2) -def cleanup_travis_junk(): - sys.stdout.close() - sys.stderr.close() +def cleanup_travis_junk(stdout=sys.stdout, stderr=sys.stderr, proc=proc): + stdout.close() + stderr.close() proc.terminate() atexit.register(cleanup_travis_junk) From 4d443e654b8a9a35a0da70ac5580925425eb249d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 13:12:31 +0000 Subject: [PATCH 178/662] tests: replace another shell script. --- tests/ansible/mitogen_ansible_playbook.py | 6 ++++++ tests/ansible/mitogen_ansible_playbook.sh | 3 --- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100755 tests/ansible/mitogen_ansible_playbook.py delete mode 100755 tests/ansible/mitogen_ansible_playbook.sh diff --git a/tests/ansible/mitogen_ansible_playbook.py b/tests/ansible/mitogen_ansible_playbook.py new file mode 100755 index 00000000..3af1791c --- /dev/null +++ b/tests/ansible/mitogen_ansible_playbook.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +import os +import subprocess +import sys +os.environ['ANSIBLE_STRATEGY'] = 'mitogen_linear' +subprocess.check_call(['./run_ansible_playbook.py'] + sys.argv[1:]) diff --git a/tests/ansible/mitogen_ansible_playbook.sh b/tests/ansible/mitogen_ansible_playbook.sh deleted file mode 100755 index 462d985b..00000000 --- a/tests/ansible/mitogen_ansible_playbook.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -export ANSIBLE_STRATEGY=mitogen_linear -exec ./run_ansible_playbook.py "$@" From 35092c5d35f9d2f661f8b1ec68a7e37e97960ee1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 13:15:20 +0000 Subject: [PATCH 179/662] tests: Unicode/bytes fixes for integration/connection/exec_command.yml --- ansible_mitogen/connection.py | 6 +++--- ansible_mitogen/target.py | 2 +- tests/ansible/integration/connection/exec_command.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index e017608e..f14576ad 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -914,9 +914,9 @@ class Connection(ansible.plugins.connection.ConnectionBase): emulate_tty=emulate_tty, ) - stderr += 'Shared connection to %s closed.%s' % ( - self._play_context.remote_addr, - ('\r\n' if emulate_tty else '\n'), + stderr += b'Shared connection to %s closed.%s' % ( + self._play_context.remote_addr.encode(), + (b'\r\n' if emulate_tty else b'\n'), ) return rc, stdout, stderr diff --git a/ansible_mitogen/target.py b/ansible_mitogen/target.py index 0e74f960..83069422 100644 --- a/ansible_mitogen/target.py +++ b/ansible_mitogen/target.py @@ -541,7 +541,7 @@ def exec_args(args, in_data='', chdir=None, shell=None, emulate_tty=False): if emulate_tty: stdout = stdout.replace(b'\n', b'\r\n') - return proc.returncode, stdout, stderr or '' + return proc.returncode, stdout, stderr or b'' def exec_command(cmd, in_data='', chdir=None, shell=None, emulate_tty=False): diff --git a/tests/ansible/integration/connection/exec_command.yml b/tests/ansible/integration/connection/exec_command.yml index 6a632961..105505d1 100644 --- a/tests/ansible/integration/connection/exec_command.yml +++ b/tests/ansible/integration/connection/exec_command.yml @@ -15,5 +15,5 @@ - assert: that: - out.result[0] == 0 - - out.result[1] == "hello, world\r\n" - - out.result[2].startswith("Shared connection to ") + - out.result[1].decode() == "hello, world\r\n" + - out.result[2].decode().startswith("Shared connection to ") From 0c3e48468b299c5b5d15b7c385c26837bbc62b8d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 13:33:33 +0000 Subject: [PATCH 180/662] tests: run disconnect_during_module.yml in subprocess Avoid entire run failing with unreachable --- .../connection/_disconnect_during_module.yml | 13 +++++++++++++ .../connection/disconnect_during_module.yml | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 tests/ansible/integration/connection/_disconnect_during_module.yml diff --git a/tests/ansible/integration/connection/_disconnect_during_module.yml b/tests/ansible/integration/connection/_disconnect_during_module.yml new file mode 100644 index 00000000..6bd8cd50 --- /dev/null +++ b/tests/ansible/integration/connection/_disconnect_during_module.yml @@ -0,0 +1,13 @@ +# issue 352: test ability to notice disconnection during a module invocation. +--- + +- name: integration/connection/_disconnect_during_module.yml + hosts: test-targets + gather_facts: no + any_errors_fatal: false + tasks: + - run_once: true # don't run against localhost + shell: | + kill -9 $PPID + register: out + ignore_errors: true diff --git a/tests/ansible/integration/connection/disconnect_during_module.yml b/tests/ansible/integration/connection/disconnect_during_module.yml index f2943b44..2b9c2c55 100644 --- a/tests/ansible/integration/connection/disconnect_during_module.yml +++ b/tests/ansible/integration/connection/disconnect_during_module.yml @@ -2,18 +2,23 @@ --- - name: integration/connection/disconnect_during_module.yml - hosts: test-targets localhost + hosts: test-targets gather_facts: no any_errors_fatal: false tasks: - - run_once: true # don't run against localhost - shell: | - kill -9 $PPID + - connection: local + command: | + ansible-playbook + -i "{{inventory_file}}" + integration/connection/_disconnect_during_module.yml + args: + chdir: ../.. register: out ignore_errors: true + - debug: var=out + - assert: that: - - out.msg.startswith('Mitogen was disconnected from the remote environment while a call was in-progress.') - - - meta: clear_host_errors + - out.rc == 4 + - "'Mitogen was disconnected from the remote environment while a call was in-progress.' in out.stdout" From c7931be524588eb1e47be9713a7e15149d7d8eba Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 14:05:34 +0000 Subject: [PATCH 181/662] issue #420: core: include PID in Latch cookie data. --- mitogen/core.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mitogen/core.py b/mitogen/core.py index 370fb742..0876cd8c 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -1778,16 +1778,16 @@ class Latch(object): self._cls_all_sockets.extend((rsock, wsock)) return rsock, wsock - COOKIE_SIZE = 33 - def _make_cookie(self): """ - Return a 33-byte string encoding the ID of the instance and the current - thread. This disambiguates legitimate wake-ups, accidental writes to - the FD, and buggy internal FD sharing. + Return a string encoding the ID of the instance and the current thread. + This disambiguates legitimate wake-ups, accidental writes to the FD, + and buggy internal FD sharing. """ ident = threading.currentThread().ident - return b(u'%016x-%016x' % (int(id(self)), ident)) + return b(u'%010d-%016x-%016x' % (os.getpid(), int(id(self)), ident)) + + COOKIE_SIZE = len(_make_cookie(None)) def get(self, timeout=None, block=True): """ From 50241a922fec63153723ef7cd5935883898e6b86 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 14:28:19 +0000 Subject: [PATCH 182/662] ansible: call on_fork() on broker shutdown; closes #420. --- ansible_mitogen/connection.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index f14576ad..569a5ad8 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -42,6 +42,7 @@ import ansible.errors import ansible.plugins.connection import ansible.utils.shlex +import mitogen.fork import mitogen.unix import mitogen.utils @@ -802,19 +803,32 @@ class Connection(ansible.plugins.connection.ConnectionBase): self.init_child_result = None self.chain = None - def close(self): + def _shutdown_broker(self): """ - Arrange for the mitogen.master.Router running in the worker to - gracefully shut down, and wait for shutdown to complete. Safe to call - multiple times. + Shutdown the broker thread during :meth:`close` or :meth:`reset`. """ - self._mitogen_reset(mode='put') if self.broker: self.broker.shutdown() self.broker.join() self.broker = None self.router = None + # #420: Ansible executes "meta" actions in the top-level process, + # meaning "reset_connection" will cause :class:`mitogen.core.Latch` FDs + # to be cached and subsequently erroneously shared by children on + # subsequent task forks. To handle that, call on_fork() to ensure any + # shared state is discarded. + mitogen.fork.on_fork() + + def close(self): + """ + Arrange for the mitogen.master.Router running in the worker to + gracefully shut down, and wait for shutdown to complete. Safe to call + multiple times. + """ + self._mitogen_reset(mode='put') + self._shutdown_broker() + reset_compat_msg = ( 'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later' ) @@ -835,6 +849,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): self._connect() self._mitogen_reset(mode='reset') + self._shutdown_broker() # Compatibility with Ansible 2.4 wait_for_connection plug-in. _reset = reset From 905fbe7cbb5c603912394b6a2713a89a2916e6d4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 14:31:11 +0000 Subject: [PATCH 183/662] issue #420: update Changelog. --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index bb1974a8..87ad7f00 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -186,6 +186,11 @@ Fixes * `#410 `_: the sudo method supports the SELinux ``--type`` and ``--role`` options. +* `#420 `_: if a :class:`Connection` + was constructed in the Ansible top-level process, for example while executing + ``meta: reset_connection``, resources could become undesirably shared in + subsequent children. + Core Library ~~~~~~~~~~~~ From f5f72b958f25a256251d044cab52597b09b043ae Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 15:02:55 +0000 Subject: [PATCH 184/662] tests: avoid -u command line parameter conflict --- tests/ansible/integration/stub_connections/mitogen_sudo.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ansible/integration/stub_connections/mitogen_sudo.yml b/tests/ansible/integration/stub_connections/mitogen_sudo.yml index b82b3ac2..cb530a56 100644 --- a/tests/ansible/integration/stub_connections/mitogen_sudo.yml +++ b/tests/ansible/integration/stub_connections/mitogen_sudo.yml @@ -10,6 +10,7 @@ - custom_python_detect_environment: vars: ansible_connection: mitogen_sudo + ansible_user: root ansible_become_exe: stub-sudo.py ansible_become_flags: --type=sometype --role=somerole register: out From 5f815ec6c425ca98abca4c92f7f81f22768853e1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 15:27:21 +0000 Subject: [PATCH 185/662] tests: try to fix PATH problem on Travis. --- tests/ansible/integration/stub_connections/setns_lxc.yml | 2 +- tests/ansible/integration/stub_connections/setns_lxd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml index a1feb4fe..32bb161e 100644 --- a/tests/ansible/integration/stub_connections/setns_lxc.yml +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -14,7 +14,7 @@ - include_tasks: _end_play_if_not_sudo_linux.yml - command: | - sudo -nE ansible + sudo -nE "{{lookup('env', 'VIRTUAL_ENV')}}/bin/ansible" -i localhost, -c setns -e mitogen_kind=lxc diff --git a/tests/ansible/integration/stub_connections/setns_lxd.yml b/tests/ansible/integration/stub_connections/setns_lxd.yml index b507f412..539eab11 100644 --- a/tests/ansible/integration/stub_connections/setns_lxd.yml +++ b/tests/ansible/integration/stub_connections/setns_lxd.yml @@ -14,7 +14,7 @@ - include_tasks: _end_play_if_not_sudo_linux.yml - command: | - sudo -nE ansible + sudo -nE "{{lookup('env', 'VIRTUAL_ENV')}}/bin/ansible" -i localhost, -c setns -e mitogen_kind=lxd From 1bb239189b74eac52c7a4306504beeb91e607724 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 17:17:16 +0000 Subject: [PATCH 186/662] tests: another attempt at working paths. --- tests/ansible/integration/stub_connections/setns_lxc.yml | 2 +- tests/ansible/integration/stub_connections/setns_lxd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml index 32bb161e..4bb21db7 100644 --- a/tests/ansible/integration/stub_connections/setns_lxc.yml +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -18,7 +18,7 @@ -i localhost, -c setns -e mitogen_kind=lxc - -e mitogen_lxc_info_path=stub-lxc-info.py + -e mitogen_lxc_info_path={{git_basedir}}/tests/data/stubs/stub-lxc-info.py -m shell -a "echo hi" localhost diff --git a/tests/ansible/integration/stub_connections/setns_lxd.yml b/tests/ansible/integration/stub_connections/setns_lxd.yml index 539eab11..e430daa9 100644 --- a/tests/ansible/integration/stub_connections/setns_lxd.yml +++ b/tests/ansible/integration/stub_connections/setns_lxd.yml @@ -18,7 +18,7 @@ -i localhost, -c setns -e mitogen_kind=lxd - -e mitogen_lxc_path=stub-lxc.py + -e mitogen_lxc_path={{git_basedir}}/tests/data/stubs/stub-lxc.py -m shell -a "echo hi" localhost From de7d4e09087bf7b10605459ea1b4b893c415e5cf Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 17:21:48 +0000 Subject: [PATCH 187/662] setns: decode utility command output for 3.x. --- mitogen/setns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitogen/setns.py b/mitogen/setns.py index be87e063..0c94cd0b 100644 --- a/mitogen/setns.py +++ b/mitogen/setns.py @@ -69,7 +69,7 @@ def _run_command(args): output, _ = proc.communicate() if not proc.returncode: - return output + return output.decode('utf-8', 'replace') raise Error("%s exitted with status %d: %s", mitogen.parent.Argv(args), proc.returncode, output) From fcdfd5f107d8acb26c544e5be24cee1eeb26a05c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 17:36:48 +0000 Subject: [PATCH 188/662] tests: fix disconnect_cleanup.yml target count assumption --- .../integration/context_service/disconnect_cleanup.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ansible/integration/context_service/disconnect_cleanup.yml b/tests/ansible/integration/context_service/disconnect_cleanup.yml index b657a0dc..359be3fb 100644 --- a/tests/ansible/integration/context_service/disconnect_cleanup.yml +++ b/tests/ansible/integration/context_service/disconnect_cleanup.yml @@ -28,7 +28,7 @@ register: out - assert: - that: out.dump|length == 4 # ssh account + 3 sudo accounts + that: out.dump|length == (play_hosts|length) * 4 # ssh account + 3 sudo accounts - meta: reset_connection @@ -43,4 +43,4 @@ register: out - assert: - that: out.dump|length == 1 # just the ssh account + that: out.dump|length == play_hosts|length # just the ssh account From ee2d10375d4629c2b83b88b678ad896e1c707c48 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 17:45:19 +0000 Subject: [PATCH 189/662] tests: don't run reset_connection tests on <2.5.6. --- .../ansible/integration/context_service/disconnect_cleanup.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/ansible/integration/context_service/disconnect_cleanup.yml b/tests/ansible/integration/context_service/disconnect_cleanup.yml index 359be3fb..dbcd27cc 100644 --- a/tests/ansible/integration/context_service/disconnect_cleanup.yml +++ b/tests/ansible/integration/context_service/disconnect_cleanup.yml @@ -8,6 +8,9 @@ - meta: end_play when: not is_mitogen + - meta: end_play + when: ansible_version.full < '2.5.6' + # Start with a clean slate. - mitogen_shutdown_all: From acf0b048767aa721426896596443959045ae6d87 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 18:50:47 +0000 Subject: [PATCH 190/662] tests: run some playbooks against only one target. --- tests/ansible/integration/ssh/variables.yml | 2 +- .../integration/strategy/_mixed_mitogen_vanilla.yml | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/ansible/integration/ssh/variables.yml b/tests/ansible/integration/ssh/variables.yml index dc4fe434..6bfcb2a5 100644 --- a/tests/ansible/integration/ssh/variables.yml +++ b/tests/ansible/integration/ssh/variables.yml @@ -3,7 +3,7 @@ # whatever reason. - name: integration/ssh/variables.yml - hosts: test-targets + hosts: test-targets[0] connection: local vars: # ControlMaster has the effect of caching the previous auth to the same diff --git a/tests/ansible/integration/strategy/_mixed_mitogen_vanilla.yml b/tests/ansible/integration/strategy/_mixed_mitogen_vanilla.yml index 7ac39e8e..1ec76fd1 100644 --- a/tests/ansible/integration/strategy/_mixed_mitogen_vanilla.yml +++ b/tests/ansible/integration/strategy/_mixed_mitogen_vanilla.yml @@ -2,9 +2,10 @@ # issue #294: ensure running mixed vanilla/Mitogen succeeds. - name: integration/strategy/_mixed_mitogen_vanilla.yml (mitogen_linear) - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true strategy: mitogen_linear + run_once: true tasks: - custom_python_detect_environment: register: out @@ -15,8 +16,9 @@ - assert: that: strategy == 'ansible.plugins.strategy.mitogen_linear.StrategyModule' + - name: integration/strategy/_mixed_mitogen_vanilla.yml (linear) - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true strategy: linear tasks: @@ -31,7 +33,7 @@ - name: integration/strategy/_mixed_mitogen_vanilla.yml (mitogen_linear) - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true strategy: mitogen_linear tasks: From 8972dbb7b988443eb0abcc825d9c892252ab0866 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 19:04:09 +0000 Subject: [PATCH 191/662] tests: more Ansible fixes. --- .travis/ansible_tests.py | 5 +++-- .../integration/context_service/disconnect_cleanup.yml | 2 +- .../ansible/integration/strategy/_mixed_vanilla_mitogen.yml | 6 +++--- .../ansible/integration/strategy/mixed_vanilla_mitogen.yml | 4 +++- tests/ansible/run_ansible_playbook.py | 5 +++++ 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.travis/ansible_tests.py b/.travis/ansible_tests.py index 8bb8f6a1..e664ec8b 100755 --- a/.travis/ansible_tests.py +++ b/.travis/ansible_tests.py @@ -63,5 +63,6 @@ with ci_lib.Fold('job_setup'): with ci_lib.Fold('ansible'): - run('/usr/bin/time ./run_ansible_playbook.py all.yml -i "%s" %s', - HOSTS_DIR, ' '.join(sys.argv[1:])) + playbook = os.environ.get('PLAYBOOK', 'all.yml') + run('/usr/bin/time ./run_ansible_playbook.py %s -i "%s" %s', + playbook, HOSTS_DIR, ' '.join(sys.argv[1:])) diff --git a/tests/ansible/integration/context_service/disconnect_cleanup.yml b/tests/ansible/integration/context_service/disconnect_cleanup.yml index dbcd27cc..575358f6 100644 --- a/tests/ansible/integration/context_service/disconnect_cleanup.yml +++ b/tests/ansible/integration/context_service/disconnect_cleanup.yml @@ -2,7 +2,7 @@ # state of dependent contexts (e.g. sudo, connection delegation, ..). - name: integration/context_service/disconnect_cleanup.yml - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true tasks: - meta: end_play diff --git a/tests/ansible/integration/strategy/_mixed_vanilla_mitogen.yml b/tests/ansible/integration/strategy/_mixed_vanilla_mitogen.yml index 891787af..babcab3f 100644 --- a/tests/ansible/integration/strategy/_mixed_vanilla_mitogen.yml +++ b/tests/ansible/integration/strategy/_mixed_vanilla_mitogen.yml @@ -2,7 +2,7 @@ # issue #294: ensure running mixed vanilla/Mitogen succeeds. - name: integration/strategy/_mixed_vanilla_mitogen.yml (linear) - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true strategy: linear tasks: @@ -16,7 +16,7 @@ that: strategy == 'ansible.plugins.strategy.linear.StrategyModule' - name: integration/strategy/_mixed_vanilla_mitogen.yml (mitogen_linear) - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true strategy: mitogen_linear tasks: @@ -31,7 +31,7 @@ - name: integration/strategy/_mixed_vanilla_mitogen.yml (linear) - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true strategy: linear tasks: diff --git a/tests/ansible/integration/strategy/mixed_vanilla_mitogen.yml b/tests/ansible/integration/strategy/mixed_vanilla_mitogen.yml index 61a55825..a7aadb19 100644 --- a/tests/ansible/integration/strategy/mixed_vanilla_mitogen.yml +++ b/tests/ansible/integration/strategy/mixed_vanilla_mitogen.yml @@ -1,12 +1,13 @@ - name: integration/strategy/mixed_vanilla_mitogen.yml (linear->mitogen->linear) - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true tasks: - connection: local command: | ansible-playbook -i "{{inventory_file}}" + -vvv integration/strategy/_mixed_mitogen_vanilla.yml args: chdir: ../.. @@ -16,6 +17,7 @@ command: | ansible-playbook -i "{{inventory_file}}" + -vvv integration/strategy/_mixed_vanilla_mitogen.yml args: chdir: ../.. diff --git a/tests/ansible/run_ansible_playbook.py b/tests/ansible/run_ansible_playbook.py index 2f85c8ac..a2eaded6 100755 --- a/tests/ansible/run_ansible_playbook.py +++ b/tests/ansible/run_ansible_playbook.py @@ -12,6 +12,11 @@ GIT_BASEDIR = os.path.dirname( ) ) +# Ensure VIRTUAL_ENV is exported. +os.environ.setdefault( + 'VIRTUAL_ENV', + os.path.dirname(os.path.dirname(sys.executable)) +) # Used by delegate_to.yml to ensure "sudo -E" preserves environment. os.environ['I_WAS_PRESERVED'] = '1' From 79ca67aadd73509ec3027711829fa916354058c1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 19:27:00 +0000 Subject: [PATCH 192/662] tests: disable connection tests for non-Mitogen --- .../integration/connection/disconnect_during_module.yml | 3 +++ .../integration/connection/disconnect_resets_connection.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/ansible/integration/connection/disconnect_during_module.yml b/tests/ansible/integration/connection/disconnect_during_module.yml index 2b9c2c55..339dc6f5 100644 --- a/tests/ansible/integration/connection/disconnect_during_module.yml +++ b/tests/ansible/integration/connection/disconnect_during_module.yml @@ -6,6 +6,9 @@ gather_facts: no any_errors_fatal: false tasks: + - meta: end_play + when: not is_mitogen + - connection: local command: | ansible-playbook diff --git a/tests/ansible/integration/connection/disconnect_resets_connection.yml b/tests/ansible/integration/connection/disconnect_resets_connection.yml index 9e186182..5f02a8d5 100644 --- a/tests/ansible/integration/connection/disconnect_resets_connection.yml +++ b/tests/ansible/integration/connection/disconnect_resets_connection.yml @@ -14,6 +14,9 @@ gather_facts: no any_errors_fatal: true tasks: + - meta: end_play + when: not is_mitogen + - mitogen_action_script: script: | import sys From 704e6c0b2c84395039379c10eae985e2de69b564 Mon Sep 17 00:00:00 2001 From: dw Date: Mon, 5 Nov 2018 20:49:58 +0000 Subject: [PATCH 193/662] Set up CI with Azure Pipelines --- azure-pipelines.yml | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..59d5d931 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,55 @@ +# Python package +# Create and test a Python package on multiple Python versions. +# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/python + +jobs: + +- job: 'Test' + pool: + vmImage: 'Ubuntu 16.04' + strategy: + matrix: + Python27: + python.version: '2.7' + Python35: + python.version: '3.5' + Python36: + python.version: '3.6' + Python37: + python.version: '3.7' + maxParallel: 4 + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + architecture: 'x64' + + - script: python -m pip install --upgrade pip && pip install -r requirements.txt + displayName: 'Install dependencies' + + - script: | + pip install pytest + pytest tests --doctest-modules --junitxml=junit/test-results.xml + displayName: 'pytest' + + - task: PublishTestResults@2 + inputs: + testResultsFiles: '**/test-results.xml' + testRunTitle: 'Python $(python.version)' + condition: succeededOrFailed() + +- job: 'Publish' + dependsOn: 'Test' + pool: + vmImage: 'Ubuntu 16.04' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.x' + architecture: 'x64' + + - script: python setup.py sdist + displayName: 'Build sdist' From 01c4f3fee13c32e1b46908c76f0275376ea82465 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 01:47:36 +0000 Subject: [PATCH 194/662] core: rearrange stdio setup to cope with buffering; closes #422 --- docs/changelog.rst | 4 ++++ mitogen/core.py | 42 +++++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 87ad7f00..52388ed0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -245,6 +245,10 @@ Core Library execution of its :keyword:`finally` block was delayed on Python 3. Now callers explicitly close the generator when finished. +* `#421 `_: the fork method could + fail to start if :data:`sys.stdout` was opened in block buffered mode, and + buffered data was pending in the parent prior to fork. + * `16ca111e `_: handle OpenSSH 7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present. diff --git a/mitogen/core.py b/mitogen/core.py index 0876cd8c..b1519c90 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2808,28 +2808,32 @@ class ExternalContext(object): mitogen.parent_ids = self.config['parent_ids'][:] mitogen.parent_id = mitogen.parent_ids[0] - def _setup_stdio(self): - # We must open this prior to closing stdout, otherwise it will recycle - # a standard handle, the dup2() will not error, and on closing it, we - # lose a standrd handle, causing later code to again recycle a standard - # handle. - fp = open('/dev/null') + def _nullify_stdio(self): + """ + Open /dev/null to replace stdin, and stdout/stderr temporarily. In case + of odd startup, assume we may be allocated a standard handle. + """ + fd = os.open('/dev/null', os.O_RDWR) + try: + for stdfd in (0, 1, 2): + if fd != stdfd: + os.dup2(fd, stdfd) + finally: + if fd not in (0, 1, 2): + os.close(fd) + def _setup_stdio(self): # When sys.stdout was opened by the runtime, overwriting it will not - # cause close to be called. However when forking from a child that - # previously used fdopen, overwriting it /will/ cause close to be - # called. So we must explicitly close it before IoLogger overwrites the - # file descriptor, otherwise the assignment below will cause stdout to - # be closed. + # close FD 1. However when forking from a child that previously used + # fdopen(), overwriting it /will/ close FD 1. So we must swallow the + # close before IoLogger overwrites FD 1, otherwise its new FD 1 will be + # clobbered. Additionally, stdout must be replaced with /dev/null prior + # to stdout.close(), since if block buffering was active in the parent, + # any pre-fork buffered data will be flushed on close(), corrupting the + # connection to the parent. + self._nullify_stdio() sys.stdout.close() - sys.stdout = None - - try: - os.dup2(fp.fileno(), 0) - os.dup2(fp.fileno(), 1) - os.dup2(fp.fileno(), 2) - finally: - fp.close() + self._nullify_stdio() self.stdout_log = IoLogger(self.broker, 'stdout', 1) self.stderr_log = IoLogger(self.broker, 'stderr', 2) From 5233c47eba95167c01356b655f9a206560e2e8f2 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 20:02:18 +0000 Subject: [PATCH 195/662] docs: Changelog typo --- docs/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 52388ed0..cdc160be 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -159,7 +159,7 @@ Fixes * `#334 `_: the SSH method tilde-expands private key paths using Ansible's logic. Previously the path - was passed unmodified to SSH, which expanded it using :func:`os.getpwent`. + was passed unmodified to SSH, which expanded it using :func:`pwd.getpwnam`. This differs from :func:`os.path.expanduser`, which uses the ``HOME`` environment variable if it is set, causing behaviour to diverge when Ansible was invoked across user accounts via ``sudo``. @@ -180,8 +180,8 @@ Fixes recurrence. * `#409 `_: the LXC and LXD methods - support ``mitogen_lxc_path`` and ``mitogen_lxc_attach`` variables to control - the location of third pary utilities. + support ``mitogen_lxc_path`` and ``mitogen_lxc_attach_path`` variables to + control the location of third pary utilities. * `#410 `_: the sudo method supports the SELinux ``--type`` and ``--role`` options. From 374fd72dbb0ddb0fe8902e70b8c5724a669e51ac Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 20:32:54 +0000 Subject: [PATCH 196/662] tests: disable mtime test on vanilla --- tests/ansible/integration/connection/_put_file.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ansible/integration/connection/_put_file.yml b/tests/ansible/integration/connection/_put_file.yml index a0fea4ed..947417b9 100644 --- a/tests/ansible/integration/connection/_put_file.yml +++ b/tests/ansible/integration/connection/_put_file.yml @@ -19,4 +19,4 @@ - assert: that: - original.stat.checksum == copied.stat.checksum - - original.stat.mtime|int == copied.stat.mtime|int + - (not is_mitogen) or (original.stat.mtime|int == copied.stat.mtime|int) From 4b61e5af02b9e79141ba38cb3ad63591809c5190 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 23:47:53 +0000 Subject: [PATCH 197/662] tests: run FD and thread checks on every test case. Trying to hunt down weirdness on Azure. --- tests/testlib.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/testlib.py b/tests/testlib.py index 1406bf2d..b9a1bec5 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -282,11 +282,10 @@ class TestCase(unittest2.TestCase): 'mitogen.master.join_thread_async' ]) - @classmethod - def _teardown_check_threads(cls): + def _teardown_check_threads(self): counts = {} for thread in threading.enumerate(): - assert thread.name in cls.ALLOWED_THREADS, \ + assert thread.name in self.ALLOWED_THREADS, \ 'Found thread %r still running after tests.' % (thread.name,) counts[thread.name] = counts.get(thread.name, 0) + 1 @@ -294,20 +293,18 @@ class TestCase(unittest2.TestCase): assert counts[name] == 1, \ 'Found %d copies of thread %r running after tests.' % (name,) - @classmethod - def _teardown_check_fds(cls): + def _teardown_check_fds(self): mitogen.core.Latch._on_fork() - if get_fd_count() != cls._fd_count_before: + if get_fd_count() != self._fd_count_before: import os; os.system('lsof -p %s' % (os.getpid(),)) assert 0, "%s leaked FDs. Count before: %s, after: %s" % ( - cls, cls._fd_count_before, get_fd_count(), + self, self._fd_count_before, get_fd_count(), ) - @classmethod - def tearDownClass(cls): - super(TestCase, cls).tearDownClass() - cls._teardown_check_threads() - cls._teardown_check_fds() + def tearDown(self): + self._teardown_check_threads() + self._teardown_check_fds() + super(TestCase, self).tearDown() def assertRaises(self, exc, func, *args, **kwargs): """Like regular assertRaises, except return the exception that was From 3f414d5967ea9215319f727db33ce46cf1fb0b2d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 21:01:10 +0000 Subject: [PATCH 198/662] tests: rename .travis -> .ci, move Azure config into .ci --- {.travis => .ci}/ansible_tests.py | 0 azure-pipelines.yml => .ci/azure-pipelines.yml | 0 {.travis => .ci}/ci_lib.py | 0 {.travis => .ci}/debops_common_tests.sh | 0 {.travis => .ci}/mitogen_tests.sh | 0 .travis.yml | 6 +++--- 6 files changed, 3 insertions(+), 3 deletions(-) rename {.travis => .ci}/ansible_tests.py (100%) rename azure-pipelines.yml => .ci/azure-pipelines.yml (100%) rename {.travis => .ci}/ci_lib.py (100%) rename {.travis => .ci}/debops_common_tests.sh (100%) rename {.travis => .ci}/mitogen_tests.sh (100%) diff --git a/.travis/ansible_tests.py b/.ci/ansible_tests.py similarity index 100% rename from .travis/ansible_tests.py rename to .ci/ansible_tests.py diff --git a/azure-pipelines.yml b/.ci/azure-pipelines.yml similarity index 100% rename from azure-pipelines.yml rename to .ci/azure-pipelines.yml diff --git a/.travis/ci_lib.py b/.ci/ci_lib.py similarity index 100% rename from .travis/ci_lib.py rename to .ci/ci_lib.py diff --git a/.travis/debops_common_tests.sh b/.ci/debops_common_tests.sh similarity index 100% rename from .travis/debops_common_tests.sh rename to .ci/debops_common_tests.sh diff --git a/.travis/mitogen_tests.sh b/.ci/mitogen_tests.sh similarity index 100% rename from .travis/mitogen_tests.sh rename to .ci/mitogen_tests.sh diff --git a/.travis.yml b/.travis.yml index 95d9c64c..8e4a8d51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,10 +19,10 @@ install: script: - | - if [ -f "${TRAVIS_BUILD_DIR}/.travis/${MODE}_tests.sh" ]; then - ${TRAVIS_BUILD_DIR}/.travis/${MODE}_tests.sh; + if [ -f "${TRAVIS_BUILD_DIR}/.ci/${MODE}_tests.sh" ]; then + ${TRAVIS_BUILD_DIR}/.ci/${MODE}_tests.sh; else - ${TRAVIS_BUILD_DIR}/.travis/${MODE}_tests.py; + ${TRAVIS_BUILD_DIR}/.ci/${MODE}_tests.py; fi From 0b86c4e45fd353cb60de1cd6f12362b6e66e2a98 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 5 Nov 2018 21:09:54 +0000 Subject: [PATCH 199/662] tests: basic (incomplete) Azure Pipelines config --- .ci/ansible_tests.py | 3 +- .ci/azure-pipelines.yml | 93 +++++++++++++++++++++++++++-------------- .ci/ci_lib.py | 34 +++++++-------- .ci/mitogen_tests.sh | 2 +- .ci/prep_azure.py | 36 ++++++++++++++++ 5 files changed, 118 insertions(+), 50 deletions(-) create mode 100755 .ci/prep_azure.py diff --git a/.ci/ansible_tests.py b/.ci/ansible_tests.py index e664ec8b..d7730ba6 100755 --- a/.ci/ansible_tests.py +++ b/.ci/ansible_tests.py @@ -41,6 +41,7 @@ with ci_lib.Fold('job_setup'): run("mkdir %s", HOSTS_DIR) run("ln -s %s/hosts/common-hosts %s", TESTS_DIR, HOSTS_DIR) + docker_hostname = ci_lib.get_docker_hostname() with open(os.path.join(HOSTS_DIR, 'target'), 'w') as fp: fp.write('[test-targets]\n') for i, distro in enumerate(ci_lib.DISTROS): @@ -51,7 +52,7 @@ with ci_lib.Fold('job_setup'): "ansible_password=has_sudo_nopw_password" "\n" % ( distro, - ci_lib.DOCKER_HOSTNAME, + docker_hostname, BASE_PORT + i, )) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 59d5d931..2a22bdad 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -5,51 +5,82 @@ jobs: -- job: 'Test' +- job: 'MitogenTests' pool: vmImage: 'Ubuntu 16.04' strategy: matrix: - Python27: + Mitogen27Debian_27: python.version: '2.7' - Python35: - python.version: '3.5' - Python36: + MODE: mitogen + DISTRO: debian + + MitogenPy27CentOS6_26: + python.version: '2.7' + MODE: mitogen + DISTRO: centos6 + + #Py26CentOS7: + #python.version: '2.7' + #MODE: mitogen + #DISTRO: centos6 + + Mitogen36CentOS6_26: python.version: '3.6' - Python37: - python.version: '3.7' - maxParallel: 4 + MODE: mitogen + DISTRO: centos6 - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: 'x64' + DebOps_2460_27_27: + python.version: '2.7' + MODE: debops_common + VER: 2.4.6.0 - - script: python -m pip install --upgrade pip && pip install -r requirements.txt - displayName: 'Install dependencies' + DebOps_262_36_27: + python.version: '3.6' + MODE: debops_common + VER: 2.6.2 - - script: | - pip install pytest - pytest tests --doctest-modules --junitxml=junit/test-results.xml - displayName: 'pytest' + Ansible_2460_26: + python.version: '2.7' + MODE: ansible + VER: 2.4.6.0 - - task: PublishTestResults@2 - inputs: - testResultsFiles: '**/test-results.xml' - testRunTitle: 'Python $(python.version)' - condition: succeededOrFailed() + Ansible_262_26: + python.version: '2.7' + MODE: ansible + VER: 2.6.2 -- job: 'Publish' - dependsOn: 'Test' - pool: - vmImage: 'Ubuntu 16.04' + Ansible_2460_36: + python.version: '3.6' + MODE: ansible + VER: 2.4.6.0 + + Ansible_262_36: + python.version: '3.6' + MODE: ansible + VER: 2.6.2 + + Vanilla_262_27: + python.version: '2.7' + MODE: ansible + VER: 2.6.2 + DISTROS: debian + STRATEGY: linear steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.x' + versionSpec: '$(python.version)' architecture: 'x64' - - script: python setup.py sdist - displayName: 'Build sdist' + - script: .ci/prep_azure.py + displayName: "Install requirements." + + - script: | + export TRAVIS_BUILD_DIR=`pwd` + if [ -f ".ci/$(MODE)_tests.sh" ]; then + .ci/$(MODE)_tests.sh; + else + .ci/$(MODE)_tests.py; + fi + displayName: Run tests. diff --git a/.ci/ci_lib.py b/.ci/ci_lib.py index f5778161..d7d6e09b 100644 --- a/.ci/ci_lib.py +++ b/.ci/ci_lib.py @@ -10,8 +10,6 @@ import shlex import shutil import tempfile -import os -os.system('curl -H Metadata-Flavor:Google http://metadata.google.internal/computeMetadata/v1/instance/machine-type') # # check_output() monkeypatch cutpasted from testlib.py @@ -37,20 +35,21 @@ if not hasattr(subprocess, 'check_output'): # Force stdout FD 1 to be a pipe, so tools like pip don't spam progress bars. -proc = subprocess.Popen( - args=['stdbuf', '-oL', 'cat'], - stdin=subprocess.PIPE -) +if sys.platform.startswith('linux'): + proc = subprocess.Popen( + args=['stdbuf', '-oL', 'cat'], + stdin=subprocess.PIPE + ) -os.dup2(proc.stdin.fileno(), 1) -os.dup2(proc.stdin.fileno(), 2) + os.dup2(proc.stdin.fileno(), 1) + os.dup2(proc.stdin.fileno(), 2) -def cleanup_travis_junk(stdout=sys.stdout, stderr=sys.stderr, proc=proc): - stdout.close() - stderr.close() - proc.terminate() + def cleanup_travis_junk(stdout=sys.stdout, stderr=sys.stderr, proc=proc): + stdout.close() + stderr.close() + proc.terminate() -atexit.register(cleanup_travis_junk) + atexit.register(cleanup_travis_junk) # ----------------- @@ -113,10 +112,11 @@ os.environ['PYTHONPATH'] = '%s:%s' % ( GIT_ROOT ) -DOCKER_HOSTNAME = subprocess.check_output([ - sys.executable, - os.path.join(GIT_ROOT, 'tests/show_docker_hostname.py'), -]).decode().strip() +def get_docker_hostname(): + return subprocess.check_output([ + sys.executable, + os.path.join(GIT_ROOT, 'tests/show_docker_hostname.py'), + ]).decode().strip() # SSH passes these through to the container when run interactively, causing # stdout to get messed up with libc warnings. diff --git a/.ci/mitogen_tests.sh b/.ci/mitogen_tests.sh index db393d73..33ee16ba 100755 --- a/.ci/mitogen_tests.sh +++ b/.ci/mitogen_tests.sh @@ -2,4 +2,4 @@ # Run the Mitogen tests. MITOGEN_TEST_DISTRO="${DISTRO:-debian}" -MITOGEN_LOG_LEVEL=debug PYTHONPATH=. ${TRAVIS_BUILD_DIR}/run_tests -vvv +MITOGEN_LOG_LEVEL=debug PYTHONPATH=. ${TRAVIS_BUILD_DIR}/run_tests -v diff --git a/.ci/prep_azure.py b/.ci/prep_azure.py new file mode 100755 index 00000000..164e04e3 --- /dev/null +++ b/.ci/prep_azure.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# Run preparation steps in parallel. + +import subprocess +import ci_lib + +subprocess.check_call( + 'echo force-unsafe-io | sudo tee /etc/dpkg/dpkg.cfg.d/nosync', + shell=True, +) + +procs = [ + subprocess.Popen( + 'pip install -r dev_requirements.txt 2>&1 | cat', + shell=True, + ), + subprocess.Popen( + """ + sudo add-apt-repository ppa:deadsnakes/ppa && \ + ( sudo apt-get update 2>&1 | cat ) && \ + sudo apt-get -y install \ + python2.6 python2.6-dev libsasl2-dev libldap2-dev 2>&1 | cat + """, + shell=True, + ) +] + +procs += [ + subprocess.Popen( + 'docker pull mitogen/%s-test 2>&1 | cat' % (distro,), + shell=True + ) + for distro in ci_lib.DISTROS +] + +assert [proc.wait() for proc in procs] == [0] * len(procs) From a717c5406cec70c2a3d0f250e2870aeabfb0ef1e Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 04:43:56 +0000 Subject: [PATCH 200/662] tests: split etc_environment test in two Turns out nobody supports ~/.pam_environment any more. Keep the behaviour around for the time being. --- .../runner/_etc_environment_global.yml | 35 +++++++++ .../runner/_etc_environment_user.yml | 32 ++++++++ .../integration/runner/etc_environment.yml | 77 ++----------------- 3 files changed, 73 insertions(+), 71 deletions(-) create mode 100644 tests/ansible/integration/runner/_etc_environment_global.yml create mode 100644 tests/ansible/integration/runner/_etc_environment_user.yml diff --git a/tests/ansible/integration/runner/_etc_environment_global.yml b/tests/ansible/integration/runner/_etc_environment_global.yml new file mode 100644 index 00000000..e24a038a --- /dev/null +++ b/tests/ansible/integration/runner/_etc_environment_global.yml @@ -0,0 +1,35 @@ +# /etc/environment + +- file: + path: /etc/environment + state: absent + become: true + +- shell: echo $MAGIC_ETC_ENV + register: echo + +- assert: + that: echo.stdout == "" + +- copy: + dest: /etc/environment + content: | + MAGIC_ETC_ENV=555 + become: true + +- shell: echo $MAGIC_ETC_ENV + register: echo + +- assert: + that: echo.stdout == "555" + +- file: + path: /etc/environment + state: absent + become: true + +- shell: echo $MAGIC_ETC_ENV + register: echo + +- assert: + that: echo.stdout == "" diff --git a/tests/ansible/integration/runner/_etc_environment_user.yml b/tests/ansible/integration/runner/_etc_environment_user.yml new file mode 100644 index 00000000..ca1dc5cc --- /dev/null +++ b/tests/ansible/integration/runner/_etc_environment_user.yml @@ -0,0 +1,32 @@ +# ~/.pam_environment + +- file: + path: ~/.pam_environment + state: absent + +- shell: echo $MAGIC_PAM_ENV + register: echo + +- assert: + that: echo.stdout == "" + +- copy: + dest: ~/.pam_environment + content: | + MAGIC_PAM_ENV=321 + +- shell: echo $MAGIC_PAM_ENV + register: echo + +- assert: + that: echo.stdout == "321" + +- file: + path: ~/.pam_environment + state: absent + +- shell: echo $MAGIC_PAM_ENV + register: echo + +- assert: + that: echo.stdout == "" diff --git a/tests/ansible/integration/runner/etc_environment.yml b/tests/ansible/integration/runner/etc_environment.yml index 0037698a..7eb405cb 100644 --- a/tests/ansible/integration/runner/etc_environment.yml +++ b/tests/ansible/integration/runner/etc_environment.yml @@ -3,78 +3,13 @@ # but less likely to brick a development workstation - name: integration/runner/etc_environment.yml - hosts: test-targets + hosts: test-targets[0] any_errors_fatal: true gather_facts: true tasks: - # ~/.pam_environment + - include_tasks: _etc_environment_user.yml + when: ansible_system == "Linux" and is_mitogen - - file: - path: ~/.pam_environment - state: absent - - - shell: echo $MAGIC_PAM_ENV - register: echo - - - assert: - that: echo.stdout == "" - - - copy: - dest: ~/.pam_environment - content: | - MAGIC_PAM_ENV=321 - - - shell: echo $MAGIC_PAM_ENV - register: echo - - - assert: - that: echo.stdout == "321" - - - file: - path: ~/.pam_environment - state: absent - - - shell: echo $MAGIC_PAM_ENV - register: echo - - - assert: - that: echo.stdout == "" - - - # /etc/environment - - meta: end_play - when: ansible_virtualization_type != "docker" - - - file: - path: /etc/environment - state: absent - become: true - - - shell: echo $MAGIC_ETC_ENV - register: echo - - - assert: - that: echo.stdout == "" - - - copy: - dest: /etc/environment - content: | - MAGIC_ETC_ENV=555 - become: true - - - shell: echo $MAGIC_ETC_ENV - register: echo - - - assert: - that: echo.stdout == "555" - - - file: - path: /etc/environment - state: absent - become: true - - - shell: echo $MAGIC_ETC_ENV - register: echo - - - assert: - that: echo.stdout == "" + - include_tasks: _etc_environment_global.yml + # Don't destroy laptops. + when: ansible_virtualization_type == "docker" From 8f1f3de1231f424dce269fef1e14d99a9c26cdda Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 12:43:12 +0000 Subject: [PATCH 201/662] tests: Pythonize env_wrapper.sh, clean up local_test --- tests/data/env_wrapper.sh | 12 ------------ tests/data/stubs/stub-python.py | 15 +++++++++++++++ tests/local_test.py | 29 ++++++++++------------------- 3 files changed, 25 insertions(+), 31 deletions(-) delete mode 100755 tests/data/env_wrapper.sh create mode 100755 tests/data/stubs/stub-python.py diff --git a/tests/data/env_wrapper.sh b/tests/data/env_wrapper.sh deleted file mode 100755 index afb523f0..00000000 --- a/tests/data/env_wrapper.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# This script exists to test the behavior of Stream.python_path being set to a -# list. It sets an environmnt variable that we can detect, then executes any -# arguments passed to it. -export EXECUTED_VIA_ENV_WRAPPER=1 -if [ "${1:0:1}" == "-" ]; then - exec "$PYTHON" "$@" -else - export ENV_WRAPPER_FIRST_ARG="$1" - shift - exec "$@" -fi diff --git a/tests/data/stubs/stub-python.py b/tests/data/stubs/stub-python.py new file mode 100755 index 00000000..d9239c2b --- /dev/null +++ b/tests/data/stubs/stub-python.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import json +import os +import subprocess +import sys + +os.environ['ORIGINAL_ARGV'] = json.dumps(sys.argv) +os.environ['THIS_IS_STUB_PYTHON'] = '1' + +if sys.argv[1].startswith('-'): + os.execvp(sys.executable, [sys.executable] + sys.argv[1:]) +else: + os.environ['STUB_PYTHON_FIRST_ARG'] = sys.argv.pop(1) + os.execvp(sys.executable, sys.argv[1:]) diff --git a/tests/local_test.py b/tests/local_test.py index 5a620e52..fe2bd149 100644 --- a/tests/local_test.py +++ b/tests/local_test.py @@ -5,11 +5,8 @@ import sys import unittest2 import mitogen -import mitogen.ssh -import mitogen.utils import testlib -import plain_old_module def get_sys_executable(): @@ -20,43 +17,37 @@ def get_os_environ(): return dict(os.environ) -class LocalTest(testlib.RouterMixin, testlib.TestCase): - stream_class = mitogen.ssh.Stream +class ConstructionTest(testlib.RouterMixin, testlib.TestCase): + stub_python_path = testlib.data_path('stubs/stub-python.py') def test_stream_name(self): context = self.router.local() pid = context.call(os.getpid) self.assertEquals('local.%d' % (pid,), context.name) - -class PythonPathTest(testlib.RouterMixin, testlib.TestCase): - stream_class = mitogen.ssh.Stream - - def test_inherited(self): + def test_python_path_inherited(self): context = self.router.local() self.assertEquals(sys.executable, context.call(get_sys_executable)) - def test_string(self): - os.environ['PYTHON'] = sys.executable + def test_python_path_string(self): context = self.router.local( - python_path=testlib.data_path('env_wrapper.sh'), + python_path=self.stub_python_path, ) - self.assertEquals(sys.executable, context.call(get_sys_executable)) env = context.call(get_os_environ) - self.assertEquals('1', env['EXECUTED_VIA_ENV_WRAPPER']) + self.assertEquals('1', env['THIS_IS_STUB_PYTHON']) - def test_list(self): + def test_python_path_list(self): context = self.router.local( python_path=[ - testlib.data_path('env_wrapper.sh'), + self.stub_python_path, "magic_first_arg", sys.executable ] ) self.assertEquals(sys.executable, context.call(get_sys_executable)) env = context.call(get_os_environ) - self.assertEquals('magic_first_arg', env['ENV_WRAPPER_FIRST_ARG']) - self.assertEquals('1', env['EXECUTED_VIA_ENV_WRAPPER']) + self.assertEquals('magic_first_arg', env['STUB_PYTHON_FIRST_ARG']) + self.assertEquals('1', env['THIS_IS_STUB_PYTHON']) if __name__ == '__main__': From fccf42414010a8fa5855f417a7764a41c2fecc37 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 12:48:24 +0000 Subject: [PATCH 202/662] tests: Pythonize print_env.sh. --- .../regression/issue_122__environment_difference.yml | 2 +- tests/ansible/regression/scripts/print_env.py | 6 ++++++ tests/ansible/regression/scripts/print_env.sh | 2 -- 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 tests/ansible/regression/scripts/print_env.py delete mode 100644 tests/ansible/regression/scripts/print_env.sh diff --git a/tests/ansible/regression/issue_122__environment_difference.yml b/tests/ansible/regression/issue_122__environment_difference.yml index bf9df861..b020cc5d 100644 --- a/tests/ansible/regression/issue_122__environment_difference.yml +++ b/tests/ansible/regression/issue_122__environment_difference.yml @@ -9,6 +9,6 @@ hosts: test-targets tasks: - - script: scripts/print_env.sh + - script: scripts/print_env.py register: env - debug: msg={{env}} diff --git a/tests/ansible/regression/scripts/print_env.py b/tests/ansible/regression/scripts/print_env.py new file mode 100644 index 00000000..50a2504e --- /dev/null +++ b/tests/ansible/regression/scripts/print_env.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import os +import pprint + +pprint.pprint(dict(os.environ)) diff --git a/tests/ansible/regression/scripts/print_env.sh b/tests/ansible/regression/scripts/print_env.sh deleted file mode 100644 index c03c9936..00000000 --- a/tests/ansible/regression/scripts/print_env.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -set From 43306fff81fed6cbadd5ab3162a5c7b670a7db9c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 14:39:47 +0000 Subject: [PATCH 203/662] docs: drop sphinx-autobuild, avoids 10 deps (-16%) sphinx-autobuild==0.6.0 (from -r docs/docs-requirements.txt (line 2)) livereload>=2.3.0 (from sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) pathtools>=0.1.2 (from sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) tornado>=3.2 (from sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) argh>=0.24.1 (from sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) watchdog>=0.7.1 (from sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) port-for==0.3.1 (from sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) backports.ssl_match_hostname (from tornado>=3.2->sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) singledispatch (from tornado>=3.2->sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) backports_abc>=0.4 (from tornado>=3.2->sphinx-autobuild==0.6.0->-r docs/docs-requirements.txt (line 2)) --- docs/Makefile | 2 +- docs/docs-requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index bc394d34..8edc7fd5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # default: - sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + sphinx-autobuild . build/html/ # You can set these variables from the command line. SPHINXOPTS = diff --git a/docs/docs-requirements.txt b/docs/docs-requirements.txt index f0bddf36..a93c2140 100644 --- a/docs/docs-requirements.txt +++ b/docs/docs-requirements.txt @@ -1,4 +1,3 @@ Sphinx==1.7.1 -sphinx-autobuild==0.6.0 # Last version to support Python 2.6 sphinxcontrib-programoutput==0.11 alabaster==0.7.10 From b60a6d0f3b397401deab9d257655b6519bace792 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 15:11:13 +0000 Subject: [PATCH 204/662] Split dev_requirements.txt up according to test mode. --- dev_requirements.txt | 28 +++++++------------ ...docs-requirements.txt => requirements.txt} | 0 tests/ansible/requirements.txt | 2 ++ tests/requirements.txt | 15 ++++++++++ 4 files changed, 27 insertions(+), 18 deletions(-) rename docs/{docs-requirements.txt => requirements.txt} (100%) create mode 100644 tests/requirements.txt diff --git a/dev_requirements.txt b/dev_requirements.txt index eb83f8be..2dc0d171 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,19 +1,11 @@ --r docs/docs-requirements.txt +# This file is no longer used by CI jobs, it's mostly for interactive use. +# Instead CI jobs grab the relevant sub-requirement. + +# mitogen_tests +-r tests/requirements.txt + +# ansible_tests -r tests/ansible/requirements.txt -psutil==5.4.8 -coverage==4.5.1 -Django==1.6.11 # Last version supporting 2.6. -mock==2.0.0 -pytz==2018.5 -cffi==1.11.2 # Random pin to try and fix pyparser==2.18 not having effect -pycparser==2.18 # Last version supporting 2.6. -faulthandler==3.1; python_version < '3.3' # used by testlib -pytest-catchlog==1.2.2 -pytest==3.1.2 -PyYAML==3.11; python_version < '2.7' -PyYAML==3.12; python_version >= '2.7' -timeoutcontext==1.2.0 -unittest2==1.1.0 -# Fix InsecurePlatformWarning while creating py26 tox environment -# https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings -urllib3[secure]; python_version < '2.7.9' + +# readthedocs +-r docs/requirements.txt diff --git a/docs/docs-requirements.txt b/docs/requirements.txt similarity index 100% rename from docs/docs-requirements.txt rename to docs/requirements.txt diff --git a/tests/ansible/requirements.txt b/tests/ansible/requirements.txt index 551af999..74477c22 100644 --- a/tests/ansible/requirements.txt +++ b/tests/ansible/requirements.txt @@ -2,3 +2,5 @@ ansible; python_version >= '2.7' ansible<2.7; python_version < '2.7' paramiko==2.3.2 # Last 2.6-compat version. google-api-python-client==1.6.5 +PyYAML==3.11; python_version < '2.7' +PyYAML==3.12; python_version >= '2.7' diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..327f563a --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,15 @@ +psutil==5.4.8 +coverage==4.5.1 +Django==1.6.11 # Last version supporting 2.6. +mock==2.0.0 +pytz==2018.5 +cffi==1.11.2 # Random pin to try and fix pyparser==2.18 not having effect +pycparser==2.18 # Last version supporting 2.6. +faulthandler==3.1; python_version < '3.3' # used by testlib +pytest-catchlog==1.2.2 +pytest==3.1.2 +timeoutcontext==1.2.0 +unittest2==1.1.0 +# Fix InsecurePlatformWarning while creating py26 tox environment +# https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings +urllib3[secure]; python_version < '2.7.9' From 2a6dbb038f4485e268ee80808c5780232e828fb8 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 13:47:09 +0000 Subject: [PATCH 205/662] Pythonize, split out and parallelize all jobs. --- .ci/ansible_install.py | 21 ++++++++ .ci/ansible_tests.py | 51 ++++++++------------ .ci/azure-pipelines.yml | 11 ++--- .ci/ci_lib.py | 69 ++++++++++++++++++++++++--- .ci/debops_common_install.py | 18 +++++++ .ci/debops_common_tests.py | 78 ++++++++++++++++++++++++++++++ .ci/debops_common_tests.sh | 90 ----------------------------------- .ci/mitogen_install.py | 15 ++++++ .ci/mitogen_tests.py | 14 ++++++ .ci/mitogen_tests.sh | 5 -- .ci/prep_azure.py | 40 +++++----------- .travis.yml | 17 +------ run_tests | 2 +- tests/show_docker_hostname.py | 9 ---- 14 files changed, 248 insertions(+), 192 deletions(-) create mode 100755 .ci/ansible_install.py create mode 100755 .ci/debops_common_install.py create mode 100755 .ci/debops_common_tests.py delete mode 100755 .ci/debops_common_tests.sh create mode 100755 .ci/mitogen_install.py create mode 100755 .ci/mitogen_tests.py delete mode 100755 .ci/mitogen_tests.sh delete mode 100644 tests/show_docker_hostname.py diff --git a/.ci/ansible_install.py b/.ci/ansible_install.py new file mode 100755 index 00000000..167a9cb1 --- /dev/null +++ b/.ci/ansible_install.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +import ci_lib + +batches = [ + [ + # Must be installed separately, as PyNACL indirect requirement causes + # newer version to be installed if done in a single pip run. + 'pip install "pycparser<2.19"', + 'pip install ' + '-r tests/requirements.txt ' + '-r tests/ansible/requirements.txt', + ] +] + +batches.extend( + ['docker pull mitogen/%s-test' % (distro,)] + for distro in ci_lib.DISTROS +) + +ci_lib.run_batches(batches) diff --git a/.ci/ansible_tests.py b/.ci/ansible_tests.py index d7730ba6..bae95902 100755 --- a/.ci/ansible_tests.py +++ b/.ci/ansible_tests.py @@ -8,53 +8,42 @@ import ci_lib from ci_lib import run -BASE_PORT = 2201 TESTS_DIR = os.path.join(ci_lib.GIT_ROOT, 'tests/ansible') HOSTS_DIR = os.path.join(ci_lib.TMP, 'hosts') +with ci_lib.Fold('unit_tests'): + os.environ['SKIP_MITOGEN'] = '1' + ci_lib.run('./run_tests -v') + + with ci_lib.Fold('docker_setup'): - for i, distro in enumerate(ci_lib.DISTROS): - try: - run("docker rm -f target-%s", distro) - except: pass - - run(""" - docker run - --rm - --detach - --publish 0.0.0.0:%s:22/tcp - --hostname=target-%s - --name=target-%s - mitogen/%s-test - """, BASE_PORT + i, distro, distro, distro) + containers = ci_lib.make_containers() + ci_lib.start_containers(containers) with ci_lib.Fold('job_setup'): - os.chdir(TESTS_DIR) - os.chmod('../data/docker/mitogen__has_sudo_pubkey.key', int('0600', 7)) - - run("pip install -qr requirements.txt") # tests/ansible/requirements # Don't set -U as that will upgrade Paramiko to a non-2.6 compatible version. run("pip install -q ansible==%s", ci_lib.ANSIBLE_VERSION) + os.chdir(TESTS_DIR) + os.chmod('../data/docker/mitogen__has_sudo_pubkey.key', int('0600', 7)) + run("mkdir %s", HOSTS_DIR) run("ln -s %s/hosts/common-hosts %s", TESTS_DIR, HOSTS_DIR) - docker_hostname = ci_lib.get_docker_hostname() with open(os.path.join(HOSTS_DIR, 'target'), 'w') as fp: fp.write('[test-targets]\n') - for i, distro in enumerate(ci_lib.DISTROS): - fp.write("target-%s " - "ansible_host=%s " - "ansible_port=%s " - "ansible_user=mitogen__has_sudo_nopw " - "ansible_password=has_sudo_nopw_password" - "\n" % ( - distro, - docker_hostname, - BASE_PORT + i, - )) + fp.writelines( + "%(name)s " + "ansible_host=%(hostname)s " + "ansible_port=%(port)s " + "ansible_user=mitogen__has_sudo_nopw " + "ansible_password=has_sudo_nopw_password" + "\n" + % container + for container in containers + ) # Build the binaries. # run("make -C %s", TESTS_DIR) diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 2a22bdad..fbbb9640 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -76,11 +76,8 @@ jobs: - script: .ci/prep_azure.py displayName: "Install requirements." - - script: | - export TRAVIS_BUILD_DIR=`pwd` - if [ -f ".ci/$(MODE)_tests.sh" ]; then - .ci/$(MODE)_tests.sh; - else - .ci/$(MODE)_tests.py; - fi + - script: .ci/$(MODE)_install.py + displayName: "Install requirements." + + - script: .ci/$(MODE)_tests.py displayName: Run tests. diff --git a/.ci/ci_lib.py b/.ci/ci_lib.py index d7d6e09b..77cc30a2 100644 --- a/.ci/ci_lib.py +++ b/.ci/ci_lib.py @@ -4,12 +4,17 @@ from __future__ import print_function import atexit import os -import subprocess -import sys import shlex import shutil +import subprocess +import sys import tempfile +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + # # check_output() monkeypatch cutpasted from testlib.py @@ -60,13 +65,26 @@ def _argv(s, *args): def run(s, *args, **kwargs): - argv = _argv(s, *args) + argv = ['/usr/bin/time', '--'] + _argv(s, *args) print('Running: %s' % (argv,)) ret = subprocess.check_call(argv, **kwargs) print('Finished running: %s' % (argv,)) return ret +def run_batches(batches): + combine = lambda batch: 'set -x; ' + (' && '.join( + '( %s; )' % (cmd,) + for cmd in batch + )) + + procs = [ + subprocess.Popen(combine(batch), shell=True) + for batch in batches + ] + assert [proc.wait() for proc in procs] == [0] * len(procs) + + def get_output(s, *args, **kwargs): argv = _argv(s, *args) print('Running: %s' % (argv,)) @@ -103,7 +121,10 @@ os.environ.setdefault('ANSIBLE_STRATEGY', os.environ.get('STRATEGY', 'mitogen_linear')) ANSIBLE_VERSION = os.environ.get('VER', '2.6.2') GIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +DISTRO = os.environ.get('DISTRO', 'debian') DISTROS = os.environ.get('DISTROS', 'debian centos6 centos7').split() +TARGET_COUNT = int(os.environ.get('TARGET_COUNT', '2')) +BASE_PORT = 2200 TMP = TempDir().path os.environ['PYTHONDONTWRITEBYTECODE'] = 'x' @@ -113,10 +134,44 @@ os.environ['PYTHONPATH'] = '%s:%s' % ( ) def get_docker_hostname(): - return subprocess.check_output([ - sys.executable, - os.path.join(GIT_ROOT, 'tests/show_docker_hostname.py'), - ]).decode().strip() + url = os.environ.get('DOCKER_HOST') + if url in (None, 'http+docker://localunixsocket'): + return 'localhost' + + parsed = urlparse.urlparse(url) + return parsed.netloc.partition(':')[0] + + +def make_containers(): + docker_hostname = get_docker_hostname() + return [ + { + "distro": distro, + "name": "target-%s-%s" % (distro, i), + "hostname": docker_hostname, + "port": BASE_PORT + i, + } + for i, distro in enumerate(DISTROS, 1) + ] + + +def start_containers(containers): + run_batches([ + [ + "docker rm -f %(name)s || true" % container, + "docker run " + "--rm " + "--detach " + "--publish 0.0.0.0:%(port)s:22/tcp " + "--hostname=%(name)s " + "--name=%(name)s " + "mitogen/%(distro)s-test " + % container + ] + for container in containers + ]) + return containers + # SSH passes these through to the container when run interactively, causing # stdout to get messed up with libc warnings. diff --git a/.ci/debops_common_install.py b/.ci/debops_common_install.py new file mode 100755 index 00000000..8830eaf6 --- /dev/null +++ b/.ci/debops_common_install.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import ci_lib + +# Naturally DebOps only supports Debian. +ci_lib.DISTROS = ['debian'] + +ci_lib.run_batches([ + [ + # Must be installed separately, as PyNACL indirect requirement causes + # newer version to be installed if done in a single pip run. + 'pip install "pycparser<2.19"', + 'pip install -qqqU debops==0.7.2 ansible==%s' % ci_lib.ANSIBLE_VERSION, + ], + [ + 'docker pull mitogen/debian-test', + ], +]) diff --git a/.ci/debops_common_tests.py b/.ci/debops_common_tests.py new file mode 100755 index 00000000..04fbb938 --- /dev/null +++ b/.ci/debops_common_tests.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +from __future__ import print_function +import os + +import ci_lib + + +# DebOps only supports Debian. +ci_lib.DISTROS = ['debian'] * ci_lib.TARGET_COUNT + +project_dir = os.path.join(ci_lib.TMP, 'project') +key_file = os.path.join( + ci_lib.GIT_ROOT, + 'tests/data/docker/mitogen__has_sudo_pubkey.key', +) +vars_path = 'ansible/inventory/group_vars/debops_all_hosts.yml' +inventory_path = 'ansible/inventory/hosts' +docker_hostname = ci_lib.get_docker_hostname() + + +with ci_lib.Fold('docker_setup'): + containers = ci_lib.make_containers() + ci_lib.start_containers(containers) + + +with ci_lib.Fold('job_setup'): + ci_lib.run('debops-init %s', project_dir) + os.chdir(project_dir) + + with open('.debops.cfg', 'w') as fp: + fp.write( + "[ansible defaults]\n" + "strategy_plugins = %s/ansible_mitogen/plugins/strategy\n" + "strategy = mitogen_linear\n" + % (ci_lib.GIT_ROOT,) + ) + + ci_lib.run('chmod go= %s', key_file) + with open(vars_path, 'w') as fp: + fp.write( + "ansible_python_interpreter: /usr/bin/python2.7\n" + "\n" + "ansible_user: mitogen__has_sudo_pubkey\n" + "ansible_become_pass: has_sudo_pubkey_password\n" + "ansible_ssh_private_key_file: %s\n" + "\n" + # Speed up slow DH generation. + "dhparam__bits: ['128', '64']\n" + % (key_file,) + ) + + with open(inventory_path, 'a') as fp: + fp.writelines( + '%(name)s ' + 'ansible_host=%(hostname)s ' + 'ansible_port=%(port)d ' + '\n' + % container + for container in containers + ) + + print() + print(' echo --- ansible/inventory/hosts: ---') + ci_lib.run('cat ansible/inventory/hosts') + print('---') + print() + + # Now we have real host key checking, we need to turn it off + os.environ['ANSIBLE_HOST_KEY_CHECKING'] = 'False' + + +with ci_lib.Fold('first_run'): + ci_lib.run('debops common') + + +with ci_lib.Fold('second_run'): + ci_lib.run('debops common') diff --git a/.ci/debops_common_tests.sh b/.ci/debops_common_tests.sh deleted file mode 100755 index 753d1c11..00000000 --- a/.ci/debops_common_tests.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash -ex -# Run some invocations of DebOps. - -TMPDIR="/tmp/debops-$$" -TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-`pwd`}" -TARGET_COUNT="${TARGET_COUNT:-2}" -ANSIBLE_VERSION="${VER:-2.6.1}" -DISTRO=debian # Naturally DebOps only supports Debian. - -export PYTHONPATH="${PYTHONPATH}:${TRAVIS_BUILD_DIR}" - -function on_exit() -{ - echo travis_fold:start:cleanup - [ "$KEEP" ] || { - rm -rf "$TMPDIR" || true - for i in $(seq $TARGET_COUNT) - do - docker kill target$i || true - done - } - echo travis_fold:end:cleanup -} - -trap on_exit EXIT -mkdir "$TMPDIR" - - -echo travis_fold:start:job_setup -pip install -qqqU debops==0.7.2 ansible==${ANSIBLE_VERSION} |cat -debops-init "$TMPDIR/project" -cd "$TMPDIR/project" - -cat > .debops.cfg <<-EOF -[ansible defaults] -strategy_plugins = ${TRAVIS_BUILD_DIR}/ansible_mitogen/plugins/strategy -strategy = mitogen_linear -EOF - -chmod go= ${TRAVIS_BUILD_DIR}/tests/data/docker/mitogen__has_sudo_pubkey.key - -cat > ansible/inventory/group_vars/debops_all_hosts.yml <<-EOF -ansible_python_interpreter: /usr/bin/python2.7 - -ansible_user: mitogen__has_sudo_pubkey -ansible_become_pass: has_sudo_pubkey_password -ansible_ssh_private_key_file: ${TRAVIS_BUILD_DIR}/tests/data/docker/mitogen__has_sudo_pubkey.key - -# Speed up slow DH generation. -dhparam__bits: ["128", "64"] -EOF - -DOCKER_HOSTNAME="$(python ${TRAVIS_BUILD_DIR}/tests/show_docker_hostname.py)" - -for i in $(seq $TARGET_COUNT) -do - port=$((2200 + $i)) - docker run \ - --rm \ - --detach \ - --publish 0.0.0.0:$port:22/tcp \ - --name=target$i \ - mitogen/${DISTRO}-test - - echo \ - target$i \ - ansible_host=$DOCKER_HOSTNAME \ - ansible_port=$port \ - >> ansible/inventory/hosts -done - -echo -echo --- ansible/inventory/hosts: ---- -cat ansible/inventory/hosts -echo --- - -# Now we have real host key checking, we need to turn it off. :) -export ANSIBLE_HOST_KEY_CHECKING=False - -echo travis_fold:end:job_setup - - -echo travis_fold:start:first_run -/usr/bin/time debops common "$@" -echo travis_fold:end:first_run - - -echo travis_fold:start:second_run -/usr/bin/time debops common "$@" -echo travis_fold:end:second_run diff --git a/.ci/mitogen_install.py b/.ci/mitogen_install.py new file mode 100755 index 00000000..4cc06c04 --- /dev/null +++ b/.ci/mitogen_install.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import ci_lib + +batches = [ + [ + 'pip install "pycparser<2.19"', + 'pip install -r tests/requirements.txt', + ], + [ + 'docker pull mitogen/%s-test' % (ci_lib.DISTRO,), + ] +] + +ci_lib.run_batches(batches) diff --git a/.ci/mitogen_tests.py b/.ci/mitogen_tests.py new file mode 100755 index 00000000..4ba796c2 --- /dev/null +++ b/.ci/mitogen_tests.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# Run the Mitogen tests. + +import os + +import ci_lib + +os.environ.update({ + 'MITOGEN_TEST_DISTRO': ci_lib.DISTRO, + 'MITOGEN_LOG_LEVEL': 'debug', + 'SKIP_ANSIBLE': '1', +}) + +ci_lib.run('./run_tests -v') diff --git a/.ci/mitogen_tests.sh b/.ci/mitogen_tests.sh deleted file mode 100755 index 33ee16ba..00000000 --- a/.ci/mitogen_tests.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -ex -# Run the Mitogen tests. - -MITOGEN_TEST_DISTRO="${DISTRO:-debian}" -MITOGEN_LOG_LEVEL=debug PYTHONPATH=. ${TRAVIS_BUILD_DIR}/run_tests -v diff --git a/.ci/prep_azure.py b/.ci/prep_azure.py index 164e04e3..85c19947 100755 --- a/.ci/prep_azure.py +++ b/.ci/prep_azure.py @@ -1,36 +1,22 @@ #!/usr/bin/env python -# Run preparation steps in parallel. -import subprocess import ci_lib -subprocess.check_call( +batches = [] +batches.append([ 'echo force-unsafe-io | sudo tee /etc/dpkg/dpkg.cfg.d/nosync', - shell=True, -) + 'sudo add-apt-repository ppa:deadsnakes/ppa', + 'sudo apt-get update', + 'sudo apt-get -y install python2.6 python2.6-dev libsasl2-dev libldap2-dev', +]) -procs = [ - subprocess.Popen( - 'pip install -r dev_requirements.txt 2>&1 | cat', - shell=True, - ), - subprocess.Popen( - """ - sudo add-apt-repository ppa:deadsnakes/ppa && \ - ( sudo apt-get update 2>&1 | cat ) && \ - sudo apt-get -y install \ - python2.6 python2.6-dev libsasl2-dev libldap2-dev 2>&1 | cat - """, - shell=True, - ) -] +batches.append([ + 'pip install -r dev_requirements.txt', +]) -procs += [ - subprocess.Popen( - 'docker pull mitogen/%s-test 2>&1 | cat' % (distro,), - shell=True - ) +batches.extend( + ['docker pull mitogen/%s-test' % (distro,)] for distro in ci_lib.DISTROS -] +) -assert [proc.wait() for proc in procs] == [0] * len(procs) +ci_lib.run_batches(batches) diff --git a/.travis.yml b/.travis.yml index 8e4a8d51..a0d1f924 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,4 @@ sudo: required -addons: - apt: - update: true notifications: email: false @@ -14,20 +11,10 @@ cache: - /home/travis/virtualenv install: -# |cat to disable progress bar. -- pip install -r dev_requirements.txt |cat +- .ci/${MODE}_install.py script: -- | - if [ -f "${TRAVIS_BUILD_DIR}/.ci/${MODE}_tests.sh" ]; then - ${TRAVIS_BUILD_DIR}/.ci/${MODE}_tests.sh; - else - ${TRAVIS_BUILD_DIR}/.ci/${MODE}_tests.py; - fi - - -services: - - docker +- .ci/${MODE}_tests.py # To avoid matrix explosion, just test against oldest->newest and diff --git a/run_tests b/run_tests index 65bf1fef..8de99ace 100755 --- a/run_tests +++ b/run_tests @@ -1,4 +1,4 @@ -#/usr/bin/env bash +#!/usr/bin/env bash echo '----- ulimits -----' ulimit -a diff --git a/tests/show_docker_hostname.py b/tests/show_docker_hostname.py deleted file mode 100644 index 995c744b..00000000 --- a/tests/show_docker_hostname.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python - -""" -For use by the Travis scripts, just print out the hostname of the Docker -daemon from the environment. -""" - -import testlib -print(testlib.get_docker_host()) From bef4b0c962368ceb2617619da49f8ee76d9c04b4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 18:51:57 +0000 Subject: [PATCH 206/662] tests: fix copy.yml title --- tests/ansible/integration/action/copy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ansible/integration/action/copy.yml b/tests/ansible/integration/action/copy.yml index d799be90..c3e961f6 100644 --- a/tests/ansible/integration/action/copy.yml +++ b/tests/ansible/integration/action/copy.yml @@ -1,6 +1,6 @@ # Verify copy module for small and large files, and inline content. -- name: integration/action/synchronize.yml +- name: integration/action/copy.yml hosts: test-targets any_errors_fatal: true tasks: From 65d9eec35380c0ee92bf9590125d35edafbbd389 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 19:50:05 +0000 Subject: [PATCH 207/662] issue #364: core: have Sender.close() supply reason= to dead() --- mitogen/core.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mitogen/core.py b/mitogen/core.py index b1519c90..cb64fc92 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -721,13 +721,19 @@ class Sender(object): _vv and IOLOG.debug('%r.send(%r..)', self, repr(data)[:100]) self.context.send(Message.pickled(data, handle=self.dst_handle)) + explicit_close_msg = 'Sender was explicitly closed' + def close(self): """ Send a dead message to the remote, causing :meth:`ChannelError` to be raised in any waiting thread. """ _vv and IOLOG.debug('%r.close()', self) - self.context.send(Message.dead(handle=self.dst_handle)) + self.context.send( + Message.dead( + reason=self.explicit_close_msg, + handle=self.dst_handle) + ) def __repr__(self): return 'Sender(%r, %r)' % (self.context, self.dst_handle) From 4267014ca6444a7efd34e475f9ce28be9282d536 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 22:14:15 +0000 Subject: [PATCH 208/662] issue #364: clarify logged error when incorrect file size detected --- mitogen/service.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mitogen/service.py b/mitogen/service.py index dc90b67f..db042041 100644 --- a/mitogen/service.py +++ b/mitogen/service.py @@ -772,7 +772,7 @@ class FileService(Service): def register(self, path): """ Authorize a path for access by children. Repeat calls with the same - path is harmless. + path has no effect. :param str path: File path. @@ -949,6 +949,7 @@ class FileService(Service): sender=recv.to_sender(), ) + received_bytes = 0 for chunk in recv: s = chunk.unpickle() LOG.debug('get_file(%r): received %d bytes', path, len(s)) @@ -958,11 +959,19 @@ class FileService(Service): size=len(s), ).close() out_fp.write(s) + received_bytes += len(s) - ok = out_fp.tell() == metadata['size'] - if not ok: + ok = received_bytes == metadata['size'] + if received_bytes < metadata['size']: LOG.error('get_file(%r): receiver was closed early, controller ' - 'is likely shutting down.', path) + 'may be shutting down, or the file was truncated ' + 'during transfer. Expected %d bytes, received %d.', + path, metadata['size'], received_bytes) + elif received_bytes > metadata['size']: + LOG.error('get_file(%r): the file appears to have grown ' + 'while transfer was in progress. Expected %d ' + 'bytes, received %d.', + path, metadata['size'], received_bytes) LOG.debug('target.get_file(): fetched %d bytes of %r from %r in %dms', metadata['size'], path, context, 1000 * (time.time() - t0)) From 2a2dda8e39dc53afbf11b1dc85694eb619bda151 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 22:20:31 +0000 Subject: [PATCH 209/662] issue #364: remove stat() caching. --- mitogen/service.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/mitogen/service.py b/mitogen/service.py index db042041..0bafc899 100644 --- a/mitogen/service.py +++ b/mitogen/service.py @@ -754,8 +754,8 @@ class FileService(Service): def __init__(self, router): super(FileService, self).__init__(router) - #: Mapping of registered path -> file size. - self._metadata_by_path = {} + #: Set of registered paths. + self._paths = set() #: Mapping of Stream->FileStreamState. self._state_by_stream = {} @@ -777,15 +777,16 @@ class FileService(Service): :param str path: File path. """ - if path in self._metadata_by_path: - return + if path not in self._paths: + LOG.debug('%r: registering %r', self, path) + self._paths.add(path) + def _generate_stat(self, path): st = os.stat(path) if not stat.S_ISREG(st.st_mode): raise IOError('%r is not a regular file.' % (path,)) - LOG.debug('%r: registering %r', self, path) - self._metadata_by_path[path] = { + return { 'size': st.st_size, 'mode': st.st_mode, 'owner': self._name_or_none(pwd.getpwuid, 0, 'pw_name'), @@ -869,26 +870,26 @@ class FileService(Service): :raises Error: Unregistered path, or Sender did not match requestee context. """ - if path not in self._metadata_by_path: + if path not in self._paths: raise Error(self.unregistered_msg) if msg.src_id != sender.context.context_id: raise Error(self.context_mismatch_msg) LOG.debug('Serving %r', path) + + # Response must arrive first so requestee can begin receive loop, + # otherwise first ack won't arrive until all pending chunks were + # delivered. In that case max BDP would always be 128KiB, aka. max + # ~10Mbit/sec over a 100ms link. try: fp = open(path, 'rb', self.IO_SIZE) + msg.reply(self._generate_stat(path)) except IOError: msg.reply(mitogen.core.CallError( sys.exc_info()[1] )) return - # Response must arrive first so requestee can begin receive loop, - # otherwise first ack won't arrive until all pending chunks were - # delivered. In that case max BDP would always be 128KiB, aka. max - # ~10Mbit/sec over a 100ms link. - msg.reply(self._metadata_by_path[path]) - stream = self.router.stream_by_id(sender.context.context_id) state = self._state_by_stream.setdefault(stream, FileStreamState()) state.lock.acquire() From 578c2c3b46446e0d4e7d7fad800f655f2b5eb774 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 22:24:07 +0000 Subject: [PATCH 210/662] issue #364: update ChangeLog. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index cdc160be..99b30214 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -164,6 +164,10 @@ Fixes environment variable if it is set, causing behaviour to diverge when Ansible was invoked across user accounts via ``sudo``. +* `#364 `_: file transfers from + controllers running Python 2.7.2 or earlier could be interrupted due to a + forking bug in the :mod:`tempfile` module. + * `#370 `_: the Ansible `reboot `_ module is supported. From f1661abe4e99c72b0efcc6f274e752cf67febc19 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:06:03 +0000 Subject: [PATCH 211/662] tests: make IterReadTest a little more robust --- tests/parent_test.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/parent_test.py b/tests/parent_test.py index 0bd8079c..ec33415b 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -233,6 +233,7 @@ class IterReadTest(testlib.TestCase): func = staticmethod(mitogen.parent.iter_read) def make_proc(self): + # I produce text every 100ms. args = [testlib.data_path('iter_read_generator.py')] proc = subprocess.Popen(args, stdout=subprocess.PIPE) mitogen.core.set_nonblock(proc.stdout.fileno()) @@ -267,18 +268,22 @@ class IterReadTest(testlib.TestCase): def test_deadline_exceeded_during_call(self): proc = self.make_proc() - reader = self.func([proc.stdout.fileno()], time.time() + 0.4) + deadline = time.time() + 0.4 + + reader = self.func([proc.stdout.fileno()], deadline) try: got = [] try: for chunk in reader: + if time.time() > (deadline + 1.0): + assert 0, 'TimeoutError not raised' got.append(chunk) - assert 0, 'TimeoutError not raised' except mitogen.core.TimeoutError: # Give a little wiggle room in case of imperfect scheduling. # Ideal number should be 9. - self.assertLess(3, len(got)) - self.assertLess(len(got), 5) + self.assertLess(deadline, time.time()) + self.assertLess(1, len(got)) + self.assertLess(len(got), 20) finally: proc.terminate() proc.stdout.close() From 3206d59c8767853004f087c7426deaff40c666f0 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:13:05 +0000 Subject: [PATCH 212/662] issue #426: teach DockerMixin to allow selecting interpreter --- tests/testlib.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/testlib.py b/tests/testlib.py index b9a1bec5..19f6ed86 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -331,13 +331,17 @@ def get_docker_host(): class DockerizedSshDaemon(object): - image = None - - def get_image(self): - if not self.image: - distro = os.environ.get('MITOGEN_TEST_DISTRO', 'debian') - self.image = 'mitogen/%s-test' % (distro,) - return self.image + distro, _, _py3 = ( + os.environ.get('MITOGEN_TEST_DISTRO', 'debian') + .partition('-') + ) + + python_path = ( + '/usr/bin/python3' + if _py3 == 'py3' + else '/usr/bin/python' + ) + image = 'mitogen/%s-test' % (distro,) # 22/tcp -> 0.0.0.0:32771 PORT_RE = re.compile(r'([^/]+)/([^ ]+) -> ([^:]+):(.*)') @@ -363,7 +367,7 @@ class DockerizedSshDaemon(object): '--privileged', '--publish-all', '--name', self.container_name, - self.get_image() + self.image, ] subprocess__check_output(args) self._get_container_port() @@ -423,6 +427,7 @@ class DockerMixin(RouterMixin): kwargs.setdefault('port', self.dockerized_ssh.port) kwargs.setdefault('check_host_keys', 'ignore') kwargs.setdefault('ssh_debug_level', 3) + kwargs.setdefault('python_path', self.dockerized_ssh.python_path) return self.router.ssh(**kwargs) def docker_ssh_any(self, **kwargs): From 934d8ac13905199703e310ccc7ad5812f124c135 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:13:22 +0000 Subject: [PATCH 213/662] issue #426: fix 2->3 issue in plain_old_module. --- tests/data/plain_old_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/plain_old_module.py b/tests/data/plain_old_module.py index 49294464..608f27a5 100755 --- a/tests/data/plain_old_module.py +++ b/tests/data/plain_old_module.py @@ -12,7 +12,7 @@ class MyError(Exception): def get_sentinel_value(): # Some proof we're even talking to the mitogen-test Docker image - return open('/etc/sentinel').read().decode() + return open('/etc/sentinel', 'rb').read().decode() def add(x, y): From c84f36e809ec5f890f5905686194bc7786af4397 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:33:55 +0000 Subject: [PATCH 214/662] issue #426: teach .ci/ Docker pulls to ignore -py3 image suffix. --- .ci/ansible_install.py | 2 +- .ci/ci_lib.py | 4 ++++ .ci/debops_common_install.py | 2 +- .ci/mitogen_install.py | 2 +- .ci/prep_azure.py | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.ci/ansible_install.py b/.ci/ansible_install.py index 167a9cb1..b2da2702 100755 --- a/.ci/ansible_install.py +++ b/.ci/ansible_install.py @@ -14,7 +14,7 @@ batches = [ ] batches.extend( - ['docker pull mitogen/%s-test' % (distro,)] + ['docker pull %s' % (ci_lib.image_for_distro(distro),)] for distro in ci_lib.DISTROS ) diff --git a/.ci/ci_lib.py b/.ci/ci_lib.py index 77cc30a2..7126887b 100644 --- a/.ci/ci_lib.py +++ b/.ci/ci_lib.py @@ -142,6 +142,10 @@ def get_docker_hostname(): return parsed.netloc.partition(':')[0] +def image_for_distro(distro): + return 'mitogen/%s-test' % (distro.partition('-')[0],) + + def make_containers(): docker_hostname = get_docker_hostname() return [ diff --git a/.ci/debops_common_install.py b/.ci/debops_common_install.py index 8830eaf6..32241449 100755 --- a/.ci/debops_common_install.py +++ b/.ci/debops_common_install.py @@ -13,6 +13,6 @@ ci_lib.run_batches([ 'pip install -qqqU debops==0.7.2 ansible==%s' % ci_lib.ANSIBLE_VERSION, ], [ - 'docker pull mitogen/debian-test', + 'docker pull %s' % (ci_lib.image_for_distro('debian'),), ], ]) diff --git a/.ci/mitogen_install.py b/.ci/mitogen_install.py index 4cc06c04..9afbe67d 100755 --- a/.ci/mitogen_install.py +++ b/.ci/mitogen_install.py @@ -8,7 +8,7 @@ batches = [ 'pip install -r tests/requirements.txt', ], [ - 'docker pull mitogen/%s-test' % (ci_lib.DISTRO,), + 'docker pull %s' % (ci_lib.image_for_distro(ci_lib.DISTRO),), ] ] diff --git a/.ci/prep_azure.py b/.ci/prep_azure.py index 85c19947..10126df2 100755 --- a/.ci/prep_azure.py +++ b/.ci/prep_azure.py @@ -15,7 +15,7 @@ batches.append([ ]) batches.extend( - ['docker pull mitogen/%s-test' % (distro,)] + ['docker pull %s' % (ci_lib.image_for_distro(distro),)] for distro in ci_lib.DISTROS ) From 8f03060e0c2c6c1d6be05bce9d2038d3b8b50838 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:14:10 +0000 Subject: [PATCH 215/662] issue #426: enable a 2->3 Mitogen job. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index a0d1f924..0092b10c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,9 @@ matrix: # 2.6 -> 2.7 - python: "2.6" env: MODE=mitogen DISTRO=centos7 + # 2.6 -> 3.5 + - python: "2.6" + env: MODE=mitogen DISTRO=debian-py3 # 3.6 -> 2.6 - python: "3.6" env: MODE=mitogen DISTRO=centos6 From 8df895a8ac457676a7e8e232fe21a0eb3e17a49f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:40:37 +0000 Subject: [PATCH 216/662] issue #426: make ansible_tests dump inventory. --- .ci/ansible_tests.py | 4 ++-- .ci/ci_lib.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.ci/ansible_tests.py b/.ci/ansible_tests.py index bae95902..3161196d 100755 --- a/.ci/ansible_tests.py +++ b/.ci/ansible_tests.py @@ -45,8 +45,8 @@ with ci_lib.Fold('job_setup'): for container in containers ) - # Build the binaries. - # run("make -C %s", TESTS_DIR) + ci_lib.dump_file(inventory_path) + if not ci_lib.exists_in_path('sshpass'): run("sudo apt-get update") run("sudo apt-get install -y sshpass") diff --git a/.ci/ci_lib.py b/.ci/ci_lib.py index 7126887b..f52e4cd4 100644 --- a/.ci/ci_lib.py +++ b/.ci/ci_lib.py @@ -177,6 +177,16 @@ def start_containers(containers): return containers +def dump_file(path): + print() + print('--- %s ---' % (path,)) + print() + with open(path, 'r') as fp: + print(fp.read().rstrip()) + print('---') + print() + + # SSH passes these through to the container when run interactively, causing # stdout to get messed up with libc warnings. os.environ.pop('LANG', None) From e7bb5c1ee06aaeaef745a1eb0e65052c2a82ad3f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:41:47 +0000 Subject: [PATCH 217/662] issue #426: teach make_containers() to parse -py3 DISTRO suffix --- .ci/ci_lib.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.ci/ci_lib.py b/.ci/ci_lib.py index f52e4cd4..bec700bd 100644 --- a/.ci/ci_lib.py +++ b/.ci/ci_lib.py @@ -148,12 +148,20 @@ def image_for_distro(distro): def make_containers(): docker_hostname = get_docker_hostname() + firstbit = lambda s: (s+'-').split('-')[0] + secondbit = lambda s: (s+'-').split('-')[1] + return [ { - "distro": distro, + "distro": firstbit(distro), "name": "target-%s-%s" % (distro, i), "hostname": docker_hostname, "port": BASE_PORT + i, + "python_path": ( + '/usr/bin/python3' + if secondbit(distro) == 'py3' + else '/usr/bin/python' + ) } for i, distro in enumerate(DISTROS, 1) ] From e12c963279c22a8ef9e3100dfbac405ffcb4dc42 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 00:52:20 +0000 Subject: [PATCH 218/662] issue #426: make ansible_tests use python_path --- .ci/ansible_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci/ansible_tests.py b/.ci/ansible_tests.py index 3161196d..ae433bd9 100755 --- a/.ci/ansible_tests.py +++ b/.ci/ansible_tests.py @@ -32,12 +32,14 @@ with ci_lib.Fold('job_setup'): run("mkdir %s", HOSTS_DIR) run("ln -s %s/hosts/common-hosts %s", TESTS_DIR, HOSTS_DIR) - with open(os.path.join(HOSTS_DIR, 'target'), 'w') as fp: + inventory_path = os.path.join(HOSTS_DIR, 'target') + with open(inventory_path, 'w') as fp: fp.write('[test-targets]\n') fp.writelines( "%(name)s " "ansible_host=%(hostname)s " "ansible_port=%(port)s " + "ansible_python_interpreter=%(python_path)s " "ansible_user=mitogen__has_sudo_nopw " "ansible_password=has_sudo_nopw_password" "\n" From 4db2168f83a5efd637861ad0174423b21c635444 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 01:16:18 +0000 Subject: [PATCH 219/662] issue #426: teach debops_common_tests to use py3 prefix --- .ci/debops_common_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/debops_common_tests.py b/.ci/debops_common_tests.py index 04fbb938..8e9f2953 100755 --- a/.ci/debops_common_tests.py +++ b/.ci/debops_common_tests.py @@ -55,6 +55,7 @@ with ci_lib.Fold('job_setup'): '%(name)s ' 'ansible_host=%(hostname)s ' 'ansible_port=%(port)d ' + 'ansible_python_interpreter=%(python_path)s ' '\n' % container for container in containers From 57504ba6ec1d42972c3732013bb573e4c93b3315 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 01:58:30 +0000 Subject: [PATCH 220/662] issue #109: core: meta_path regression in newer Pythons Python at some point (at least since https://bugs.python.org/issue14605) began populating sys.meta_path with its internal importer classes, meaning that interpreters no longer start with an empty sys.meta_path. --- mitogen/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitogen/core.py b/mitogen/core.py index cb64fc92..7f00da05 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2791,7 +2791,7 @@ class ExternalContext(object): self.importer = importer self.router.importer = importer - sys.meta_path.append(self.importer) + sys.meta_path.insert(0, self.importer) def _setup_package(self): global mitogen From 1756cea65bcc444c87c1b0157afbaaed1b6685dc Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 02:12:25 +0000 Subject: [PATCH 221/662] issue #109: update Changelog. --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 99b30214..308fe4b3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -210,6 +210,12 @@ Core Library receivers to wake with :class:`mitogen.core.ChannelError`, even when one participant is not a parent of the other. +* `#109 `_, + `57504ba6 `_: newer Python 3 + releases explicitly populate :data:`sys.meta_path` with importer internals, + causing Mitogen to install itself at the end of the importer chain rather + than the front. + * `#387 `_, `#413 `_: dead messages include an optional reason in their body. This is used to cause From 15ddecdb583bbc7d2e7d1f002b0c30c9a22f805d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 02:20:34 +0000 Subject: [PATCH 222/662] issue #391: FileService metadata dict keys must be Unicode. This is a regression since moving FileService from a 2.6-compatible file with unicode_literals set, to a <2.5-compatible file. --- mitogen/service.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mitogen/service.py b/mitogen/service.py index 0bafc899..e2b752fe 100644 --- a/mitogen/service.py +++ b/mitogen/service.py @@ -787,12 +787,12 @@ class FileService(Service): raise IOError('%r is not a regular file.' % (path,)) return { - 'size': st.st_size, - 'mode': st.st_mode, - 'owner': self._name_or_none(pwd.getpwuid, 0, 'pw_name'), - 'group': self._name_or_none(grp.getgrgid, 0, 'gr_name'), - 'mtime': st.st_mtime, - 'atime': st.st_atime, + u'size': st.st_size, + u'mode': st.st_mode, + u'owner': self._name_or_none(pwd.getpwuid, 0, 'pw_name'), + u'group': self._name_or_none(grp.getgrgid, 0, 'gr_name'), + u'mtime': st.st_mtime, + u'atime': st.st_atime, } def on_shutdown(self): From 4553039ed2ce46a5fcdf9a80b3fb5ea828e17c6b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 02:23:53 +0000 Subject: [PATCH 223/662] docs: update Changelog; closes #391. --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 308fe4b3..140e86cc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -176,6 +176,11 @@ Fixes print a useful hint on failure, as no useful error is normally logged to the console by these tools. +* `#391 `_: file transfer from 2.x + controllers to 3.x targets was broken due to a regression caused by + refactoring, and compounded by `#426 + `_. + * `#400 `_: work around a threading bug in the AWX display callback when running with high verbosity setting. From 44d6ca771a5e851705957a1dc46c0e5d89fb1a0a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 02:26:52 +0000 Subject: [PATCH 224/662] issue #426: fix local/delegate_to issue --- tests/ansible/bench/file_transfer.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/ansible/bench/file_transfer.yml b/tests/ansible/bench/file_transfer.yml index d0ac727d..2ca46f1c 100644 --- a/tests/ansible/bench/file_transfer.yml +++ b/tests/ansible/bench/file_transfer.yml @@ -5,11 +5,11 @@ tasks: - name: Make 32MiB file - connection: local + delegate_to: localhost shell: openssl rand 33554432 > /tmp/bigfile.in - name: Make 320MiB file - connection: local + delegate_to: localhost shell: > cat /tmp/bigfile.in @@ -47,21 +47,21 @@ file: path: "{{item}}" state: absent - connection: local + delegate_to: localhost become: true with_items: - /tmp/bigfile.out - /tmp/bigbigfile.out - name: Copy 32MiB file via localhost sudo - connection: local + delegate_to: localhost become: true copy: src: /tmp/bigfile.in dest: /tmp/bigfile.out - name: Copy 320MiB file via localhost sudo - connection: local + delegate_to: localhost become: true copy: src: /tmp/bigbigfile.in From 16911c9464af72510d66d86e3f359dc771e1ed01 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 7 Nov 2018 02:28:06 +0000 Subject: [PATCH 225/662] ansible: fix 3.x TypeError regression. --- ansible_mitogen/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py index 45bb5f0b..5ab0236c 100644 --- a/ansible_mitogen/runner.py +++ b/ansible_mitogen/runner.py @@ -255,7 +255,7 @@ class Runner(object): self.service_context = service_context self.econtext = econtext self.detach = detach - self.args = json.loads(json_args) + self.args = json.loads(mitogen.core.to_text(json_args)) self.good_temp_dir = good_temp_dir self.extra_env = extra_env self.env = env From fd5698c191d6a7d18f27de83cb71dd175c4aee2f Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 21 Nov 2018 18:10:14 +0000 Subject: [PATCH 226/662] docs: 4kify images. These commits were squashed to avoid repo size exploding. --- docs/_static/style.css | 40 ++++++++++++++++++ docs/ansible.rst | 19 +++++---- docs/api.rst | 4 +- docs/getting_started.rst | 8 ++-- docs/howitworks.rst | 6 ++- docs/images/ansible/ansible_mitogen.svg | 2 + docs/images/ansible/cell_division.png | Bin 18066 -> 0 bytes .../images/ansible/run_hostname_100_times.png | Bin 96250 -> 0 bytes .../ansible/run_hostname_100_times_mito.svg | 1 + .../ansible/run_hostname_100_times_plain.svg | 1 + docs/images/async.png | Bin 13281 -> 72907 bytes docs/images/billing.png | Bin 4492 -> 0 bytes docs/images/billing.svg | 1 + docs/images/cell_division.png | Bin 16666 -> 0 bytes docs/images/context-tree.png | Bin 11679 -> 0 bytes docs/images/context-tree.svg | 1 + docs/images/detached-subtree.png | Bin 13488 -> 0 bytes docs/images/detached-subtree.svg | 1 + docs/images/distribute2.png | Bin 21260 -> 80898 bytes docs/images/fakessh.png | Bin 9416 -> 0 bytes docs/images/fakessh.svg | 1 + docs/images/jumpbox.png | Bin 14030 -> 0 bytes docs/images/jumpbox.svg | 1 + docs/images/layout.png | Bin 26684 -> 0 bytes docs/images/layout.svg | 1 + docs/images/mitogen.svg | 2 + docs/images/pandora-orig.jpg | Bin 99013 -> 0 bytes docs/images/pandora.jpg | Bin 26975 -> 0 bytes docs/images/pandora.svg | 2 + docs/images/persistent.png | Bin 12848 -> 0 bytes docs/images/persistent.svg | 1 + docs/images/pipe.png | Bin 21092 -> 76099 bytes docs/images/radiation.png | Bin 15305 -> 0 bytes docs/images/route.png | Bin 16786 -> 0 bytes docs/images/route.svg | 1 + docs/images/topogit.png | Bin 31650 -> 90817 bytes docs/index.rst | 18 ++++---- docs/svg-boxify.py | 13 ++++++ 38 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 docs/images/ansible/ansible_mitogen.svg delete mode 100644 docs/images/ansible/cell_division.png delete mode 100644 docs/images/ansible/run_hostname_100_times.png create mode 100644 docs/images/ansible/run_hostname_100_times_mito.svg create mode 100644 docs/images/ansible/run_hostname_100_times_plain.svg delete mode 100644 docs/images/billing.png create mode 100644 docs/images/billing.svg delete mode 100644 docs/images/cell_division.png delete mode 100644 docs/images/context-tree.png create mode 100644 docs/images/context-tree.svg delete mode 100644 docs/images/detached-subtree.png create mode 100644 docs/images/detached-subtree.svg delete mode 100644 docs/images/fakessh.png create mode 100644 docs/images/fakessh.svg delete mode 100644 docs/images/jumpbox.png create mode 100644 docs/images/jumpbox.svg delete mode 100644 docs/images/layout.png create mode 100644 docs/images/layout.svg create mode 100644 docs/images/mitogen.svg delete mode 100644 docs/images/pandora-orig.jpg delete mode 100644 docs/images/pandora.jpg create mode 100644 docs/images/pandora.svg delete mode 100644 docs/images/persistent.png create mode 100644 docs/images/persistent.svg delete mode 100644 docs/images/radiation.png delete mode 100644 docs/images/route.png create mode 100644 docs/images/route.svg create mode 100644 docs/svg-boxify.py diff --git a/docs/_static/style.css b/docs/_static/style.css index 610f221e..2ca15e1d 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -10,3 +10,43 @@ div.figure { div.body li { margin-bottom: 0.5em; } + + + +/* + * Setting :width; on an image causes Sphinx to turn the image into a link, so + * set :Class: instead. + */ +.mitogen-full-width { + width: 100%; +} + +.mitogen-right-150 { + float: right; + padding-left: 8px; + width: 150px; +} + +.mitogen-right-225 { + float: right; + padding-left: 8px; + width: 225px; +} + +.mitogen-right-275 { + float: right; + padding-left: 8px; + width: 275px; +} + +.mitogen-right-300 { + float: right; + padding-left: 8px; + width: 300px; +} + +.mitogen-right-350 { + float: right; + padding-left: 8px; + width: 350px; +} diff --git a/docs/ansible.rst b/docs/ansible.rst index 33c73d06..614f9cc8 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -1,6 +1,7 @@ -.. image:: images/ansible/cell_division.png - :align: right +.. image:: images/ansible/ansible_mitogen.svg + :class: mitogen-right-225 + Mitogen for Ansible =================== @@ -211,8 +212,8 @@ New Features & Notes Connection Delegation ~~~~~~~~~~~~~~~~~~~~~ -.. image:: images/jumpbox.png - :align: right +.. image:: images/jumpbox.svg + :class: mitogen-right-275 Included is a preview of **Connection Delegation**, a Mitogen-specific implementation of `stackable connection plug-ins`_. This enables connections @@ -959,12 +960,12 @@ Sample Profiles Local VM connection ~~~~~~~~~~~~~~~~~~~ -This demonstrates Mitogen vs. connection pipelining to a local VM, executing -the 100 simple repeated steps of ``run_hostname_100_times.yml`` from the -examples directory. Mitogen requires **43x less bandwidth and 4.25x less -time**. +This demonstrates Mitogen vs. connection pipelining to a local VM executing +``bench/loop-100-items.yml``, which simply executes ``hostname`` 100 times. +Mitogen requires **43x less bandwidth and 6.5x less time**. -.. image:: images/ansible/run_hostname_100_times.png +.. image:: images/ansible/run_hostname_100_times_mito.svg +.. image:: images/ansible/run_hostname_100_times_plain.svg Kathmandu to Paris diff --git a/docs/api.rst b/docs/api.rst index bfba1f77..5cd5f2af 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -71,8 +71,8 @@ mitogen.parent mitogen.fakessh --------------- -.. image:: images/fakessh.png - :align: right +.. image:: images/fakessh.svg + :class: mitogen-right-300 .. automodule:: mitogen.fakessh .. currentmodule:: mitogen.fakessh diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 20d1c2b3..020760bc 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -13,8 +13,8 @@ Liability Waiver Before proceeding, it is critical you understand what you're involving yourself and possibly your team and its successors with: -.. image:: images/pandora.jpg - :align: right +.. image:: images/pandora.svg + :class: mitogen-right-350 * Constructing the most fundamental class, :py:class:`Broker `, causes a new thread to be spawned, exposing a huge @@ -78,7 +78,9 @@ operate across machines and privilege domains: Broker And Router ----------------- -.. image:: images/layout.png +.. image:: images/layout.svg + :class: mitogen-full-width + .. currentmodule:: mitogen.core Execution starts when your program constructs a :py:class:`Broker` and diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 65a6daee..1ff0af48 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -554,7 +554,8 @@ associated with that stream if the stream is disconnected for any reason. Example ####### -.. image:: images/context-tree.png +.. image:: images/context-tree.svg + :class: mitogen-full-width In the diagram, when ``node12b`` is creating the ``sudo:node12b:webapp`` context, it must send ``ADD_ROUTE`` messages to ``rack12``, which will @@ -572,7 +573,8 @@ When ``sudo:node22a:webapp`` wants to send a message to ``sudo:node22a:webapp -> node22a -> rack22 -> dc2 -> bastion -> dc1 -> rack12 -> node12b -> sudo:node12b:webapp`` -.. image:: images/route.png +.. image:: images/route.svg + :class: mitogen-full-width Disconnect Propagation diff --git a/docs/images/ansible/ansible_mitogen.svg b/docs/images/ansible/ansible_mitogen.svg new file mode 100644 index 00000000..922f1200 --- /dev/null +++ b/docs/images/ansible/ansible_mitogen.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/images/ansible/cell_division.png b/docs/images/ansible/cell_division.png deleted file mode 100644 index 6f0df4ca6b14fd2944336ab20c68ac29c6fc9e46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18066 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 TyrT)+*301O>gTe~DWM4fqlMc< diff --git a/docs/images/ansible/run_hostname_100_times.png b/docs/images/ansible/run_hostname_100_times.png deleted file mode 100644 index 00574a4bfe0eb0e24b37d29f28ea7a8ce9f066fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96250 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 z 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 C83exo diff --git a/docs/images/ansible/run_hostname_100_times_mito.svg b/docs/images/ansible/run_hostname_100_times_mito.svg new file mode 100644 index 00000000..53837ec9 --- /dev/null +++ b/docs/images/ansible/run_hostname_100_times_mito.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/ansible/run_hostname_100_times_plain.svg b/docs/images/ansible/run_hostname_100_times_plain.svg new file mode 100644 index 00000000..68cf8af3 --- /dev/null +++ b/docs/images/ansible/run_hostname_100_times_plain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/async.png b/docs/images/async.png index c73c288841b5acecb0260c2edd570d25e30cf857..fdbb74a05b31a13a843ba8eabbb5009831e4428d 100644 GIT binary patch literal 72907 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 diff --git a/docs/images/billing.svg b/docs/images/billing.svg new file mode 100644 index 00000000..abc09e22 --- /dev/null +++ b/docs/images/billing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/cell_division.png b/docs/images/cell_division.png deleted file mode 100644 index 067d5613fc5f20b4ddbd21b32f0379f168d157ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16666 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 EAIFMai~s-t diff --git a/docs/images/context-tree.svg b/docs/images/context-tree.svg new file mode 100644 index 00000000..9b39b8eb --- /dev/null +++ b/docs/images/context-tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/detached-subtree.png b/docs/images/detached-subtree.png deleted file mode 100644 index d8bb46df9cab5077510896cd85b51332d72f4d0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13488 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

7ewF5Rgc-*4KKz#EF1c@aJS<;- zqVd|Hw|2EMWUqi^txQ0!zaQ&!iW4I`n?BGPBB(Rr&oCi5@3XudM}^#V|1^ zRr{4oo(fl13o9G{4HYVF3HmOI(vrHO#26#^BeYA^c|W^EHpfzq#ezfg?(Bp7mMm8R0KHhD((-|6M`PN z_Kusb8JaQ4h4Fx z_PYd&SUN&(zFdFJaKBgL zMI%`lU#!*}bGWBguMmR1`cGvXwvVhBoBMe5c&+(w@XMqKCm^e-=+^H`B_#Wf-?RInW>ENrq4;!m(SJdqFYrWkg$uB$O@?TBg zx|`yNxfS=Q{9Vb#F1IXQZ@^gNeZ1xKd6p13zIk(>oZXC@Hii1=&`#vr2?q;R0KhdBr|l|UM{0?3;alFma^u7N%U(3=k8H~!}aKEy*( ziSV04@aWFP8WdRncF~@6r*1U>78ufS8B}M_Kq$;wK`+q{e0>gb($jQ*l$vEkJ=o&qKbZy?<{5da1!b1K{)EAgzZFNl_P`K00W@vh;F; zqU!J8zX`5TgMMM)EJ(orRS53OI1O88C%Ji-(E+v>w5IN*R575UA|jyIuYYdKOmZWk p2q8GexA&$>0q6;SC>>GwU~-RN>74=pj|71z$f!scJ}`Oy{{U^EGGYJ# diff --git a/docs/images/detached-subtree.svg b/docs/images/detached-subtree.svg new file mode 100644 index 00000000..cb4add59 --- /dev/null +++ b/docs/images/detached-subtree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/distribute2.png b/docs/images/distribute2.png index a738925d088cbf6e3d198cdc6a8cc878ff94982d..36acf154ed72ce972dc873ac9228870a86a428d2 100644 GIT binary patch literal 80898 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 Jm0KT;{R?IY`GNod diff --git a/docs/images/fakessh.png b/docs/images/fakessh.png deleted file mode 100644 index d25b006b6b9803d3f900cec7e55c57bdd9da812b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9416 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 diff --git a/docs/images/fakessh.svg b/docs/images/fakessh.svg new file mode 100644 index 00000000..2b4a4c05 --- /dev/null +++ b/docs/images/fakessh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/jumpbox.png b/docs/images/jumpbox.png deleted file mode 100644 index 9ec1aec7b19c4fada3278a5e25364c46b9582dc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14030 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 diff --git a/docs/images/jumpbox.svg b/docs/images/jumpbox.svg new file mode 100644 index 00000000..9c5138f0 --- /dev/null +++ b/docs/images/jumpbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/layout.png b/docs/images/layout.png deleted file mode 100644 index 2c4d952438fe3f0bffd6f4fa3766eded69c5d520..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26684 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 HokIT~!tLbr diff --git a/docs/images/layout.svg b/docs/images/layout.svg new file mode 100644 index 00000000..83b4901f --- /dev/null +++ b/docs/images/layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/mitogen.svg b/docs/images/mitogen.svg new file mode 100644 index 00000000..29081f99 --- /dev/null +++ b/docs/images/mitogen.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/images/pandora-orig.jpg b/docs/images/pandora-orig.jpg deleted file mode 100644 index 86deb6fca1c66364f0e05eaa770f7300cab6e1f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99013 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

2xt7Z7nu)bdY=a@7}0 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

0#YXWn7A?)0A6Vv?n>g012 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

e6YewR8h%@NW$X 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 diff --git a/docs/images/pandora.svg b/docs/images/pandora.svg new file mode 100644 index 00000000..3238d750 --- /dev/null +++ b/docs/images/pandora.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/images/persistent.png b/docs/images/persistent.png deleted file mode 100644 index 5ca6cd4ac162cbc45e11dc335f2f968f90ff8905..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12848 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 diff --git a/docs/images/persistent.svg b/docs/images/persistent.svg new file mode 100644 index 00000000..e059b070 --- /dev/null +++ b/docs/images/persistent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/pipe.png b/docs/images/pipe.png index 3d674273a470f6d33f69d38f9fc401a1de051fc7..b842b4470c06f94dcd65d48559f11afb6c997cc3 100644 GIT binary patch literal 76099 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

1Dki(XLtIZu|e8vf;h^6E{$HX5p~{Z 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 literal 21092 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