From 3ae80ef3bba9d9b068bc49d50ccc0a8a2f3302f7 Mon Sep 17 00:00:00 2001 From: "Trez.One" Date: Thu, 9 Apr 2026 19:51:43 -0400 Subject: [PATCH 1/3] Adding Mitogen plugin and strategy. --- ansible.cfg | 1 + plugins/mitogen-0.3.44.tar.gz | Bin 0 -> 241367 bytes plugins/mitogen-0.3.44/LICENSE | 26 + plugins/mitogen-0.3.44/MANIFEST.in | 1 + plugins/mitogen-0.3.44/PKG-INFO | 52 + plugins/mitogen-0.3.44/README.md | 9 + .../ansible_mitogen/__init__.py | 0 .../ansible_mitogen/affinity.py | 287 ++ .../ansible_mitogen/compat/__init__.py | 0 .../ansible_mitogen/connection.py | 1209 +++++ .../mitogen-0.3.44/ansible_mitogen/loaders.py | 86 + .../mitogen-0.3.44/ansible_mitogen/logging.py | 127 + .../mitogen-0.3.44/ansible_mitogen/mixins.py | 504 ++ .../ansible_mitogen/module_finder.py | 279 ++ .../mitogen-0.3.44/ansible_mitogen/parsing.py | 77 + .../mitogen-0.3.44/ansible_mitogen/planner.py | 711 +++ .../ansible_mitogen/plugins/__init__.py | 0 .../plugins/action/__init__.py | 0 .../plugins/action/mitogen_fetch.py | 207 + .../plugins/action/mitogen_get_stack.py | 58 + .../plugins/connection/__init__.py | 0 .../plugins/connection/mitogen_buildah.py | 44 + .../plugins/connection/mitogen_doas.py | 44 + .../plugins/connection/mitogen_docker.py | 51 + .../plugins/connection/mitogen_incus.py | 21 + .../plugins/connection/mitogen_jail.py | 44 + .../plugins/connection/mitogen_kubectl.py | 73 + .../plugins/connection/mitogen_local.py | 80 + .../plugins/connection/mitogen_lxc.py | 44 + .../plugins/connection/mitogen_lxd.py | 44 + .../plugins/connection/mitogen_machinectl.py | 44 + .../plugins/connection/mitogen_podman.py | 44 + .../plugins/connection/mitogen_setns.py | 44 + .../plugins/connection/mitogen_ssh.py | 70 + .../plugins/connection/mitogen_su.py | 44 + .../plugins/connection/mitogen_sudo.py | 44 + .../plugins/strategy/__init__.py | 0 .../plugins/strategy/mitogen.py | 61 + .../plugins/strategy/mitogen_free.py | 62 + .../plugins/strategy/mitogen_host_pinned.py | 67 + .../plugins/strategy/mitogen_linear.py | 62 + .../mitogen-0.3.44/ansible_mitogen/process.py | 710 +++ .../mitogen-0.3.44/ansible_mitogen/runner.py | 1098 +++++ .../ansible_mitogen/services.py | 559 +++ .../ansible_mitogen/strategy.py | 397 ++ .../mitogen-0.3.44/ansible_mitogen/target.py | 755 +++ .../ansible_mitogen/transport_config.py | 891 ++++ .../ansible_mitogen/utils/__init__.py | 29 + .../ansible_mitogen/utils/unsafe.py | 123 + plugins/mitogen-0.3.44/mitogen/__init__.py | 121 + plugins/mitogen-0.3.44/mitogen/buildah.py | 72 + .../mitogen-0.3.44/mitogen/compat/__init__.py | 0 .../mitogen-0.3.44/mitogen/compat/pkgutil.py | 594 +++ .../mitogen-0.3.44/mitogen/compat/tokenize.py | 453 ++ plugins/mitogen-0.3.44/mitogen/core.py | 4349 +++++++++++++++++ plugins/mitogen-0.3.44/mitogen/debug.py | 236 + plugins/mitogen-0.3.44/mitogen/doas.py | 142 + plugins/mitogen-0.3.44/mitogen/docker.py | 83 + plugins/mitogen-0.3.44/mitogen/fakessh.py | 460 ++ plugins/mitogen-0.3.44/mitogen/fork.py | 250 + .../mitogen/imports/__init__.py | 38 + .../mitogen-0.3.44/mitogen/imports/_py2.py | 54 + .../mitogen-0.3.44/mitogen/imports/_py314.py | 26 + .../mitogen-0.3.44/mitogen/imports/_py36.py | 25 + plugins/mitogen-0.3.44/mitogen/incus.py | 50 + plugins/mitogen-0.3.44/mitogen/jail.py | 65 + plugins/mitogen-0.3.44/mitogen/kubectl.py | 66 + plugins/mitogen-0.3.44/mitogen/lxc.py | 73 + plugins/mitogen-0.3.44/mitogen/lxd.py | 75 + plugins/mitogen-0.3.44/mitogen/master.py | 1566 ++++++ plugins/mitogen-0.3.44/mitogen/minify.py | 143 + plugins/mitogen-0.3.44/mitogen/os_fork.py | 187 + plugins/mitogen-0.3.44/mitogen/parent.py | 2840 +++++++++++ plugins/mitogen-0.3.44/mitogen/podman.py | 73 + plugins/mitogen-0.3.44/mitogen/profiler.py | 164 + plugins/mitogen-0.3.44/mitogen/select.py | 348 ++ plugins/mitogen-0.3.44/mitogen/service.py | 1168 +++++ plugins/mitogen-0.3.44/mitogen/setns.py | 241 + plugins/mitogen-0.3.44/mitogen/ssh.py | 306 ++ plugins/mitogen-0.3.44/mitogen/su.py | 155 + plugins/mitogen-0.3.44/mitogen/sudo.py | 294 ++ plugins/mitogen-0.3.44/mitogen/unix.py | 233 + plugins/mitogen-0.3.44/mitogen/utils.py | 230 + plugins/mitogen-0.3.44/setup.cfg | 18 + plugins/mitogen-0.3.44/setup.py | 112 + plugins/mitogen-0.3.44/tests/testlib.py | 680 +++ .../tests/testmod_system_distro.py | 2 + .../mitogen-0.3.44/tests/testmod_toplevel.py | 28 + .../mitogen-0.3.44/tests/testmods/__init__.py | 0 .../module_finder_testmod/__init__.py | 0 .../module_finder_testmod/empty_mod.py | 0 .../module_finder_testmod/regular_mod.py | 2 + .../sibling_dep_mod_abs_import.py | 2 + .../sibling_dep_mod_py2_import.py | 1 + .../sibling_dep_mod_rel_import.py | 1 + .../testmods/pkg_like_ansible/__init__.py | 0 .../pkg_like_ansible/module_utils/__init__.py | 0 .../module_utils/distro/__init__.py | 5 + .../module_utils/distro/_distro.py | 1 + .../module_utils/sys_distro/__init__.py | 5 + .../module_utils/sys_distro/_distro.py | 1 + .../testmods/pkg_like_plumbum/__init__.py | 0 .../tests/testmods/pkg_like_plumbum/colors.py | 14 + .../tests/testmods/resourced_pkg/__init__.py | 0 .../resourced_pkg/sub_pkg/__init__.py | 0 .../tests/testmods/simple_pkg/__init__.py | 0 .../tests/testmods/simple_pkg/a.py | 5 + .../tests/testmods/simple_pkg/b.py | 2 + .../simple_pkg/imports_replaces_self.py | 6 + .../tests/testmods/simple_pkg/ping.py | 4 + .../testmods/simple_pkg/replaces_self.py | 4 + .../tests/testmods/six_brokenpkg/__init__.py | 56 + .../tests/testmods/six_brokenpkg/_six.py | 868 ++++ .../tests/testmods/stdio_checks.py | 31 + tar-valon_config_deploy.yml | 1 + 115 files changed, 26212 insertions(+) create mode 100644 plugins/mitogen-0.3.44.tar.gz create mode 100644 plugins/mitogen-0.3.44/LICENSE create mode 100644 plugins/mitogen-0.3.44/MANIFEST.in create mode 100644 plugins/mitogen-0.3.44/PKG-INFO create mode 100644 plugins/mitogen-0.3.44/README.md create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/__init__.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/affinity.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/compat/__init__.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/connection.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/loaders.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/logging.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/mixins.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/module_finder.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/parsing.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/planner.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/__init__.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/__init__.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_fetch.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_get_stack.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/__init__.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_buildah.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_doas.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_docker.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_incus.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_jail.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_kubectl.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_local.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxc.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxd.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_machinectl.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_podman.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_setns.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_ssh.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_su.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_sudo.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/__init__.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_free.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_host_pinned.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_linear.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/process.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/runner.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/services.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/strategy.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/target.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/transport_config.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/utils/__init__.py create mode 100644 plugins/mitogen-0.3.44/ansible_mitogen/utils/unsafe.py create mode 100644 plugins/mitogen-0.3.44/mitogen/__init__.py create mode 100644 plugins/mitogen-0.3.44/mitogen/buildah.py create mode 100644 plugins/mitogen-0.3.44/mitogen/compat/__init__.py create mode 100644 plugins/mitogen-0.3.44/mitogen/compat/pkgutil.py create mode 100644 plugins/mitogen-0.3.44/mitogen/compat/tokenize.py create mode 100644 plugins/mitogen-0.3.44/mitogen/core.py create mode 100644 plugins/mitogen-0.3.44/mitogen/debug.py create mode 100644 plugins/mitogen-0.3.44/mitogen/doas.py create mode 100644 plugins/mitogen-0.3.44/mitogen/docker.py create mode 100644 plugins/mitogen-0.3.44/mitogen/fakessh.py create mode 100644 plugins/mitogen-0.3.44/mitogen/fork.py create mode 100644 plugins/mitogen-0.3.44/mitogen/imports/__init__.py create mode 100644 plugins/mitogen-0.3.44/mitogen/imports/_py2.py create mode 100644 plugins/mitogen-0.3.44/mitogen/imports/_py314.py create mode 100644 plugins/mitogen-0.3.44/mitogen/imports/_py36.py create mode 100644 plugins/mitogen-0.3.44/mitogen/incus.py create mode 100644 plugins/mitogen-0.3.44/mitogen/jail.py create mode 100644 plugins/mitogen-0.3.44/mitogen/kubectl.py create mode 100644 plugins/mitogen-0.3.44/mitogen/lxc.py create mode 100644 plugins/mitogen-0.3.44/mitogen/lxd.py create mode 100644 plugins/mitogen-0.3.44/mitogen/master.py create mode 100644 plugins/mitogen-0.3.44/mitogen/minify.py create mode 100644 plugins/mitogen-0.3.44/mitogen/os_fork.py create mode 100644 plugins/mitogen-0.3.44/mitogen/parent.py create mode 100644 plugins/mitogen-0.3.44/mitogen/podman.py create mode 100644 plugins/mitogen-0.3.44/mitogen/profiler.py create mode 100644 plugins/mitogen-0.3.44/mitogen/select.py create mode 100644 plugins/mitogen-0.3.44/mitogen/service.py create mode 100644 plugins/mitogen-0.3.44/mitogen/setns.py create mode 100644 plugins/mitogen-0.3.44/mitogen/ssh.py create mode 100644 plugins/mitogen-0.3.44/mitogen/su.py create mode 100644 plugins/mitogen-0.3.44/mitogen/sudo.py create mode 100644 plugins/mitogen-0.3.44/mitogen/unix.py create mode 100644 plugins/mitogen-0.3.44/mitogen/utils.py create mode 100644 plugins/mitogen-0.3.44/setup.cfg create mode 100644 plugins/mitogen-0.3.44/setup.py create mode 100644 plugins/mitogen-0.3.44/tests/testlib.py create mode 100644 plugins/mitogen-0.3.44/tests/testmod_system_distro.py create mode 100755 plugins/mitogen-0.3.44/tests/testmod_toplevel.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/module_finder_testmod/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/module_finder_testmod/empty_mod.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/module_finder_testmod/regular_mod.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/module_finder_testmod/sibling_dep_mod_abs_import.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/module_finder_testmod/sibling_dep_mod_py2_import.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/module_finder_testmod/sibling_dep_mod_rel_import.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_ansible/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_ansible/module_utils/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_ansible/module_utils/distro/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_ansible/module_utils/distro/_distro.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_ansible/module_utils/sys_distro/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_ansible/module_utils/sys_distro/_distro.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_plumbum/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/pkg_like_plumbum/colors.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/resourced_pkg/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/resourced_pkg/sub_pkg/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/simple_pkg/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/simple_pkg/a.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/simple_pkg/b.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/simple_pkg/imports_replaces_self.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/simple_pkg/ping.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/simple_pkg/replaces_self.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/six_brokenpkg/__init__.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/six_brokenpkg/_six.py create mode 100644 plugins/mitogen-0.3.44/tests/testmods/stdio_checks.py diff --git a/ansible.cfg b/ansible.cfg index e43d1f2..8fae853 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -3,3 +3,4 @@ inventory = inventory/hosts.yml collections_path = ./collections host_key_checking = False retry_files_enabled = False +strategy_plugins = ../plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy diff --git a/plugins/mitogen-0.3.44.tar.gz b/plugins/mitogen-0.3.44.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..5c38f2ef03aeab470baad24a442f89e710256f9b GIT binary patch literal 241367 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.4, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.* +Description-Content-Type: text/markdown +License-File: LICENSE +Dynamic: author +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: requires-python +Dynamic: summary + +# Mitogen + +[![PyPI - Version](https://img.shields.io/pypi/v/mitogen)](https://pypi.org/project/mitogen/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mitogen)](https://pypi.org/project/mitogen/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/mitogen-hq/mitogen/tests.yml?branch=master)](https://github.com/mitogen-hq/mitogen/actions?query=branch%3Amaster) + +Please see the documentation. + +![](https://i.imgur.com/eBM6LhJ.gif) diff --git a/plugins/mitogen-0.3.44/README.md b/plugins/mitogen-0.3.44/README.md new file mode 100644 index 0000000..26e033e --- /dev/null +++ b/plugins/mitogen-0.3.44/README.md @@ -0,0 +1,9 @@ +# Mitogen + +[![PyPI - Version](https://img.shields.io/pypi/v/mitogen)](https://pypi.org/project/mitogen/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mitogen)](https://pypi.org/project/mitogen/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/mitogen-hq/mitogen/tests.yml?branch=master)](https://github.com/mitogen-hq/mitogen/actions?query=branch%3Amaster) + +Please see the documentation. + +![](https://i.imgur.com/eBM6LhJ.gif) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/__init__.py b/plugins/mitogen-0.3.44/ansible_mitogen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/affinity.py b/plugins/mitogen-0.3.44/ansible_mitogen/affinity.py new file mode 100644 index 0000000..223794a --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/affinity.py @@ -0,0 +1,287 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +As Mitogen separates asynchronous IO out to a broker thread, communication +necessarily involves context switching and waking that thread. When application +threads and the broker share a CPU, this can be almost invisibly fast - around +25 microseconds for a full A->B->A round-trip. + +However when threads are scheduled on different CPUs, round-trip delays +regularly vary wildly, and easily into milliseconds. Many contributing factors +exist, not least scenarios like: + +1. A is preempted immediately after waking B, but before releasing the GIL. +2. B wakes from IO wait only to immediately enter futex wait. +3. A may wait 10ms or more for another timeslice, as the scheduler on its CPU + runs threads unrelated to its transaction (i.e. not B), wake only to release + its GIL, before entering IO sleep waiting for a reply from B, which cannot + exist yet. +4. B wakes, acquires GIL, performs work, and sends reply to A, causing it to + wake. B is preempted before releasing GIL. +5. A wakes from IO wait only to immediately enter futex wait. +6. B may wait 10ms or more for another timeslice, wake only to release its GIL, + before sleeping again. +7. A wakes, acquires GIL, finally receives reply. + +Per above if we are unlucky, on an even moderately busy machine it is possible +to lose milliseconds just in scheduling delay, and the effect is compounded +when pairs of threads in process A are communicating with pairs of threads in +process B using the same scheme, such as when Ansible WorkerProcess is +communicating with ContextService in the connection multiplexer. In the worst +case it could involve 4 threads working in lockstep spread across 4 busy CPUs. + +Since multithreading in Python is essentially useless except for waiting on IO +due to the presence of the GIL, at least in Ansible there is no good reason for +threads in the same process to run on distinct CPUs - they always operate in +lockstep due to the GIL, and are thus vulnerable to issues like above. + +Linux lacks any natural API to describe what we want, it only permits +individual threads to be constrained to run on specific CPUs, and for that +constraint to be inherited by new threads and forks of the constrained thread. + +This module therefore implements a CPU pinning policy for Ansible processes, +providing methods that should be called early in any new process, either to +rebalance which CPU it is pinned to, or in the case of subprocesses, to remove +the pinning entirely. It is likely to require ongoing tweaking, since pinning +necessarily involves preventing the scheduler from making load balancing +decisions. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import ctypes +import logging +import mmap +import multiprocessing +import os +import struct + +import mitogen.parent + + +LOG = logging.getLogger(__name__) + + +try: + _libc = ctypes.CDLL(None, use_errno=True) + _strerror = _libc.strerror + _strerror.restype = ctypes.c_char_p + _sem_init = _libc.sem_init + _sem_wait = _libc.sem_wait + _sem_post = _libc.sem_post + _sched_setaffinity = _libc.sched_setaffinity +except (OSError, AttributeError): + _libc = None + _strerror = None + _sem_init = None + _sem_wait = None + _sem_post = None + _sched_setaffinity = None + + +class sem_t(ctypes.Structure): + """ + Wrap sem_t to allow storing a lock in shared memory. + """ + _fields_ = [ + ('data', ctypes.c_uint8 * 128), + ] + + def init(self): + if _sem_init(self.data, 1, 1): + raise Exception(_strerror(ctypes.get_errno())) + + def acquire(self): + if _sem_wait(self.data): + raise Exception(_strerror(ctypes.get_errno())) + + def release(self): + if _sem_post(self.data): + raise Exception(_strerror(ctypes.get_errno())) + + +class State(ctypes.Structure): + """ + Contents of shared memory segment. This allows :meth:`Manager.assign` to be + called from any child, since affinity assignment must happen from within + the context of the new child process. + """ + _fields_ = [ + ('lock', sem_t), + ('counter', ctypes.c_uint8), + ] + + +class Policy(object): + """ + Process affinity policy. + """ + def assign_controller(self): + """ + Assign the Ansible top-level policy to this process. + """ + + def assign_muxprocess(self, index): + """ + Assign the MuxProcess policy to this process. + """ + + def assign_worker(self): + """ + Assign the WorkerProcess policy to this process. + """ + + def assign_subprocess(self): + """ + Assign the helper subprocess policy to this process. + """ + +class FixedPolicy(Policy): + """ + :class:`Policy` for machines where the only control method available is + fixed CPU placement. The scheme here was tested on an otherwise idle 16 + thread machine. + + - The connection multiplexer is pinned to CPU 0. + - The Ansible top-level (strategy) is pinned to CPU 1. + - WorkerProcesses are pinned sequentually to 2..N, wrapping around when no + more CPUs exist. + - Children such as SSH may be scheduled on any CPU except 0/1. + + If the machine has less than 4 cores available, the top-level and workers + are pinned between CPU 2..N, i.e. no CPU is reserved for the top-level + process. + + This could at least be improved by having workers pinned to independent + cores, before reusing the second hyperthread of an existing core. + + A hook is installed that causes :meth:`reset` to run in the child of any + process created with :func:`mitogen.parent.popen`, ensuring CPU-intensive + children like SSH are not forced to share the same core as the (otherwise + potentially very busy) parent. + """ + def __init__(self, cpu_count=None): + #: For tests. + self.cpu_count = cpu_count or multiprocessing.cpu_count() + self.mem = mmap.mmap(-1, 4096) + self.state = State.from_buffer(self.mem) + self.state.lock.init() + + if self.cpu_count < 2: + # uniprocessor + self._reserve_mux = False + self._reserve_controller = False + self._reserve_mask = 0 + self._reserve_shift = 0 + elif self.cpu_count < 4: + # small SMP + self._reserve_mux = True + self._reserve_controller = False + self._reserve_mask = 1 + self._reserve_shift = 1 + else: + # big SMP + self._reserve_mux = True + self._reserve_controller = True + self._reserve_mask = 3 + self._reserve_shift = 2 + + def _set_affinity(self, descr, mask): + if descr: + LOG.debug('CPU mask for %s: %#08x', descr, mask) + mitogen.parent._preexec_hook = self._clear + self._set_cpu_mask(mask) + + def _balance(self, descr): + self.state.lock.acquire() + try: + n = self.state.counter + self.state.counter += 1 + finally: + self.state.lock.release() + + self._set_cpu(descr, self._reserve_shift + ( + (n % (self.cpu_count - self._reserve_shift)) + )) + + def _set_cpu(self, descr, cpu): + self._set_affinity(descr, 1 << (cpu % self.cpu_count)) + + def _clear(self): + all_cpus = (1 << self.cpu_count) - 1 + self._set_affinity(None, all_cpus & ~self._reserve_mask) + + def assign_controller(self): + if self._reserve_controller: + self._set_cpu('Ansible top-level process', 1) + else: + self._balance('Ansible top-level process') + + def assign_muxprocess(self, index): + self._set_cpu('MuxProcess %d' % (index,), index) + + def assign_worker(self): + self._balance('WorkerProcess') + + def assign_subprocess(self): + self._clear() + + +class LinuxPolicy(FixedPolicy): + def _mask_to_bytes(self, mask): + """ + Convert the (type long) mask to a cpu_set_t. + """ + chunks = [] + shiftmask = (2 ** 64) - 1 + for x in range(16): + chunks.append(struct.pack('>= 64 + return b''.join(chunks) + + def _get_thread_ids(self): + try: + ents = os.listdir('/proc/self/task') + except OSError: + LOG.debug('cannot fetch thread IDs for current process') + return [os.getpid()] + + return [int(s) for s in ents if s.isdigit()] + + def _set_cpu_mask(self, mask): + s = self._mask_to_bytes(mask) + for tid in self._get_thread_ids(): + _sched_setaffinity(tid, len(s), s) + + +if _sched_setaffinity is not None: + policy = LinuxPolicy() +else: + policy = Policy() diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/compat/__init__.py b/plugins/mitogen-0.3.44/ansible_mitogen/compat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/connection.py b/plugins/mitogen-0.3.44/ansible_mitogen/connection.py new file mode 100644 index 0000000..170fb80 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/connection.py @@ -0,0 +1,1209 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +from __future__ import unicode_literals +__metaclass__ = type + +import errno +import logging +import os +import pprint +import stat +import sys +import time + +import ansible.constants as C +import ansible.errors +import ansible.plugins.connection + +import mitogen.core +import mitogen.parent +import mitogen.service + +import ansible_mitogen.mixins +import ansible_mitogen.process +import ansible_mitogen.target +import ansible_mitogen.transport_config +import ansible_mitogen.utils.unsafe + + +LOG = logging.getLogger(__name__) + +task_vars_msg = ( + 'could not recover task_vars. This means some connection ' + 'settings may erroneously be reset to their defaults. ' + 'Please report a bug if you encounter this message.' +) + + +def get_remote_name(spec): + """ + Return the value to use for the "remote_name" parameter. + """ + if spec.mitogen_mask_remote_name(): + return 'ansible' + return None + + +def optional_int(value): + """ + Convert `value` to an integer if it is not :data:`None`, otherwise return + :data:`None`. + """ + try: + return int(value) + except (TypeError, ValueError): + return None + + +def convert_bool(obj): + if isinstance(obj, bool): + return obj + if str(obj).lower() in ('no', 'false', '0'): + return False + if str(obj).lower() not in ('yes', 'true', '1'): + raise ansible.errors.AnsibleConnectionFailure( + 'expected yes/no/true/false/0/1, got %r' % (obj,) + ) + return True + + +def default(value, default): + """ + Return `default` is `value` is :data:`None`, otherwise return `value`. + """ + if value is None: + return default + return value + + +def _connect_local(spec): + """ + Return ContextService arguments for a local connection. + """ + return { + 'method': 'local', + 'kwargs': { + 'python_path': spec.python_path(), + } + } + + +def _connect_ssh(spec): + """ + Return ContextService arguments for an SSH connection. + """ + if spec.host_key_checking(): + check_host_keys = 'enforce' + 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': { + 'check_host_keys': check_host_keys, + 'hostname': spec.remote_addr(), + 'username': spec.remote_user(), + 'compression': convert_bool( + default(spec.mitogen_ssh_compression(), True) + ), + 'password': spec.password(), + 'port': spec.port(), + 'python_path': spec.python_path(), + 'identity_file': private_key_file, + 'identities_only': False, + 'ssh_path': spec.ssh_executable(), + 'connect_timeout': spec.timeout(), + 'ssh_args': spec.ssh_args(), + 'ssh_debug_level': spec.verbosity(), + 'remote_name': get_remote_name(spec), + 'keepalive_count': ( + spec.mitogen_ssh_keepalive_count() or 10 + ), + 'keepalive_interval': ( + spec.mitogen_ssh_keepalive_interval() or 30 + ), + } + } + + +def _connect_buildah(spec): + """ + Return ContextService arguments for a Buildah connection. + """ + return { + 'method': 'buildah', + 'kwargs': { + 'username': spec.remote_user(), + 'container': spec.remote_addr(), + 'python_path': spec.python_path(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_docker(spec): + """ + Return ContextService arguments for a Docker connection. + """ + return { + 'method': 'docker', + 'kwargs': { + 'username': spec.remote_user(), + 'container': spec.remote_addr(), + 'python_path': spec.python_path(rediscover_python=True), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_kubectl(spec): + """ + Return ContextService arguments for a Kubernetes connection. + """ + return { + 'method': 'kubectl', + 'kwargs': { + 'pod': spec.remote_addr(), + 'python_path': spec.python_path(), + 'connect_timeout': spec.timeout(), + 'kubectl_path': spec.mitogen_kubectl_path(), + 'kubectl_args': spec.extra_args(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_jail(spec): + """ + Return ContextService arguments for a FreeBSD jail connection. + """ + return { + 'method': 'jail', + 'kwargs': { + 'username': spec.remote_user(), + 'container': spec.remote_addr(), + 'python_path': spec.python_path(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_lxc(spec): + """ + Return ContextService arguments for an LXC Classic container connection. + """ + return { + 'method': 'lxc', + 'kwargs': { + 'container': spec.remote_addr(), + 'python_path': spec.python_path(), + 'lxc_attach_path': spec.mitogen_lxc_attach_path(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_lxd(spec): + """ + Return ContextService arguments for an LXD container connection. + """ + return { + 'method': 'lxd', + 'kwargs': { + 'container': spec.remote_addr(), + 'python_path': spec.python_path(), + 'lxc_path': spec.mitogen_lxc_path(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_incus(spec): + """ + Return ContextService arguments for an Incus container connection. + """ + return { + 'method': 'incus', + 'kwargs': { + 'container': spec.remote_addr(), + 'python_path': spec.python_path(), + 'incus_path': spec.mitogen_incus_path(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_machinectl(spec): + """ + Return ContextService arguments for a machinectl connection. + """ + return _connect_setns(spec, kind='machinectl') + + +def _connect_podman(spec): + """ + Return ContextService arguments for a Docker connection. + """ + return { + 'method': 'podman', + 'kwargs': { + 'username': spec.remote_user(), + 'container': spec.remote_addr(), + 'python_path': spec.python_path(rediscover_python=True), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_setns(spec, kind=None): + """ + Return ContextService arguments for a mitogen_setns connection. + """ + return { + 'method': 'setns', + 'kwargs': { + 'container': spec.remote_addr(), + 'username': spec.remote_user(), + 'python_path': spec.python_path(), + 'kind': kind or 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(), + } + } + + +def _connect_su(spec): + """ + Return ContextService arguments for su as a become method. + """ + return { + 'method': 'su', + 'enable_lru': True, + 'kwargs': { + 'username': spec.become_user(), + 'password': spec.become_pass(), + 'python_path': spec.python_path(), + 'su_path': spec.become_exe(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_sudo(spec): + """ + Return ContextService arguments for sudo as a become method. + """ + return { + 'method': 'sudo', + 'enable_lru': True, + 'kwargs': { + 'username': spec.become_user(), + 'password': spec.become_pass(), + 'python_path': spec.python_path(), + 'sudo_path': spec.become_exe(), + 'connect_timeout': spec.timeout(), + 'sudo_args': spec.sudo_args(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_doas(spec): + """ + Return ContextService arguments for doas as a become method. + """ + return { + 'method': 'doas', + 'enable_lru': True, + 'kwargs': { + 'username': spec.become_user(), + 'password': spec.become_pass(), + 'python_path': spec.python_path(), + 'doas_path': spec.become_exe(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_mitogen_su(spec): + """ + Return ContextService arguments for su as a first class connection. + """ + return { + 'method': 'su', + 'kwargs': { + 'username': spec.remote_user(), + 'password': spec.password(), + 'python_path': spec.python_path(), + 'su_path': spec.become_exe(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_mitogen_sudo(spec): + """ + Return ContextService arguments for sudo as a first class connection. + """ + return { + 'method': 'sudo', + 'kwargs': { + 'username': spec.remote_user(), + 'password': spec.password(), + 'python_path': spec.python_path(), + 'sudo_path': spec.become_exe(), + 'connect_timeout': spec.timeout(), + 'sudo_args': spec.sudo_args(), + 'remote_name': get_remote_name(spec), + } + } + + +def _connect_mitogen_doas(spec): + """ + Return ContextService arguments for doas as a first class connection. + """ + return { + 'method': 'doas', + 'kwargs': { + 'username': spec.remote_user(), + 'password': spec.password(), + 'python_path': spec.python_path(), + 'doas_path': spec.ansible_doas_exe(), + 'connect_timeout': spec.timeout(), + 'remote_name': get_remote_name(spec), + } + } + + +#: Mapping of connection method names to functions invoked as `func(spec)` +#: generating ContextService keyword arguments matching a connection +#: specification. +CONNECTION_METHOD = { + # Ansible connection plugins + 'buildah': _connect_buildah, + 'docker': _connect_docker, + 'kubectl': _connect_kubectl, + 'jail': _connect_jail, + 'local': _connect_local, + 'incus': _connect_incus, + 'lxc': _connect_lxc, + 'lxd': _connect_lxd, + 'machinectl': _connect_machinectl, + 'podman': _connect_podman, + 'setns': _connect_setns, + 'ssh': _connect_ssh, + 'smart': _connect_ssh, # issue #548. + + # Ansible become plugins + 'community.general.doas': _connect_doas, + 'su': _connect_su, + 'sudo': _connect_sudo, + 'doas': _connect_doas, + + # Mitogen specific methods + 'mitogen_su': _connect_mitogen_su, + 'mitogen_sudo': _connect_mitogen_sudo, + 'mitogen_doas': _connect_mitogen_doas, +} + + +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 pending, 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,) + ) + + def call(self, func, *args, **kwargs): + """ + Like :meth:`mitogen.parent.CallChain.call`, but log timings. + """ + t0 = time.time() + try: + recv = self.call_async(func, *args, **kwargs) + return self._rethrow(recv) + finally: + LOG.debug('Call took %d ms: %r', 1000 * (time.time() - t0), + mitogen.parent.CallSpec(func, args, kwargs)) + + +class Connection(ansible.plugins.connection.ConnectionBase): + #: The :class:`ansible_mitogen.process.Binding` representing the connection + #: multiplexer this connection's target is assigned to. :data:`None` when + #: disconnected. + binding = None + + #: mitogen.parent.Context for the target account on the target, possibly + #: reached via become. + context = None + + #: Context for the login account on the target. This is always the login + #: account, even when become=True. + login_context = None + + #: Only sudo, su, and doas are supported for now. + # Ansible ConnectionBase attribute, removed in Ansible >= 2.8 + become_methods = ['sudo', 'su', 'doas'] + + #: Dict containing init_child() return value as recorded at startup by + #: ContextService. Contains: + #: + #: fork_context: Context connected to the fork parent : process in the + #: target account. + #: home_dir: Target context's home directory. + #: good_temp_dir: A writeable directory where new temporary directories + #: can be created. + init_child_result = None + + #: A :class:`mitogen.parent.CallChain` for calls made to the target + #: account, to ensure subsequent calls fail with the original exception if + #: pipelined directory creation or file transfer fails. + chain = None + + # + # Note: any of the attributes below may be :data:`None` if the connection + # plugin was constructed directly by a non-cooperative action, such as in + # the case of the synchronize module. + # + + #: Set to task_vars by on_action_run(). + _task_vars = None + + #: Set by on_action_run() + delegate_to_hostname = None + + #: Set to '_loader.get_basedir()' by on_action_run(). Used by mitogen_local + #: to change the working directory to that of the current playbook, + #: matching vanilla Ansible behaviour. + loader_basedir = None + + # set by `_get_task_vars()` for interpreter discovery + _action = None + + def on_action_run(self, task_vars, delegate_to_hostname, loader_basedir): + """ + Invoked by ActionModuleMixin to indicate a new task is about to start + executing. We use the opportunity to grab relevant bits from the + task-specific data. + + :param dict task_vars: + Task variable dictionary. + :param str delegate_to_hostname: + :data:`None`, or the template-expanded inventory hostname this task + is being delegated to. A similar variable exists on PlayContext + when ``delegate_to:`` is active, however it is unexpanded. + :param str loader_basedir: + Loader base directory; see :attr:`loader_basedir`. + """ + self._task_vars = task_vars + self.delegate_to_hostname = delegate_to_hostname + self.loader_basedir = loader_basedir + self._put_connection() + + def _get_task_vars(self): + """ + More information is needed than normally provided to an Ansible + connection. For proxied connections, intermediary configuration must + be inferred, and for any connection the configured Python interpreter + must be known. + + There is no clean way to access this information that would not deviate + from the running Ansible version. The least invasive method known is to + reuse the running task's task_vars dict. + + This method walks the stack to find task_vars of the Action plugin's + run(), or if no Action is present, from Strategy's _execute_meta(), as + in the case of 'meta: reset_connection'. The stack is walked in + addition to subclassing Action.run()/on_action_run(), as it is possible + for new connections to be constructed in addition to the preconstructed + connection passed into any running action. + """ + if self._task_vars is not None: + # check for if self._action has already been set or not + # there are some cases where the ansible executor passes in task_vars + # so we don't walk the stack to find them + # TODO: is there a better way to get the ActionModuleMixin object? + # ansible python discovery needs it to run discover_interpreter() + if not isinstance(self._action, ansible_mitogen.mixins.ActionModuleMixin): + f = sys._getframe() + while f: + if f.f_code.co_name == 'run': + f_self = f.f_locals.get('self') + if isinstance(f_self, ansible_mitogen.mixins.ActionModuleMixin): + self._action = f_self + break + elif f.f_code.co_name == '_execute_meta': + break + f = f.f_back + + return self._task_vars + + f = sys._getframe() + while f: + if f.f_code.co_name == 'run': + f_locals = f.f_locals + f_self = f_locals.get('self') + if isinstance(f_self, ansible_mitogen.mixins.ActionModuleMixin): + # backref for python interpreter discovery, should be safe because _get_task_vars + # is always called before running interpreter discovery + self._action = f_self + task_vars = f_locals.get('task_vars') + if task_vars: + LOG.debug('recovered task_vars from Action') + return task_vars + elif f.f_code.co_name == '_execute_meta': + f_all_vars = f.f_locals.get('all_vars') + if isinstance(f_all_vars, dict): + LOG.debug('recovered task_vars from meta:') + return f_all_vars + + f = f.f_back + + raise ansible.errors.AnsibleConnectionFailure(task_vars_msg) + + def get_host_vars(self, inventory_hostname): + """ + Fetch the HostVars for a host. + + :returns: + Variables dictionary or :data:`None`. + :raises ansible.errors.AnsibleConnectionFailure: + Task vars unavailable. + """ + task_vars = self._get_task_vars() + hostvars = task_vars.get('hostvars') + if hostvars: + return hostvars.get(inventory_hostname) + + raise ansible.errors.AnsibleConnectionFailure(task_vars_msg) + + def get_task_var(self, key, default=None): + """ + Fetch the value of a task variable related to connection configuration, + or, if delegate_to is active, fetch the same variable via HostVars for + the delegated-to machine. + + When running with delegate_to, Ansible tasks have variables associated + with the original machine, not the delegated-to machine, therefore it + does not make sense to extract connection-related configuration for the + delegated-to machine from them. + """ + def _fetch_task_var(task_vars, key): + """ + Special helper func in case vars can be templated + """ + SPECIAL_TASK_VARS = [ + 'ansible_python_interpreter' + ] + if key in task_vars: + val = task_vars[key] + if '{' in str(val) and key in SPECIAL_TASK_VARS: + # template every time rather than storing in a cache + # in case a different template value is used in a different task + val = self.templar.template( + val, + preserve_trailing_newlines=True, + escape_backslashes=False + ) + return val + + task_vars = self._get_task_vars() + if self.delegate_to_hostname is None: + return _fetch_task_var(task_vars, key) + else: + delegated_vars = task_vars['ansible_delegated_vars'] + if self.delegate_to_hostname in delegated_vars: + task_vars = delegated_vars[self.delegate_to_hostname] + return _fetch_task_var(task_vars, key) + + return default + + @property + def homedir(self): + self._connect() + return self.init_child_result['home_dir'] + + def get_binding(self): + """ + Return the :class:`ansible_mitogen.process.Binding` representing the + process that hosts the physical connection and services (context + establishment, file transfer, ..) for our desired target. + """ + assert self.binding is not None + return self.binding + + @property + def connected(self): + """ + Ansible connection plugin property. Used by ansible-connection command. + """ + return self.context is not None + + def _spec_from_via(self, proxied_inventory_name, via_spec): + """ + Produce a dict connection specifiction given a string `via_spec`, of + the form `[[become_method:]become_user@]inventory_hostname`. + """ + become_user, _, inventory_name = via_spec.rpartition('@') + become_method, _, become_user = become_user.rpartition(':') + + # must use __contains__ to avoid a TypeError for a missing host on + # Ansible 2.3. + via_vars = self.get_host_vars(inventory_name) + if via_vars is None: + raise ansible.errors.AnsibleConnectionFailure( + self.unknown_via_msg % ( + via_spec, + proxied_inventory_name, + ) + ) + + return ansible_mitogen.transport_config.MitogenViaSpec( + inventory_name=inventory_name, + play_context=self._play_context, + host_vars=dict(via_vars), # TODO: make it lazy + task_vars=self._get_task_vars(), # needed for interpreter discovery in parse_python_path + action=self._action, + become_method=become_method or None, + become_user=become_user or None, + ) + + unknown_via_msg = 'mitogen_via=%s of %s specifies an unknown hostname' + via_cycle_msg = 'mitogen_via=%s of %s creates a cycle (%s)' + + def _stack_from_spec(self, spec, stack=(), seen_names=()): + """ + Return a tuple of ContextService parameter dictionaries corresponding + to the connection described by `spec`, and any connection referenced by + its `mitogen_via` or `become` fields. Each element is a dict of the + form:: + + { + # Optional. If present and `True`, this hop is elegible for + # interpreter recycling. + "enable_lru": True, + # mitogen.master.Router method name. + "method": "ssh", + # mitogen.master.Router method kwargs. + "kwargs": { + "hostname": "..." + } + } + + :param ansible_mitogen.transport_config.Spec spec: + Connection specification. + :param tuple stack: + Stack elements from parent call (used for recursion). + :param tuple seen_names: + Inventory hostnames from parent call (cycle detection). + :returns: + Tuple `(stack, seen_names)`. + """ + if spec.inventory_name() in seen_names: + raise ansible.errors.AnsibleConnectionFailure( + self.via_cycle_msg % ( + spec.mitogen_via(), + spec.inventory_name(), + ' -> '.join(reversed( + seen_names + (spec.inventory_name(),) + )), + ) + ) + + if spec.mitogen_via(): + stack = self._stack_from_spec( + self._spec_from_via(spec.inventory_name(), spec.mitogen_via()), + stack=stack, + seen_names=seen_names + (spec.inventory_name(),), + ) + + stack += (CONNECTION_METHOD[spec.transport()](spec),) + if spec.become() and ((spec.become_user() != spec.remote_user()) or + C.BECOME_ALLOW_SAME_USER): + stack += (CONNECTION_METHOD[spec.become_method()](spec),) + + return ansible_mitogen.utils.unsafe.cast(stack) + + def _build_stack(self): + """ + Construct a list of dictionaries representing the connection + configuration between the controller and the target. This is + additionally used by the integration tests "mitogen_get_stack" action + to fetch the would-be connection configuration. + """ + spec = ansible_mitogen.transport_config.PlayContextSpec( + connection=self, + play_context=self._play_context, + transport=self.transport, + inventory_name=self.get_task_var('inventory_hostname'), + ) + stack = self._stack_from_spec(spec) + return spec.inventory_name(), stack + + def _connect_stack(self, stack): + """ + Pass `stack` to ContextService, requesting a copy of the context object + representing the last tuple element. If no connection exists yet, + ContextService will recursively establish it before returning it or + throwing an error. + + See :meth:`ansible_mitogen.services.ContextService.get` docstring for + description of the returned dictionary. + """ + try: + dct = mitogen.service.call( + call_context=self.binding.get_service_context(), + service_name='ansible_mitogen.services.ContextService', + method_name='get', + stack=ansible_mitogen.utils.unsafe.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: + raise ansible.errors.AnsibleModuleError(dct['msg']) + raise ansible.errors.AnsibleConnectionFailure(dct['msg']) + + self.context = dct['context'] + self.chain = CallChain(self, self.context, pipelined=True) + if self.become: + self.login_context = dct['via'] + else: + self.login_context = self.context + + self.init_child_result = dct['init_child_result'] + + def get_good_temp_dir(self): + """ + Return the 'good temporary directory' as discovered by + :func:`ansible_mitogen.target.init_child` immediately after + ContextService constructed the target context. + """ + self._connect() + return self.init_child_result['good_temp_dir'] + + def _connect(self): + """ + Establish a connection to the master process's UNIX listener socket, + constructing a mitogen.master.Router to communicate with the master, + and a mitogen.parent.Context to represent it. + + Depending on the original transport we should emulate, trigger one of + the _connect_*() service calls defined above to cause the master + process to establish the real connection on our behalf, or return a + reference to the existing one. + + Ansible connection plugin method. + """ + # In some Ansible connection plugins this method returns self. + # However nothing I've found uses it, it's not even assigned. + if self.connected: + return + + inventory_name, stack = self._build_stack() + worker_model = ansible_mitogen.process.get_worker_model() + self.binding = worker_model.get_binding( + ansible_mitogen.utils.unsafe.cast(inventory_name) + ) + self._connect_stack(stack) + + def _put_connection(self): + """ + 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. + """ + if not self.context: + return + + self.chain.reset() + mitogen.service.call( + call_context=self.binding.get_service_context(), + service_name='ansible_mitogen.services.ContextService', + method_name='put', + context=self.context + ) + + self.context = None + self.login_context = None + self.init_child_result = None + self.chain = None + + 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. + + Ansible connection plugin method. + """ + self._put_connection() + if self.binding: + self.binding.close() + self.binding = None + + def _mitogen_var_options(self, templar): + # Workaround for https://github.com/ansible/ansible/issues/84238 + var_names = C.config.get_plugin_vars('connection', self._load_name) + variables = templar.available_variables + var_options = { + var_name: templar.template(variables[var_name]) + for var_name in var_names + if var_name in variables + } + + if self.allow_extras: + extras_var_prefix = 'ansible_%s_' % self.extras_prefix + var_options['_extras'] = { + var_name: templar.template(variables[var_name]) + for var_name in variables + if var_name not in var_options + and var_name.startswith(extras_var_prefix) + } + else: + var_options['_extras'] = {} + + return var_options + + 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 + 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. + + Ansible connection plugin method. + """ + 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 + ) + + # Handle templated connection variables during `meta: reset_connection`. + # Many bugs/implementation details of Mitogen & Ansible collide here. + # See #1079, #1096, #1132, ansible/ansible#84238, ... + try: + task, templar = self._play_context.vars.pop( + '_mitogen.smuggled.reset_connection', + ) + except KeyError: + self._action_monkey_patched_by_mitogen = False + else: + # LOG.info('%r.reset(): remote_addr=%r', self, self._play_context.remote_addr) + # ansible.plugins.strategy.StrategyBase._execute_meta() doesn't + # have an action object, which we need for interpreter_discovery. + # Create a temporary action object for this purpose. + self._action = ansible_mitogen.mixins.ActionModuleMixin( + task=task, + connection=self, + play_context=self._play_context, + loader=templar._loader, + templar=templar, + shared_loader_obj=0, + ) + self._action_monkey_patched_by_mitogen = True + + # Workaround for https://github.com/ansible/ansible/issues/84238 + self.set_options( + task_keys=task.dump_attrs(), + var_options=self._mitogen_var_options(templar), + ) + + del task + del templar + + # Clear out state in case we were ever connected. + self.close() + + inventory_name, stack = self._build_stack() + if self.become: + stack = stack[:-1] + + worker_model = ansible_mitogen.process.get_worker_model() + binding = worker_model.get_binding(inventory_name) + try: + mitogen.service.call( + call_context=binding.get_service_context(), + service_name='ansible_mitogen.services.ContextService', + method_name='reset', + stack=ansible_mitogen.utils.unsafe.cast(list(stack)), + ) + finally: + binding.close() + + # Cleanup any monkey patching we did for `meta: reset_connection` + if self._action_monkey_patched_by_mitogen: + del self._action + del self._action_monkey_patched_by_mitogen + + # Compatibility with Ansible 2.4 wait_for_connection plug-in. + _reset = reset + + def get_chain(self, use_login=False, use_fork=False): + """ + Return the :class:`mitogen.parent.CallChain` to use for executing + function calls. + + :param bool use_login: + If :data:`True`, always return the chain for the login account + rather than any active become user. + :param bool use_fork: + If :data:`True`, return the chain for the fork parent. + :returns mitogen.parent.CallChain: + """ + self._connect() + if use_login: + return self.login_context.default_call_chain + # See FORK_SUPPORTED comments in target.py. + if use_fork and self.init_child_result['fork_context'] is not None: + return self.init_child_result['fork_context'].default_call_chain + return self.chain + + def spawn_isolated_child(self): + """ + Fork or launch a new child off the target context. + + :returns: + mitogen.core.Context of the new child. + """ + return self.get_chain(use_fork=True).call( + ansible_mitogen.target.spawn_isolated_child + ) + + def get_extra_args(self): + """ + Overridden by connections/mitogen_kubectl.py to a list of additional + arguments for the command. + """ + # TODO: maybe use this for SSH too. + return [] + + def get_default_cwd(self): + """ + Overridden by connections/mitogen_local.py to emulate behaviour of CWD + being fixed to that of ActionBase._loader.get_basedir(). + """ + return None + + def get_default_env(self): + """ + Overridden by connections/mitogen_local.py to emulate behaviour of + WorkProcess environment inherited from WorkerProcess. + """ + return None + + def exec_command(self, cmd, in_data='', sudoable=True, mitogen_chdir=None): + """ + Implement exec_command() by calling the corresponding + ansible_mitogen.target function in the target. + + :param str cmd: + Shell command to execute. + :param bytes in_data: + Data to supply on ``stdin`` of the process. + :returns: + (return code, stdout bytes, stderr bytes) + + Ansible connection plugin method. + """ + emulate_tty = (not in_data and sudoable) + rc, stdout, stderr = self.get_chain().call( + ansible_mitogen.target.exec_command, + cmd=ansible_mitogen.utils.unsafe.cast(cmd), + in_data=ansible_mitogen.utils.unsafe.cast(in_data), + chdir=mitogen_chdir or self.get_default_cwd(), + emulate_tty=emulate_tty, + ) + + 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 + + def fetch_file(self, in_path, out_path): + """ + Implement fetch_file() by calling the corresponding + ansible_mitogen.target function in the target. + + :param str in_path: + Remote filesystem path to read. + :param str out_path: + Local filesystem path to write. + + Ansible connection plugin method. + """ + self._connect() + ansible_mitogen.target.transfer_file( + context=self.context, + # in_path may be AnsibleUnicode + in_path=ansible_mitogen.utils.unsafe.cast(in_path), + out_path=out_path + ) + + def put_data(self, out_path, data, mode=None, utimes=None): + """ + Implement put_file() by caling the corresponding ansible_mitogen.target + function in the target, transferring small files inline. This is + pipelined and will return immediately; failed transfers are reported as + exceptions in subsequent functon calls. + + :param str out_path: + Remote filesystem path to write. + :param byte data: + File contents to put. + """ + self.get_chain().call_no_reply( + ansible_mitogen.target.write_path, + ansible_mitogen.utils.unsafe.cast(out_path), + mitogen.core.Blob(data), + mode=mode, + utimes=utimes, + ) + + #: Maximum size of a small file before switching to streaming + #: transfer. This should really be the same as + #: mitogen.services.FileService.IO_SIZE, however the message format has + #: slightly more overhead, so just randomly subtract 4KiB. + SMALL_FILE_LIMIT = mitogen.core.CHUNK_SIZE - 4096 + + def _throw_io_error(self, e, path): + if e.args[0] == errno.ENOENT: + s = 'file or module does not exist: ' + path + raise ansible.errors.AnsibleFileNotFound(s) + + def put_file(self, in_path, out_path): + """ + Implement put_file() by streamily transferring the file via + FileService. + + :param str in_path: + Local filesystem path to read. + :param str out_path: + Remote filesystem path to write. + + Ansible connection plugin method. + """ + try: + st = os.stat(in_path) + except OSError as e: + self._throw_io_error(e, in_path) + raise + + if not stat.S_ISREG(st.st_mode): + raise IOError('%r is not a regular file.' % (in_path,)) + + # If the file is sufficiently small, just ship it in the argument list + # rather than introducing an extra RTT for the child to request it from + # FileService. + if st.st_size <= self.SMALL_FILE_LIMIT: + try: + fp = open(in_path, 'rb') + try: + s = fp.read(self.SMALL_FILE_LIMIT + 1) + finally: + fp.close() + except OSError as e: + self._throw_io_error(e, in_path) + raise + + # Ensure did not grow during read. + if len(s) == st.st_size: + return self.put_data(out_path, s, mode=st.st_mode, + utimes=(st.st_atime, st.st_mtime)) + + self._connect() + mitogen.service.call( + call_context=self.binding.get_service_context(), + service_name='mitogen.service.FileService', + method_name='register', + path=ansible_mitogen.utils.unsafe.cast(in_path) + ) + + # For now this must remain synchronous, as the action plug-in may have + # passed us a temporary file to transfer. A future FileService could + # maintain an LRU list of open file descriptors to keep the temporary + # file alive, but that requires more work. + self.get_chain().call( + ansible_mitogen.target.transfer_file, + context=self.binding.get_child_service_context(), + in_path=ansible_mitogen.utils.unsafe.cast(in_path), + out_path=ansible_mitogen.utils.unsafe.cast(out_path) + ) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/loaders.py b/plugins/mitogen-0.3.44/ansible_mitogen/loaders.py new file mode 100644 index 0000000..f753b6b --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/loaders.py @@ -0,0 +1,86 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Stable names for PluginLoader instances across Ansible versions. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import ansible.errors + +import ansible_mitogen.utils + +__all__ = [ + 'action_loader', + 'become_loader', + 'connection_loader', + 'module_loader', + 'module_utils_loader', + 'shell_loader', + 'strategy_loader', +] + + +ANSIBLE_VERSION_MIN = (2, 10) +OLD_VERSION_MSG = ( + "Your version of Ansible (%s) is too old. The oldest version supported by " + "Mitogen for Ansible is %s." +) + + +def assert_supported_release(): + """ + Throw AnsibleError with a descriptive message in case of being loaded into + an unsupported Ansible release. + """ + v = ansible_mitogen.utils.ansible_version + if v[:2] < ANSIBLE_VERSION_MIN: + raise ansible.errors.AnsibleError( + OLD_VERSION_MSG % (v, ANSIBLE_VERSION_MIN) + ) + + +# this is the first file our strategy plugins import, so we need to check this here +# in prior Ansible versions, connection_loader.get_with_context didn't exist, so if a user +# is trying to load an old Ansible version, we'll fail and error gracefully +assert_supported_release() + + +from ansible.plugins.loader import action_loader +from ansible.plugins.loader import become_loader +from ansible.plugins.loader import connection_loader +from ansible.plugins.loader import module_loader +from ansible.plugins.loader import module_utils_loader +from ansible.plugins.loader import shell_loader +from ansible.plugins.loader import strategy_loader + +# These are original, unwrapped implementations +action_loader__get_with_context = action_loader.get_with_context +connection_loader__get_with_context = connection_loader.get_with_context diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/logging.py b/plugins/mitogen-0.3.44/ansible_mitogen/logging.py new file mode 100644 index 0000000..70a8f15 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/logging.py @@ -0,0 +1,127 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import logging +import os + +import ansible.utils.display + +import mitogen.utils + + +display = ansible.utils.display.Display() + +#: The process name set via :func:`set_process_name`. +_process_name = None + +#: The PID of the process that last called :func:`set_process_name`, so its +#: value can be ignored in unknown fork children. +_process_pid = None + + +def set_process_name(name): + """ + Set a name to adorn log messages with. + """ + global _process_name + _process_name = name + + global _process_pid + _process_pid = os.getpid() + + +class Handler(logging.Handler): + """ + Use Mitogen's log format, but send the result to a Display method. + """ + def __init__(self, normal_method): + logging.Handler.__init__(self) + self.formatter = mitogen.utils.log_get_formatter() + self.normal_method = normal_method + + #: Set of target loggers that produce warnings and errors that spam the + #: console needlessly. Their log level is forced to INFO. A better strategy + #: may simply be to bury all target logs in DEBUG output, but not by + #: overriding their log level as done here. + NOISY_LOGGERS = frozenset([ + 'dnf', # issue #272; warns when a package is already installed. + 'boto', # issue #541; normal boto retry logic can cause ERROR logs. + ]) + + def emit(self, record): + mitogen_name = getattr(record, 'mitogen_name', '') + if mitogen_name == 'stderr': + record.levelno = logging.ERROR + if mitogen_name in self.NOISY_LOGGERS and record.levelno >= logging.WARNING: + record.levelno = logging.DEBUG + + if _process_pid == os.getpid(): + process_name = _process_name + else: + process_name = '?' + + s = '[%-4s %d] %s' % (process_name, os.getpid(), self.format(record)) + if record.levelno >= logging.ERROR: + display.error(s, wrap_text=False) + elif record.levelno >= logging.WARNING: + display.warning(s, formatted=True) + else: + self.normal_method(s) + + +def setup(): + """ + Install handlers for Mitogen loggers to redirect them into the Ansible + display framework. Ansible installs its own logging framework handlers when + C.DEFAULT_LOG_PATH is set, therefore disable propagation for our handlers. + """ + l_mitogen = logging.getLogger('mitogen') + l_mitogen_io = logging.getLogger('mitogen.io') + l_ansible_mitogen = logging.getLogger('ansible_mitogen') + l_operon = logging.getLogger('operon') + + for logger in l_mitogen, l_mitogen_io, l_ansible_mitogen, l_operon: + logger.handlers = [Handler(display.vvv)] + logger.propagate = False + + if display.verbosity > 2: + l_ansible_mitogen.setLevel(logging.DEBUG) + l_mitogen.setLevel(logging.DEBUG) + else: + # Mitogen copies the active log level into new children, allowing them + # to filter tiny messages before they hit the network, and therefore + # before they wake the IO loop. Explicitly setting INFO saves ~4% + # running against just the local machine. + l_mitogen.setLevel(logging.ERROR) + l_ansible_mitogen.setLevel(logging.ERROR) + + if display.verbosity > 3: + l_mitogen_io.setLevel(logging.DEBUG) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/mixins.py b/plugins/mitogen-0.3.44/ansible_mitogen/mixins.py new file mode 100644 index 0000000..7065c69 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/mixins.py @@ -0,0 +1,504 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import json +import logging +import os +import pwd +import random +import traceback + +import ansible +import ansible.plugins.action +import ansible.utils.unsafe_proxy +import ansible.vars.clean + +from ansible.module_utils.common.text.converters import to_bytes, to_text +from ansible.module_utils.six.moves import shlex_quote + +import mitogen.core +import mitogen.select + +import ansible_mitogen.connection +import ansible_mitogen.planner +import ansible_mitogen.target +import ansible_mitogen.utils +import ansible_mitogen.utils.unsafe + + +LOG = logging.getLogger(__name__) + + +class ActionModuleMixin(ansible.plugins.action.ActionBase): + """ + The Mitogen-patched PluginLoader dynamically mixes this into every action + class that Ansible attempts to load. It exists to override all the + assumptions built into the base action class that should really belong in + some middle layer, or at least in the connection layer. + + Functionality is defined here for: + + * Capturing the final set of task variables and giving Connection a chance + to update its idea of the correct execution environment, before any + attempt is made to call a Connection method. While it's not expected for + the interpreter to change on a per-task basis, Ansible permits this, and + so it must be supported. + + * Overriding lots of methods that try to call out to shell for mundane + reasons, such as copying files around, changing file permissions, + creating temporary directories and suchlike. + + * Short-circuiting any use of Ansiballz or related code for executing a + module remotely using shell commands and SSH. + + * Short-circuiting most of the logic in dealing with the fact that Ansible + always runs become: tasks across at least the SSH user account and the + destination user account, and handling the security permission issues + that crop up due to this. Mitogen always runs a task completely within + the target user account, so it's not a problem for us. + """ + def __init__(self, task, connection, *args, **kwargs): + """ + Verify the received connection is really a Mitogen connection. If not, + transmute this instance back into the original unadorned base class. + + This allows running the Mitogen strategy in mixed-target playbooks, + where some targets use SSH while others use WinRM or some fancier UNIX + connection plug-in. That's because when the Mitogen strategy is active, + ActionModuleMixin is unconditionally mixed into any action module that + is instantiated, and there is no direct way for the monkey-patch to + know what kind of connection will be used upfront. + """ + super(ActionModuleMixin, self).__init__(task, connection, *args, **kwargs) + if not isinstance(connection, ansible_mitogen.connection.Connection): + _, self.__class__ = type(self).__bases__ + + # required for python interpreter discovery + connection.templar = self._templar + + self._mitogen_discovering_interpreter = False + self._mitogen_interpreter_candidate = None + self._mitogen_rediscovered_interpreter = False + + def run(self, tmp=None, task_vars=None): + """ + Override run() to notify Connection of task-specific data, so it has a + chance to know e.g. the Python interpreter in use. + """ + self._connection.on_action_run( + task_vars=task_vars, + delegate_to_hostname=self._task.delegate_to, + loader_basedir=self._loader.get_basedir(), + ) + return super(ActionModuleMixin, self).run(tmp, task_vars) + + COMMAND_RESULT = { + 'rc': 0, + 'stdout': '', + 'stdout_lines': [], + 'stderr': '' + } + + def fake_shell(self, func, stdout=False): + """ + Execute a function and decorate its return value in the style of + _low_level_execute_command(). This produces a return value that looks + like some shell command was run, when really func() was implemented + entirely in Python. + + If the function raises :py:class:`mitogen.core.CallError`, this will be + translated into a failed shell command with a non-zero exit status. + + :param func: + Function invoked as `func()`. + :returns: + See :py:attr:`COMMAND_RESULT`. + """ + dct = self.COMMAND_RESULT.copy() + try: + rc = func() + if stdout: + dct['stdout'] = repr(rc) + except mitogen.core.CallError: + LOG.exception('While emulating a shell command') + dct['rc'] = 1 + dct['stderr'] = traceback.format_exc() + + return dct + + def _remote_file_exists(self, path): + """ + Determine if `path` exists by directly invoking os.path.exists() in the + target user account. + """ + LOG.debug('_remote_file_exists(%r)', path) + return self._connection.get_chain().call( + ansible_mitogen.target.file_exists, + ansible_mitogen.utils.unsafe.cast(path) + ) + + def _configure_module(self, module_name, module_args, task_vars=None): + """ + Mitogen does not use the Ansiballz framework. This call should never + happen when ActionMixin is active, so crash if it does. + """ + assert False, "_configure_module() should never be called." + + def _is_pipelining_enabled(self, module_style, wrap_async=False): + """ + Mitogen does not use SSH pipelining. This call should never happen when + ActionMixin is active, so crash if it does. + """ + assert False, "_is_pipelining_enabled() should never be called." + + def _generate_tmp_path(self): + return os.path.join( + self._connection.get_good_temp_dir(), + 'ansible_mitogen_action_%016x' % ( + random.getrandbits(8*8), + ) + ) + + def _make_tmp_path(self, remote_user=None): + """ + Create a temporary subdirectory as a child of the temporary directory + managed by the remote interpreter. + """ + LOG.debug('_make_tmp_path(remote_user=%r)', remote_user) + path = self._generate_tmp_path() + LOG.debug('Temporary directory: %r', path) + self._connection.get_chain().call_no_reply(os.mkdir, path) + self._connection._shell.tmpdir = path + return path + + def _remove_tmp_path(self, tmp_path): + """ + Replace the base implementation's invocation of rm -rf, replacing it + with a pipelined call to :func:`ansible_mitogen.target.prune_tree`. + """ + LOG.debug('_remove_tmp_path(%r)', tmp_path) + if tmp_path is None and ansible_mitogen.utils.ansible_version[:2] >= (2, 6): + tmp_path = self._connection._shell.tmpdir # 06f73ad578d + if tmp_path is not None: + self._connection.get_chain().call_no_reply( + ansible_mitogen.target.prune_tree, + tmp_path, + ) + self._connection._shell.tmpdir = None + + def _transfer_data(self, remote_path, data): + """ + Used by the base _execute_module(), and in <2.4 also by the template + action module, and probably others. + """ + if data is None and ansible_mitogen.utils.ansible_version[:2] <= (2, 18): + data = '{}' + if isinstance(data, dict): + try: + data = json.dumps(data, ensure_ascii=False) + except UnicodeDecodeError: + data = json.dumps(data) + if not isinstance(data, bytes): + data = to_bytes(data, errors='surrogate_or_strict') + + LOG.debug('_transfer_data(%r, %s ..%d bytes)', + remote_path, type(data), len(data)) + self._connection.put_data(remote_path, data) + return remote_path + + #: Actions listed here cause :func:`_fixup_perms2` to avoid a needless + #: roundtrip, as they modify file modes separately afterwards. This is due + #: to the method prototype having a default of `execute=True`. + FIXUP_PERMS_RED_HERRING = set(['copy']) + + def _fixup_perms2(self, remote_paths, remote_user=None, execute=True): + """ + Mitogen always executes ActionBase helper methods in the context of the + target user account, so it is never necessary to modify permissions + except to ensure the execute bit is set if requested. + """ + LOG.debug('_fixup_perms2(%r, remote_user=%r, execute=%r)', + remote_paths, remote_user, execute) + if execute and self._task.action not in self.FIXUP_PERMS_RED_HERRING: + return self._remote_chmod(remote_paths, mode='u+x') + return self.COMMAND_RESULT.copy() + + def _remote_chmod(self, paths, mode, sudoable=False): + """ + Issue an asynchronous set_file_mode() call for every path in `paths`, + then format the resulting return value list with fake_shell(). + """ + LOG.debug('_remote_chmod(%r, mode=%r, sudoable=%r)', + paths, mode, sudoable) + return self.fake_shell(lambda: mitogen.select.Select.all( + self._connection.get_chain().call_async( + ansible_mitogen.target.set_file_mode, + ansible_mitogen.utils.unsafe.cast(path), + mode, + ) + for path in paths + )) + + def _remote_chown(self, paths, user, sudoable=False): + """ + Issue an asynchronous os.chown() call for every path in `paths`, then + format the resulting return value list with fake_shell(). + """ + LOG.debug('_remote_chown(%r, user=%r, sudoable=%r)', + paths, user, sudoable) + ent = self._connection.get_chain().call(pwd.getpwnam, user) + return self.fake_shell(lambda: mitogen.select.Select.all( + self._connection.get_chain().call_async( + os.chown, path, ent.pw_uid, ent.pw_gid + ) + for path in paths + )) + + def _remote_expand_user(self, path, sudoable=True): + """ + Replace the base implementation's attempt to emulate + os.path.expanduser() with an actual call to os.path.expanduser(). + + :param bool sudoable: + If :data:`True`, indicate unqualified tilde ("~" with no username) + should be evaluated in the context of the login account, not any + become_user. + """ + LOG.debug('_remote_expand_user(%r, sudoable=%r)', path, sudoable) + if not path.startswith('~'): + # /home/foo -> /home/foo + return path + if sudoable or not self._connection.become: + if path == '~': + # ~ -> /home/dmw + return self._connection.homedir + if path.startswith('~/'): + # ~/.ansible -> /home/dmw/.ansible + return os.path.join(self._connection.homedir, path[2:]) + # ~root/.ansible -> /root/.ansible + return self._connection.get_chain(use_login=(not sudoable)).call( + os.path.expanduser, + ansible_mitogen.utils.unsafe.cast(path), + ) + + def get_task_timeout_secs(self): + """ + Return the task "async:" value, portable across 2.4-2.5. + """ + try: + return self._task.async_val + except AttributeError: + return getattr(self._task, 'async') + + def _set_temp_file_args(self, module_args, wrap_async): + # Ansible>2.5 module_utils reuses the action's temporary directory if + # one exists. Older versions error if this key is present. + if ansible_mitogen.utils.ansible_version[:2] >= (2, 5): + if wrap_async: + # Sharing is not possible with async tasks, as in that case, + # the directory must outlive the action plug-in. + module_args['_ansible_tmpdir'] = None + else: + module_args['_ansible_tmpdir'] = self._connection._shell.tmpdir + + # If _ansible_tmpdir is unset, Ansible>2.6 module_utils will use + # _ansible_remote_tmp as the location to create the module's temporary + # directory. Older versions error if this key is present. + if ansible_mitogen.utils.ansible_version[:2] >= (2, 6): + module_args['_ansible_remote_tmp'] = ( + self._connection.get_good_temp_dir() + ) + + def _execute_module(self, module_name=None, module_args=None, tmp=None, + task_vars=None, persist_files=False, + delete_remote_tmp=True, wrap_async=False, + ignore_unknown_opts=False, + ): + """ + Collect up a module's execution environment then use it to invoke + target.run_module() or helpers.run_module_async() in the target + context. + """ + if module_name is None: + module_name = self._task.action + if module_args is None: + module_args = self._task.args + if task_vars is None: + task_vars = {} + + if ansible_mitogen.utils.ansible_version[:2] >= (2, 17): + self._update_module_args( + module_name, module_args, task_vars, + ignore_unknown_opts=ignore_unknown_opts, + ) + else: + self._update_module_args(module_name, module_args, task_vars) + env = {} + self._compute_environment_string(env) + self._set_temp_file_args(module_args, wrap_async) + + # there's a case where if a task shuts down the node and then immediately calls + # wait_for_connection, the `ping` test from Ansible won't pass because we lost connection + # clearing out context forces a reconnect + # see https://github.com/dw/mitogen/issues/655 and Ansible's `wait_for_connection` module for more info + if module_name == 'ansible.legacy.ping' and type(self).__name__ == 'wait_for_connection': + self._connection.context = None + + self._connection._connect() + result = ansible_mitogen.planner.invoke( + ansible_mitogen.planner.Invocation( + action=self, + connection=self._connection, + module_name=ansible_mitogen.utils.unsafe.cast(mitogen.core.to_text(module_name)), + module_args=ansible_mitogen.utils.unsafe.cast(module_args), + task_vars=task_vars, + templar=self._templar, + env=ansible_mitogen.utils.unsafe.cast(env), + wrap_async=wrap_async, + timeout_secs=self.get_task_timeout_secs(), + ) + ) + + if tmp and delete_remote_tmp and ansible_mitogen.utils.ansible_version[:2] < (2, 5): + # Built-in actions expected tmpdir to be cleaned up automatically + # on _execute_module(). + self._remove_tmp_path(tmp) + + # prevents things like discovered_interpreter_* or ansible_discovered_interpreter_* from being set + ansible.vars.clean.remove_internal_keys(result) + + # taken from _execute_module of ansible 2.8.6 + # propagate interpreter discovery results back to the controller + if self._discovered_interpreter_key: + if result.get('ansible_facts') is None: + result['ansible_facts'] = {} + + # only cache discovered_interpreter if we're not running a rediscovery + # rediscovery happens in places like docker connections that could have different + # python interpreters than the main host + if not self._mitogen_rediscovered_interpreter: + result['ansible_facts'][self._discovered_interpreter_key] = self._discovered_interpreter + + discovery_warnings = getattr(self, '_discovery_warnings', []) + if discovery_warnings: + if result.get('warnings') is None: + result['warnings'] = [] + result['warnings'].extend(discovery_warnings) + + discovery_deprecation_warnings = getattr(self, '_discovery_deprecation_warnings', []) + if discovery_deprecation_warnings: + if result.get('deprecations') is None: + result['deprecations'] = [] + result['deprecations'].extend(discovery_deprecation_warnings) + + return ansible.utils.unsafe_proxy.wrap_var(result) + + def _postprocess_response(self, result): + """ + Apply fixups mimicking ActionBase._execute_module(); this is copied + verbatim from action/__init__.py, the guts of _parse_returned_data are + garbage and should be removed or reimplemented once tests exist. + + :param dict result: + Dictionary with format:: + + { + "rc": int, + "stdout": "stdout data", + "stderr": "stderr data" + } + """ + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + data = self._parse_returned_data(result, profile='legacy') + else: + data = self._parse_returned_data(result) + + # Cutpasted from the base implementation. + if 'stdout' in data and 'stdout_lines' not in data: + data['stdout_lines'] = (data['stdout'] or u'').splitlines() + if 'stderr' in data and 'stderr_lines' not in data: + data['stderr_lines'] = (data['stderr'] or u'').splitlines() + + return data + + def _low_level_execute_command(self, cmd, sudoable=True, in_data=None, + executable=None, + encoding_errors='surrogate_then_replace', + chdir=None): + """ + Override the base implementation by simply calling + target.exec_command() in the target context. + """ + LOG.debug('_low_level_execute_command(%r, in_data=%r, exe=%r, dir=%r)', + cmd, type(in_data), executable, chdir) + + if executable is None: # executable defaults to False + executable = self._play_context.executable + if executable: + cmd = executable + ' -c ' + shlex_quote(cmd) + + # TODO: HACK: if finding python interpreter then we need to keep + # calling exec_command until we run into the right python we'll use + # chicken-and-egg issue, mitogen needs a python to run low_level_execute_command + # which is required by Ansible's discover_interpreter function + if self._mitogen_discovering_interpreter: + possible_pythons = self._mitogen_interpreter_candidates + else: + # not used, just adding a filler value + possible_pythons = ['python'] + + for possible_python in possible_pythons: + try: + self._mitogen_interpreter_candidate = possible_python + rc, stdout, stderr = self._connection.exec_command( + cmd, in_data, sudoable, mitogen_chdir=chdir, + ) + except BaseException as exc: + # we've reached the last python attempted and failed + if possible_python == possible_pythons[-1]: + raise + else: + LOG.debug( + '%r._low_level_execute_command: candidate=%r ignored: %s, %r', + self, possible_python, type(exc), exc, + ) + continue + + stdout_text = to_text(stdout, errors=encoding_errors) + stderr_text = to_text(stderr, errors=encoding_errors) + + return { + 'rc': rc, + 'stdout': stdout_text, + 'stdout_lines': stdout_text.splitlines(), + 'stderr': stderr_text, + 'stderr_lines': stderr_text.splitlines(), + } diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/module_finder.py b/plugins/mitogen-0.3.44/ansible_mitogen/module_finder.py new file mode 100644 index 0000000..8eb163b --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/module_finder.py @@ -0,0 +1,279 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +from __future__ import unicode_literals +__metaclass__ = type + +import collections +import logging +import os +import re +import sys + +if sys.version_info >= (3, 4): + import importlib.machinery + import importlib.util +else: + import imp + +import mitogen.imports + + +LOG = logging.getLogger(__name__) +PREFIX = 'ansible.module_utils.' + + +# Analog of `importlib.machinery.ModuleSpec` or `pkgutil.ModuleInfo`. +# name Unqualified name of the module. +# path Filesystem path of the module. +# kind One of the constants in `imp`, as returned in `imp.find_module()` +# parent `ansible_mitogen.module_finder.Module` of parent package (if any). +Module = collections.namedtuple('Module', 'name path kind parent') + + +def get_fullname(module): + """ + Reconstruct a Module's canonical path by recursing through its parents. + """ + bits = [str(module.name)] + while module.parent: + bits.append(str(module.parent.name)) + module = module.parent + return '.'.join(reversed(bits)) + + +def get_code(module): + """ + Compile and return a Module's code object. + """ + fp = open(module.path, 'rb') + try: + return compile(fp.read(), str(module.name), 'exec') + finally: + fp.close() + + +def is_pkg(module): + """ + Return :data:`True` if a Module represents a package. + """ + return module.kind == imp.PKG_DIRECTORY + + +def find(name, path=(), parent=None): + """ + Return a Module instance describing the first matching module found on the + search path. + + :param str name: + Module name. + :param list path: + List of directory names to search for the module. + :param Module parent: + Optional module parent. + """ + assert isinstance(path, tuple) + head, _, tail = name.partition('.') + try: + tup = imp.find_module(head, list(path)) + except ImportError: + return parent + + fp, modpath, (suffix, mode, kind) = tup + if fp: + fp.close() + + if parent and modpath == parent.path: + # 'from timeout import timeout', where 'timeout' is a function but also + # the name of the module being imported. + return None + + if kind == imp.PKG_DIRECTORY: + modpath = os.path.join(modpath, '__init__.py') + + module = Module(head, modpath, kind, parent) + # TODO: this code is entirely wrong on Python 3.x, but works well enough + # for Ansible. We need a new find_child() that only looks in the package + # directory, never falling back to the parent search path. + if tail and kind == imp.PKG_DIRECTORY: + return find_relative(module, tail, path) + return module + + +def find_relative(parent, name, path=()): + if parent.kind == imp.PKG_DIRECTORY: + path = (os.path.dirname(parent.path),) + path + return find(name, path, parent=parent) + + +def scan_fromlist(code): + """Return an iterator of (level, name) for explicit imports in a code + object. + + Not all names identify a module. `from os import name, path` generates + `(0, 'os.name'), (0, 'os.path')`, but `os.name` is usually a string. + + >>> src = 'import a; import b.c; from d.e import f; from g import h, i\\n' + >>> code = compile(src, '', 'exec') + >>> list(scan_fromlist(code)) + [(0, 'a'), (0, 'b.c'), (0, 'd.e.f'), (0, 'g.h'), (0, 'g.i')] + """ + for level, modname_s, fromlist in mitogen.imports.codeobj_imports(code): + for name in fromlist: + yield level, str('%s.%s' % (modname_s, name)) + if not fromlist: + yield level, modname_s + + +def walk_imports(code, prefix=None): + """Return an iterator of names for implicit parent imports & explicit + imports in a code object. + + If a prefix is provided, then only children of that prefix are included. + Not all names identify a module. `from os import name, path` generates + `'os', 'os.name', 'os.path'`, but `os.name` is usually a string. + + >>> source = 'import a; import b; import b.c; from b.d import e, f\\n' + >>> code = compile(source, '', 'exec') + >>> list(walk_imports(code)) + ['a', 'b', 'b', 'b.c', 'b', 'b.d', 'b.d.e', 'b.d.f'] + >>> list(walk_imports(code, prefix='b')) + ['b.c', 'b.d', 'b.d.e', 'b.d.f'] + """ + if prefix is None: + prefix = '' + pattern = re.compile(r'(^|\.)(\w+)') + start = len(prefix) + for _, name, fromlist in mitogen.imports.codeobj_imports(code): + if not name.startswith(prefix): + continue + for match in pattern.finditer(name, start): + yield name[:match.end()] + for leaf in fromlist: + yield str('%s.%s' % (name, leaf)) + + +def scan(module_name, module_path, search_path): + # type: (str, str, list[str]) -> list[(str, str, bool)] + """Return a list of (name, path, is_package) for ansible.module_utils + imports used by an Ansible module. + """ + log = LOG.getChild('scan') + log.debug('%r, %r, %r', module_name, module_path, search_path) + + if sys.version_info >= (3, 4): + result = _scan_importlib_find_spec( + module_name, module_path, search_path, + ) + log.debug('_scan_importlib_find_spec %r', result) + else: + result = _scan_imp_find_module(module_name, module_path, search_path) + log.debug('_scan_imp_find_module %r', result) + return result + + +def _scan_importlib_find_spec(module_name, module_path, search_path): + # type: (str, str, list[str]) -> list[(str, str, bool)] + module = importlib.machinery.ModuleSpec( + module_name, loader=None, origin=module_path, + ) + prefix = importlib.machinery.ModuleSpec( + PREFIX.rstrip('.'), loader=None, + ) + prefix.submodule_search_locations = search_path + queue = collections.deque([module]) + specs = {prefix.name: prefix} + while queue: + spec = queue.popleft() + if spec.origin is None: + continue + try: + with open(spec.origin, 'rb') as f: + code = compile(f.read(), spec.name, 'exec') + except Exception as exc: + raise ValueError((exc, module, spec, specs)) + + for name in walk_imports(code, prefix.name): + if name in specs: + continue + + parent_name = name.rpartition('.')[0] + parent = specs[parent_name] + if parent is None or not parent.submodule_search_locations: + specs[name] = None + continue + + child = importlib.util._find_spec( + name, parent.submodule_search_locations, + ) + if child is None or child.origin is None: + specs[name] = None + continue + + specs[name] = child + queue.append(child) + + del specs[prefix.name] + return sorted( + (spec.name, spec.origin, spec.submodule_search_locations is not None) + for spec in specs.values() if spec is not None + ) + + +def _scan_imp_find_module(module_name, module_path, search_path): + # type: (str, str, list[str]) -> list[(str, str, bool)] + module = Module(module_name, module_path, imp.PY_SOURCE, None) + stack = [module] + seen = set() + + while stack: + module = stack.pop(0) + for level, fromname in scan_fromlist(get_code(module)): + if not fromname.startswith(PREFIX): + continue + + imported = find(fromname[len(PREFIX):], search_path) + if imported is None or imported in seen: + continue + + seen.add(imported) + stack.append(imported) + parent = imported.parent + while parent: + fullname = get_fullname(parent) + module = Module(fullname, parent.path, parent.kind, None) + if module not in seen: + seen.add(module) + stack.append(module) + parent = parent.parent + + return sorted( + (PREFIX + get_fullname(module), module.path, is_pkg(module)) + for module in seen + ) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/parsing.py b/plugins/mitogen-0.3.44/ansible_mitogen/parsing.py new file mode 100644 index 0000000..2ca5031 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/parsing.py @@ -0,0 +1,77 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +from __future__ import unicode_literals +__metaclass__ = type + +import mitogen.core + + +def parse_script_interpreter(source): + """ + Parse the script interpreter portion of a UNIX hashbang using the rules + Linux uses. + + :param str source: String like "/usr/bin/env python". + + :returns: + Tuple of `(interpreter, arg)`, where `intepreter` is the script + interpreter and `arg` is its sole argument if present, otherwise + :py:data:`None`. + """ + # Find terminating newline. Assume last byte of binprm_buf if absent. + nl = source.find(b'\n', 0, 128) + if nl == -1: + nl = min(128, len(source)) + + # Split once on the first run of whitespace. If no whitespace exists, + # bits just contains the interpreter filename. + bits = source[0:nl].strip().split(None, 1) + if len(bits) == 1: + return mitogen.core.to_text(bits[0]), None + return mitogen.core.to_text(bits[0]), mitogen.core.to_text(bits[1]) + + +def parse_hashbang(source): + """ + Parse a UNIX "hashbang line" using the syntax supported by Linux. + + :param str source: String like "#!/usr/bin/env python". + + :returns: + Tuple of `(interpreter, arg)`, where `intepreter` is the script + interpreter and `arg` is its sole argument if present, otherwise + :py:data:`None`. + """ + # Linux requires first 2 bytes with no whitespace, pretty sure it's the + # same everywhere. See binfmt_script.c. + if not source.startswith(b'#!'): + return None, None + + return parse_script_interpreter(source[2:]) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/planner.py b/plugins/mitogen-0.3.44/ansible_mitogen/planner.py new file mode 100644 index 0000000..5d11de3 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/planner.py @@ -0,0 +1,711 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Classes to detect each case from [0] and prepare arguments necessary for the +corresponding Runner class within the target, including preloading requisite +files/modules known missing. + +[0] "Ansible Module Architecture", developing_program_flow_modules.html +""" + +from __future__ import absolute_import, division, print_function +from __future__ import unicode_literals +__metaclass__ = type + +import json +import logging +import os +import random +import re + +import ansible.collections.list +import ansible.errors +import ansible.executor.module_common + +import mitogen.core +import mitogen.select +import mitogen.service + +import ansible_mitogen.loaders +import ansible_mitogen.parsing +import ansible_mitogen.target +import ansible_mitogen.utils.unsafe + + +LOG = logging.getLogger(__name__) +NO_METHOD_MSG = 'Mitogen: no invocation method found for: ' +NO_INTERPRETER_MSG = 'module (%s) is missing interpreter line' +# NOTE: Ansible 2.10 no longer has a `.` at the end of NO_MODULE_MSG error +NO_MODULE_MSG = 'The module %s was not found in configured module paths' + +_planner_by_path = {} + + +class Invocation(object): + """ + Collect up a module's execution environment then use it to invoke + target.run_module() or helpers.run_module_async() in the target context. + """ + def __init__(self, action, connection, module_name, module_args, + task_vars, templar, env, wrap_async, timeout_secs): + #: ActionBase instance invoking the module. Required to access some + #: output postprocessing methods that don't belong in ActionBase at + #: all. + self.action = action + #: Ansible connection to use to contact the target. Must be an + #: ansible_mitogen connection. + self.connection = connection + #: Name of the module ('command', 'shell', etc.) to execute. + self.module_name = module_name + #: Final module arguments. + self.module_args = module_args + #: Task variables, needed to extract ansible_*_interpreter. + self.task_vars = task_vars + #: Templar, needed to extract ansible_*_interpreter. + self.templar = templar + #: Final module environment. + self.env = env + #: Boolean, if :py:data:`True`, launch the module asynchronously. + self.wrap_async = wrap_async + #: Integer, if >0, limit the time an asynchronous job may run for. + self.timeout_secs = timeout_secs + #: Initially ``None``, but set by :func:`invoke`. The path on the + #: master to the module's implementation file. + self.module_path = None + #: Initially ``None``, but set by :func:`invoke`. The raw source or + #: binary contents of the module. + self._module_source = None + #: Initially ``{}``, but set by :func:`invoke`. Optional source to send + #: to :func:`propagate_paths_and_modules` to fix Python3.5 relative import errors + self._overridden_sources = {} + #: Initially ``set()``, but set by :func:`invoke`. Optional source paths to send + #: to :func:`propagate_paths_and_modules` to handle loading source dependencies from + #: places outside of the main source path, such as collections + self._extra_sys_paths = set() + + def get_module_source(self): + if self._module_source is None: + self._module_source = read_file(self.module_path) + return self._module_source + + def __repr__(self): + return 'Invocation(module_name=%s)' % (self.module_name,) + + +class Planner(object): + """ + A Planner receives a module name and the contents of its implementation + file, indicates whether or not it understands how to run the module, and + exports a method to run the module. + """ + def __init__(self, invocation): + self._inv = invocation + + @classmethod + def detect(cls, path, source): + """ + Return true if the supplied `invocation` matches the module type + implemented by this planner. + """ + raise NotImplementedError() + + def should_fork(self): + """ + Asynchronous tasks must always be forked. + """ + return self._inv.wrap_async + + def get_push_files(self): + """ + Return a list of files that should be propagated to the target context + using PushFileService. The default implementation pushes nothing. + """ + return [] + + def get_module_deps(self): + """ + Return a list of the Python module names imported by the module. + """ + return [] + + def get_kwargs(self, **kwargs): + """ + If :meth:`detect` returned :data:`True`, plan for the module's + execution, including granting access to or delivering any files to it + that are known to be absent, and finally return a dict:: + + { + # Name of the class from runners.py that implements the + # target-side execution of this module type. + "runner_name": "...", + + # Remaining keys are passed to the constructor of the class + # named by `runner_name`. + } + """ + binding = self._inv.connection.get_binding() + + kwargs = ansible_mitogen.utils.unsafe.cast(kwargs) + new = dict((mitogen.core.UnicodeType(k), kwargs[k]) + for k in kwargs) + new.setdefault('good_temp_dir', + self._inv.connection.get_good_temp_dir()) + new.setdefault('cwd', self._inv.connection.get_default_cwd()) + new.setdefault('extra_env', self._inv.connection.get_default_env()) + new.setdefault('emulate_tty', True) + new.setdefault('service_context', binding.get_child_service_context()) + return new + + def __repr__(self): + return '%s()' % (type(self).__name__,) + + +class BinaryPlanner(Planner): + """ + Binary modules take their arguments and will return data to Ansible in the + same way as want JSON modules. + """ + runner_name = 'BinaryRunner' + + @classmethod + def detect(cls, path, source): + return ansible.executor.module_common._is_binary(source) + + def get_push_files(self): + return [mitogen.core.to_text(self._inv.module_path)] + + def get_kwargs(self, **kwargs): + return super(BinaryPlanner, self).get_kwargs( + runner_name=self.runner_name, + module=self._inv.module_name, + path=self._inv.module_path, + json_args=json.dumps(self._inv.module_args), + env=ansible_mitogen.utils.unsafe.cast(self._inv.env), + **kwargs + ) + + +class ScriptPlanner(BinaryPlanner): + """ + Common functionality for script module planners -- handle interpreter + detection and rewrite. + """ + def _rewrite_interpreter(self, path): + """ + Given the interpreter path (from the script's hashbang line), return + the desired interpreter path. This tries, in order + + 1. Look up & render the `ansible_*_interpreter` variable, if set + 2. Look up the `discovered_interpreter_*` fact, if present + 3. The unmodified path from the hashbang line. + + :param str path: + Absolute path to original interpreter (e.g. '/usr/bin/python'). + + :returns: + Shell fragment prefix used to execute the script via "/bin/sh -c". + While `ansible_*_interpreter` documentation suggests shell isn't + involved here, the vanilla implementation uses it and that use is + exploited in common playbooks. + """ + interpreter_name = os.path.basename(path).strip() + key = u'ansible_%s_interpreter' % interpreter_name + try: + template = self._inv.task_vars[key] + except KeyError: + pass + else: + configured_interpreter = self._inv.templar.template(template) + return ansible_mitogen.utils.unsafe.cast(configured_interpreter) + + key = u'discovered_interpreter_%s' % interpreter_name + try: + discovered_interpreter = self._inv.task_vars['ansible_facts'][key] + except KeyError: + pass + else: + return ansible_mitogen.utils.unsafe.cast(discovered_interpreter) + + return path + + def _get_interpreter(self): + path, arg = ansible_mitogen.parsing.parse_hashbang( + self._inv.get_module_source() + ) + if path is None: + raise ansible.errors.AnsibleError(NO_INTERPRETER_MSG % ( + self._inv.module_name, + )) + + fragment = self._rewrite_interpreter(path) + if arg: + fragment += ' ' + arg + + is_python = path.startswith('python') + return fragment, is_python + + def get_kwargs(self, **kwargs): + interpreter_fragment, is_python = self._get_interpreter() + return super(ScriptPlanner, self).get_kwargs( + interpreter_fragment=interpreter_fragment, + is_python=is_python, + **kwargs + ) + + +class JsonArgsPlanner(ScriptPlanner): + """ + Script that has its interpreter directive and the task arguments + substituted into its source as a JSON string. + """ + runner_name = 'JsonArgsRunner' + + @classmethod + def detect(cls, path, source): + return ansible.executor.module_common.REPLACER_JSONARGS in source + + +class WantJsonPlanner(ScriptPlanner): + """ + If a module has the string WANT_JSON in it anywhere, Ansible treats it as a + non-native module that accepts a filename as its only command line + parameter. The filename is for a temporary file containing a JSON string + containing the module's parameters. The module needs to open the file, read + and parse the parameters, operate on the data, and print its return data as + a JSON encoded dictionary to stdout before exiting. + + These types of modules are self-contained entities. As of Ansible 2.1, + Ansible only modifies them to change a shebang line if present. + """ + runner_name = 'WantJsonRunner' + + @classmethod + def detect(cls, path, source): + return b'WANT_JSON' in source + + +class NewStylePlanner(ScriptPlanner): + """ + The Ansiballz framework differs from module replacer in that it uses real + Python imports of things in ansible/module_utils instead of merely + preprocessing the module. + """ + runner_name = 'NewStyleRunner' + MARKER = re.compile(br'from ansible(?:_collections|\.module_utils)\.') + + @classmethod + def detect(cls, path, source): + return cls.MARKER.search(source) is not None + + def _get_interpreter(self): + return None, None + + def get_push_files(self): + return super(NewStylePlanner, self).get_push_files() + [ + mitogen.core.to_text(path) + for fullname, path, is_pkg in self.get_module_map()['custom'] + ] + + def get_module_deps(self): + return self.get_module_map()['builtin'] + + #: Module names appearing in this set always require forking, usually due + #: to some terminal leakage that cannot be worked around in any sane + #: manner. + ALWAYS_FORK_MODULES = frozenset([ + 'dnf', # issue #280; py-dnf/hawkey need therapy + 'firewalld', # issue #570: ansible module_utils caches dbus conn + 'ansible.legacy.dnf', # issue #776 + 'ansible.builtin.dnf', # issue #832 + 'freeipa.ansible_freeipa.ipaautomember', # issue #1216 + 'freeipa.ansible_freeipa.ipaautomountkey', + 'freeipa.ansible_freeipa.ipaautomountlocation', + 'freeipa.ansible_freeipa.ipaautomountmap', + 'freeipa.ansible_freeipa.ipacert', + 'freeipa.ansible_freeipa.ipaclient_api', + 'freeipa.ansible_freeipa.ipaclient_fix_ca', + 'freeipa.ansible_freeipa.ipaclient_fstore', + 'freeipa.ansible_freeipa.ipaclient_get_otp', + 'freeipa.ansible_freeipa.ipaclient_ipa_conf', + 'freeipa.ansible_freeipa.ipaclient_join', + 'freeipa.ansible_freeipa.ipaclient_set_hostname', + 'freeipa.ansible_freeipa.ipaclient_setup_automount', + 'freeipa.ansible_freeipa.ipaclient_setup_certmonger', + 'freeipa.ansible_freeipa.ipaclient_setup_firefox', + 'freeipa.ansible_freeipa.ipaclient_setup_krb5', + 'freeipa.ansible_freeipa.ipaclient_setup_nis', + 'freeipa.ansible_freeipa.ipaclient_setup_nss', + 'freeipa.ansible_freeipa.ipaclient_setup_ntp', + 'freeipa.ansible_freeipa.ipaclient_setup_ssh', + 'freeipa.ansible_freeipa.ipaclient_setup_sshd', + 'freeipa.ansible_freeipa.ipaclient_temp_krb5', + 'freeipa.ansible_freeipa.ipaclient_test', + 'freeipa.ansible_freeipa.ipaclient_test_keytab', + 'freeipa.ansible_freeipa.ipaconfig', + 'freeipa.ansible_freeipa.ipadelegation', + 'freeipa.ansible_freeipa.ipadnsconfig', + 'freeipa.ansible_freeipa.ipadnsforwardzone', + 'freeipa.ansible_freeipa.ipadnsrecord', + 'freeipa.ansible_freeipa.ipadnszone', + 'freeipa.ansible_freeipa.ipagroup', + 'freeipa.ansible_freeipa.ipahbacrule', + 'freeipa.ansible_freeipa.ipahbacsvc', + 'freeipa.ansible_freeipa.ipahbacsvcgroup', + 'freeipa.ansible_freeipa.ipahost', + 'freeipa.ansible_freeipa.ipahostgroup', + 'freeipa.ansible_freeipa.idoverridegroup', + 'freeipa.ansible_freeipa.idoverrideuser', + 'freeipa.ansible_freeipa.idp', + 'freeipa.ansible_freeipa.idrange', + 'freeipa.ansible_freeipa.idview', + 'freeipa.ansible_freeipa.ipalocation', + 'freeipa.ansible_freeipa.ipanetgroup', + 'freeipa.ansible_freeipa.ipapermission', + 'freeipa.ansible_freeipa.ipaprivilege', + 'freeipa.ansible_freeipa.ipapwpolicy', + 'freeipa.ansible_freeipa.iparole', + 'freeipa.ansible_freeipa.ipaselfservice', + 'freeipa.ansible_freeipa.ipaserver', + 'freeipa.ansible_freeipa.ipaservice', + 'freeipa.ansible_freeipa.ipaservicedelegationrule', + 'freeipa.ansible_freeipa.ipaservicedelegationtarget', + 'freeipa.ansible_freeipa.ipasudocmd', + 'freeipa.ansible_freeipa.ipasudocmdgroup', + 'freeipa.ansible_freeipa.ipasudorule', + 'freeipa.ansible_freeipa.ipatopologysegment', + 'freeipa.ansible_freeipa.ipatopologysuffix', + 'freeipa.ansible_freeipa.ipatrust', + 'freeipa.ansible_freeipa.ipauser', + 'freeipa.ansible_freeipa.ipavault', + ]) + + def should_fork(self): + """ + In addition to asynchronous tasks, new-style modules should be forked + if: + + * the user specifies mitogen_task_isolation=fork, or + * the new-style module has a custom module search path, or + * the module is known to leak like a sieve. + """ + return ( + super(NewStylePlanner, self).should_fork() or + (self._inv.task_vars.get('mitogen_task_isolation') == 'fork') or + (self._inv.module_name in self.ALWAYS_FORK_MODULES) or + (len(self.get_module_map()['custom']) > 0) + ) + + def get_search_path(self): + return tuple( + path + for path in ansible_mitogen.loaders.module_utils_loader._get_paths( + subdirs=False + ) + ) + + _module_map = None + + def get_module_map(self): + if self._module_map is None: + binding = self._inv.connection.get_binding() + self._module_map = mitogen.service.call( + call_context=binding.get_service_context(), + service_name='ansible_mitogen.services.ModuleDepService', + method_name='scan', + + module_name='ansible_module_%s' % (self._inv.module_name,), + module_path=self._inv.module_path, + search_path=self.get_search_path(), + builtin_path=ansible.executor.module_common._MODULE_UTILS_PATH, + context=self._inv.connection.context, + ) + return self._module_map + + def get_kwargs(self): + return super(NewStylePlanner, self).get_kwargs( + module_map=self.get_module_map(), + py_module_name=py_modname_from_path( + self._inv.module_name, + self._inv.module_path, + ), + ) + + +class ReplacerPlanner(NewStylePlanner): + """ + The Module Replacer framework is the original framework implementing + new-style modules. It is essentially a preprocessor (like the C + Preprocessor for those familiar with that programming language). It does + straight substitutions of specific substring patterns in the module file. + There are two types of substitutions. + + * Replacements that only happen in the module file. These are public + replacement strings that modules can utilize to get helpful boilerplate + or access to arguments. + + "from ansible.module_utils.MOD_LIB_NAME import *" is replaced with the + contents of the ansible/module_utils/MOD_LIB_NAME.py. These should only + be used with new-style Python modules. + + "#<>" is equivalent to + "from ansible.module_utils.basic import *" and should also only apply to + new-style Python modules. + + "# POWERSHELL_COMMON" substitutes the contents of + "ansible/module_utils/powershell.ps1". It should only be used with + new-style Powershell modules. + """ + runner_name = 'ReplacerRunner' + + @classmethod + def detect(cls, path, source): + return ansible.executor.module_common.REPLACER in source + + +class OldStylePlanner(ScriptPlanner): + runner_name = 'OldStyleRunner' + + @classmethod + def detect(cls, path, source): + # Everything else. + return True + + +_planners = [ + BinaryPlanner, + # ReplacerPlanner, + NewStylePlanner, + JsonArgsPlanner, + WantJsonPlanner, + OldStylePlanner, +] + + +def py_modname_from_path(name, path): + """ + Fetch the logical name of a new-style module as it might appear in + :data:`sys.modules` of the target's Python interpreter. + + * Since Ansible 2.9, modules appearing within a package have the original + package hierarchy approximated on the target, enabling relative imports + to function correctly. For example, "ansible.modules.system.setup". + """ + try: + return ansible.executor.module_common._get_ansible_module_fqn(path) + except AttributeError: + pass + except ValueError: + pass + + return 'ansible.modules.' + name + + +def read_file(path): + fd = os.open(path, os.O_RDONLY) + try: + bits = [] + chunk = True + while True: + chunk = os.read(fd, 65536) + if not chunk: + break + bits.append(chunk) + finally: + os.close(fd) + + return b''.join(bits) + + +def _propagate_deps(invocation, planner, context): + binding = invocation.connection.get_binding() + mitogen.service.call( + call_context=binding.get_service_context(), + service_name='mitogen.service.PushFileService', + method_name='propagate_paths_and_modules', + + context=context, + paths=planner.get_push_files(), + # modules=planner.get_module_deps(), TODO + overridden_sources=invocation._overridden_sources, + # needs to be a list because can't unpickle() a set() + extra_sys_paths=list(invocation._extra_sys_paths), + ) + + +def _invoke_async_task(invocation, planner): + job_id = '%016x' % random.randint(0, 2**64) + context = invocation.connection.spawn_isolated_child() + _propagate_deps(invocation, planner, context) + + with mitogen.core.Receiver(context.router) as started_recv: + call_recv = context.call_async( + ansible_mitogen.target.run_module_async, + job_id=job_id, + timeout_secs=ansible_mitogen.utils.unsafe.cast(invocation.timeout_secs), + started_sender=started_recv.to_sender(), + kwargs=planner.get_kwargs(), + ) + + # Wait for run_module_async() to crash, or for AsyncRunner to indicate + # the job file has been written. + for msg in mitogen.select.Select([started_recv, call_recv]): + if msg.receiver is call_recv: + # It can only be an exception. + raise msg.unpickle() + break + + return { + 'stdout': json.dumps({ + # modules/utilities/logic/async_wrapper.py::_run_module(). + 'changed': True, + 'started': 1, + 'finished': 0, + 'ansible_job_id': job_id, + }) + } + + +def _invoke_isolated_task(invocation, planner): + context = invocation.connection.spawn_isolated_child() + _propagate_deps(invocation, planner, context) + try: + return context.call( + ansible_mitogen.target.run_module, + kwargs=planner.get_kwargs(), + ) + finally: + context.shutdown() + + +def _get_planner(invocation, source): + for klass in _planners: + if klass.detect(invocation.module_path, source): + LOG.debug( + '%r accepted %r (filename %r)', + klass, invocation.module_name, invocation.module_path, + ) + return klass + LOG.debug('%r rejected %r', klass, invocation.module_name) + raise ansible.errors.AnsibleError(NO_METHOD_MSG + repr(invocation)) + + +def _fix_py35(invocation, module_source): + """ + super edge case with a relative import error in Python 3.5.1-3.5.3 + in Ansible's setup module when using Mitogen + https://github.com/dw/mitogen/issues/672#issuecomment-636408833 + We replace a relative import in the setup module with the actual full file path + This works in vanilla Ansible but not in Mitogen otherwise + """ + if invocation.module_name in {'ansible.builtin.setup', 'ansible.legacy.setup', 'setup'} and \ + invocation.module_path not in invocation._overridden_sources: + # in-memory replacement of setup module's relative import + # would check for just python3.5 and run this then but we don't know the + # target python at this time yet + # NOTE: another ansible 2.10-specific fix: `from ..module_utils` used to be `from ...module_utils` + module_source = module_source.replace( + b"from ..module_utils.basic import AnsibleModule", + b"from ansible.module_utils.basic import AnsibleModule" + ) + invocation._overridden_sources[invocation.module_path] = module_source + + +def _fix_dnf(invocation, module_source): + """ + Handles edge case where dnf ansible module showed failure due to a missing import in the dnf module. + Specifically addresses errors like "Failed loading plugin 'debuginfo-install': module 'dnf' has no attribute 'cli'". + https://github.com/mitogen-hq/mitogen/issues/1143 + This issue is resolved by adding 'dnf.cli' to the import statement in the module source. + This works in vanilla Ansible but not in Mitogen otherwise. + """ + if invocation.module_name in {'ansible.builtin.dnf', 'ansible.legacy.dnf', 'dnf'} and \ + invocation.module_path not in invocation._overridden_sources: + module_source = module_source.replace( + b"import dnf\n", + b"import dnf, dnf.cli\n" + ) + invocation._overridden_sources[invocation.module_path] = module_source + + +def _load_collections(invocation): + """ + Special loader that ensures that `ansible_collections` exist as a module path for import + Goes through all collection path possibilities and stores paths to installed collections + Stores them on the current invocation to later be passed to the master service + """ + for collection_path in ansible.collections.list.list_collection_dirs(): + invocation._extra_sys_paths.add(collection_path.decode('utf-8')) + + +def invoke(invocation): + """ + Find a Planner subclass corresponding to `invocation` and use it to invoke + the module. + + :param Invocation invocation: + :returns: + Module return dict. + :raises ansible.errors.AnsibleError: + Unrecognized/unsupported module type. + """ + path = ansible_mitogen.loaders.module_loader.find_plugin( + invocation.module_name, + '', + ) + if path is None: + raise ansible.errors.AnsibleError(NO_MODULE_MSG % ( + invocation.module_name, + )) + + invocation.module_path = mitogen.core.to_text(path) + if invocation.module_path not in _planner_by_path: + if 'ansible_collections' in invocation.module_path: + _load_collections(invocation) + + module_source = invocation.get_module_source() + _fix_py35(invocation, module_source) + _fix_dnf(invocation, module_source) + _planner_by_path[invocation.module_path] = _get_planner( + invocation, + module_source + ) + + planner = _planner_by_path[invocation.module_path](invocation) + if invocation.wrap_async: + response = _invoke_async_task(invocation, planner) + elif planner.should_fork(): + response = _invoke_isolated_task(invocation, planner) + else: + _propagate_deps(invocation, planner, invocation.connection.context) + response = invocation.connection.get_chain().call( + ansible_mitogen.target.run_module, + kwargs=planner.get_kwargs(), + ) + + return invocation.action._postprocess_response(response) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/__init__.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/__init__.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_fetch.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_fetch.py new file mode 100644 index 0000000..c1ef190 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_fetch.py @@ -0,0 +1,207 @@ +# (c) 2012-2014, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import base64 +from ansible.errors import AnsibleError, AnsibleActionFail, AnsibleActionSkip +from ansible.module_utils.common.text.converters import to_bytes, to_text +from ansible.module_utils.six import string_types +from ansible.module_utils.parsing.convert_bool import boolean +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display +from ansible.utils.hashing import checksum, checksum_s, md5, secure_hash +from ansible.utils.path import makedirs_safe, is_subpath + +display = Display() + + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + ''' handler for fetch operations ''' + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + try: + if self._play_context.check_mode: + raise AnsibleActionSkip('check mode not (yet) supported for this module') + + source = self._task.args.get('src', None) + original_dest = dest = self._task.args.get('dest', None) + flat = boolean(self._task.args.get('flat'), strict=False) + fail_on_missing = boolean(self._task.args.get('fail_on_missing', True), strict=False) + validate_checksum = boolean(self._task.args.get('validate_checksum', True), strict=False) + + msg = '' + # validate source and dest are strings FIXME: use basic.py and module specs + if not isinstance(source, string_types): + msg = "Invalid type supplied for source option, it must be a string" + + if not isinstance(dest, string_types): + msg = "Invalid type supplied for dest option, it must be a string" + + if source is None or dest is None: + msg = "src and dest are required" + + if msg: + raise AnsibleActionFail(msg) + + source = self._connection._shell.join_path(source) + source = self._remote_expand_user(source) + + remote_stat = {} + remote_checksum = None + if True: + # Get checksum for the remote file even using become. Mitogen doesn't need slurp. + # Follow symlinks because fetch always follows symlinks + try: + remote_stat = self._execute_remote_stat(source, all_vars=task_vars, follow=True) + except AnsibleError as ae: + result['changed'] = False + result['file'] = source + if fail_on_missing: + result['failed'] = True + result['msg'] = to_text(ae) + else: + result['msg'] = "%s, ignored" % to_text(ae, errors='surrogate_or_replace') + + return result + + remote_checksum = remote_stat.get('checksum') + if remote_stat.get('exists'): + if remote_stat.get('isdir'): + result['failed'] = True + result['changed'] = False + result['msg'] = "remote file is a directory, fetch cannot work on directories" + + # Historically, these don't fail because you may want to transfer + # a log file that possibly MAY exist but keep going to fetch other + # log files. Today, this is better achieved by adding + # ignore_errors or failed_when to the task. Control the behaviour + # via fail_when_missing + if not fail_on_missing: + result['msg'] += ", not transferring, ignored" + del result['changed'] + del result['failed'] + + return result + + # use slurp if permissions are lacking or privilege escalation is needed + remote_data = None + if remote_checksum in (None, '1', ''): + slurpres = self._execute_module(module_name='ansible.legacy.slurp', module_args=dict(src=source), task_vars=task_vars) + if slurpres.get('failed'): + if not fail_on_missing: + result['file'] = source + result['changed'] = False + else: + result.update(slurpres) + + if 'not found' in slurpres.get('msg', ''): + result['msg'] = "the remote file does not exist, not transferring, ignored" + elif slurpres.get('msg', '').startswith('source is a directory'): + result['msg'] = "remote file is a directory, fetch cannot work on directories" + + return result + else: + if slurpres['encoding'] == 'base64': + remote_data = base64.b64decode(slurpres['content']) + if remote_data is not None: + remote_checksum = checksum_s(remote_data) + + # calculate the destination name + if os.path.sep not in self._connection._shell.join_path('a', ''): + source = self._connection._shell._unquote(source) + source_local = source.replace('\\', '/') + else: + source_local = source + + # ensure we only use file name, avoid relative paths + if not is_subpath(dest, original_dest): + # TODO: ? dest = os.path.expanduser(dest.replace(('../',''))) + raise AnsibleActionFail("Detected directory traversal, expected to be contained in '%s' but got '%s'" % (original_dest, dest)) + + if flat: + if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict')) and not dest.endswith(os.sep): + raise AnsibleActionFail("dest is an existing directory, use a trailing slash if you want to fetch src into that directory") + if dest.endswith(os.sep): + # if the path ends with "/", we'll use the source filename as the + # destination filename + base = os.path.basename(source_local) + dest = os.path.join(dest, base) + if not dest.startswith("/"): + # if dest does not start with "/", we'll assume a relative path + dest = self._loader.path_dwim(dest) + else: + # files are saved in dest dir, with a subdir for each host, then the filename + if 'inventory_hostname' in task_vars: + target_name = task_vars['inventory_hostname'] + else: + target_name = self._play_context.remote_addr + dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local) + + dest = os.path.normpath(dest) + + # calculate checksum for the local file + local_checksum = checksum(dest) + + if remote_checksum != local_checksum: + # create the containing directories, if needed + makedirs_safe(os.path.dirname(dest)) + + # fetch the file and check for changes + if remote_data is None: + self._connection.fetch_file(source, dest) + else: + try: + f = open(to_bytes(dest, errors='surrogate_or_strict'), 'wb') + f.write(remote_data) + f.close() + except (IOError, OSError) as e: + raise AnsibleActionFail("Failed to fetch the file: %s" % e) + new_checksum = secure_hash(dest) + # For backwards compatibility. We'll return None on FIPS enabled systems + try: + new_md5 = md5(dest) + except ValueError: + new_md5 = None + + if validate_checksum and new_checksum != remote_checksum: + result.update(dict(failed=True, md5sum=new_md5, + msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None, + checksum=new_checksum, remote_checksum=remote_checksum)) + else: + result.update({'changed': True, 'md5sum': new_md5, 'dest': dest, + 'remote_md5sum': None, 'checksum': new_checksum, + 'remote_checksum': remote_checksum}) + else: + # For backwards compatibility. We'll return None on FIPS enabled systems + try: + local_md5 = md5(dest) + except ValueError: + local_md5 = None + result.update(dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum)) + + finally: + self._remove_tmp_path(self._connection._shell.tmpdir) + + return result diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_get_stack.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_get_stack.py new file mode 100644 index 0000000..a8634e5 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/action/mitogen_get_stack.py @@ -0,0 +1,58 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Fetch the connection configuration stack that would be used to connect to a +target, without actually connecting to it. +""" + +from __future__ import absolute_import, division, print_function +from __future__ import unicode_literals +__metaclass__ = type + +import ansible_mitogen.connection + +from ansible.plugins.action import ActionBase + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + if not isinstance(self._connection, + ansible_mitogen.connection.Connection): + return { + 'skipped': True, + } + + _, stack = self._connection._build_stack() + return { + 'changed': True, + 'result': stack, + '_ansible_verbose_always': True, + # for ansible < 2.8, we'll default to /usr/bin/python like before + 'discovered_interpreter': self._connection._action._discovered_interpreter + } diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/__init__.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_buildah.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_buildah.py new file mode 100644 index 0000000..4827375 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_buildah.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'buildah' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_doas.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_doas.py new file mode 100644 index 0000000..23cf4f0 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_doas.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'mitogen_doas' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_docker.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_docker.py new file mode 100644 index 0000000..ac3ba87 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_docker.py @@ -0,0 +1,51 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'docker' + + @property + def docker_cmd(self): + """ + Ansible 2.3 synchronize module wants to know how we run Docker. + """ + return 'docker' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_incus.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_incus.py new file mode 100644 index 0000000..ad3a0c9 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_incus.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2019 David Wilson +# SPDX-FileCopyrightText: 2026 Mitogen authors +# SPDX-License-Identifier: BSD-3-Clause +# !mitogen: minify_safe + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'incus' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_jail.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_jail.py new file mode 100644 index 0000000..9882117 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_jail.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'jail' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_kubectl.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_kubectl.py new file mode 100644 index 0000000..b9088ff --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_kubectl.py @@ -0,0 +1,73 @@ +# coding: utf-8 +# Copyright 2018, Yannig Perré +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +import ansible.errors + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection +import ansible_mitogen.loaders + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'kubectl' + (vanilla_class, load_context) = ansible_mitogen.loaders.connection_loader__get_with_context( + 'kubectl', + class_only=True, + ) + + 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 not Connection.vanilla_class: + raise ansible.errors.AnsibleConnectionFailure(self.not_supported_msg) + super(Connection, self).__init__(*args, **kwargs) + + def get_extra_args(self): + connection_options = Connection.vanilla_class.connection_options + parameters = [] + for key in connection_options: + task_var_name = 'ansible_%s' % key + task_var = self.get_task_var(task_var_name) + if task_var is not None: + parameters += [connection_options[key], task_var] + + return parameters diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_local.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_local.py new file mode 100644 index 0000000..dfc04a2 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_local.py @@ -0,0 +1,80 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection +import ansible_mitogen.process + +viewkeys = getattr(dict, 'viewkeys', dict.keys) + + +def dict_diff(old, new): + """ + Return a dict representing the differences between the dicts `old` and + `new`. Deleted keys appear as a key with the value :data:`None`, added and + changed keys appear as a key with the new value. + """ + old_keys = viewkeys(old) + new_keys = viewkeys(dict(new)) + out = {} + for key in new_keys - old_keys: + out[key] = new[key] + for key in old_keys - new_keys: + out[key] = None + for key in old_keys & new_keys: + if old[key] != new[key]: + out[key] = new[key] + return out + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'local' + + def get_default_cwd(self): + # https://github.com/ansible/ansible/issues/14489 + return self.loader_basedir + + def get_default_env(self): + """ + Vanilla Ansible local commands execute with an environment inherited + from WorkerProcess, we must emulate that. + """ + return dict_diff( + old=ansible_mitogen.process.MuxProcess.cls_original_env, + new=os.environ, + ) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxc.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxc.py new file mode 100644 index 0000000..dc81268 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxc.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'lxc' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxd.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxd.py new file mode 100644 index 0000000..88aa867 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_lxd.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'lxd' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_machinectl.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_machinectl.py new file mode 100644 index 0000000..58f608a --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_machinectl.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'machinectl' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_podman.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_podman.py new file mode 100644 index 0000000..adca559 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_podman.py @@ -0,0 +1,44 @@ +# Copyright 2022, Mitogen contributers +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'podman' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_setns.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_setns.py new file mode 100644 index 0000000..254b703 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_setns.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'setns' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_ssh.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_ssh.py new file mode 100644 index 0000000..6cd126d --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_ssh.py @@ -0,0 +1,70 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +from ansible.plugins.connection.ssh import ( + DOCUMENTATION as _ansible_ssh_DOCUMENTATION, +) + +DOCUMENTATION = """ + name: mitogen_ssh + author: David Wilson + short_description: Connect over SSH via Mitogen + description: + - This connects using an OpenSSH client controlled by the Mitogen for + Ansible extension. It accepts every option the vanilla ssh plugin + accepts. + options: +""" + _ansible_ssh_DOCUMENTATION.partition('options:\n')[2] + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection +import ansible_mitogen.loaders + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'ssh' + (vanilla_class, load_context) = ansible_mitogen.loaders.connection_loader__get_with_context( + 'ssh', + class_only=True, + ) + + @staticmethod + def _create_control_path(*args, **kwargs): + """Forward _create_control_path() to the implementation in ssh.py.""" + # https://github.com/dw/mitogen/issues/342 + return Connection.vanilla_class._create_control_path(*args, **kwargs) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_su.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_su.py new file mode 100644 index 0000000..db30231 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_su.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'mitogen_su' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_sudo.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_sudo.py new file mode 100644 index 0000000..0e390dd --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/connection/mitogen_sudo.py @@ -0,0 +1,44 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'mitogen_sudo' diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/__init__.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen.py new file mode 100644 index 0000000..63c639a --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen.py @@ -0,0 +1,61 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +# +# This is not the real Strategy implementation module, it simply exists as a +# proxy to the real module, which is loaded using Python's regular import +# mechanism, to prevent Ansible's PluginLoader from making up a fake name that +# results in ansible_mitogen plugin modules being loaded twice: once by +# PluginLoader with a name like "ansible.plugins.strategy.mitogen", which is +# stuffed into sys.modules even though attempting to import it will trigger an +# ImportError, and once under its canonical name, "ansible_mitogen.strategy". +# +# Therefore we have a proxy module that imports it under the real name, and +# sets up the duff PluginLoader-imported module to just contain objects from +# the real module, so duplicate types don't exist in memory, and things like +# debuggers and isinstance() work predictably. +# + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.strategy +import ansible.plugins.strategy.linear + + +class StrategyModule(ansible_mitogen.strategy.StrategyMixin, + ansible.plugins.strategy.linear.StrategyModule): + pass diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_free.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_free.py new file mode 100644 index 0000000..0d62671 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_free.py @@ -0,0 +1,62 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +# +# This is not the real Strategy implementation module, it simply exists as a +# proxy to the real module, which is loaded using Python's regular import +# mechanism, to prevent Ansible's PluginLoader from making up a fake name that +# results in ansible_mitogen plugin modules being loaded twice: once by +# PluginLoader with a name like "ansible.plugins.strategy.mitogen", which is +# stuffed into sys.modules even though attempting to import it will trigger an +# ImportError, and once under its canonical name, "ansible_mitogen.strategy". +# +# Therefore we have a proxy module that imports it under the real name, and +# sets up the duff PluginLoader-imported module to just contain objects from +# the real module, so duplicate types don't exist in memory, and things like +# debuggers and isinstance() work predictably. +# + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.loaders +import ansible_mitogen.strategy + + +Base = ansible_mitogen.loaders.strategy_loader.get('free', class_only=True) + +class StrategyModule(ansible_mitogen.strategy.StrategyMixin, Base): + pass diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_host_pinned.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_host_pinned.py new file mode 100644 index 0000000..45a8e96 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_host_pinned.py @@ -0,0 +1,67 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +# +# This is not the real Strategy implementation module, it simply exists as a +# proxy to the real module, which is loaded using Python's regular import +# mechanism, to prevent Ansible's PluginLoader from making up a fake name that +# results in ansible_mitogen plugin modules being loaded twice: once by +# PluginLoader with a name like "ansible.plugins.strategy.mitogen", which is +# stuffed into sys.modules even though attempting to import it will trigger an +# ImportError, and once under its canonical name, "ansible_mitogen.strategy". +# +# Therefore we have a proxy module that imports it under the real name, and +# sets up the duff PluginLoader-imported module to just contain objects from +# the real module, so duplicate types don't exist in memory, and things like +# debuggers and isinstance() work predictably. +# + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.loaders +import ansible_mitogen.strategy + + +Base = ansible_mitogen.loaders.strategy_loader.get('host_pinned', class_only=True) + +if Base is None: + raise ImportError( + 'The host_pinned strategy is only available in Ansible 2.7 or newer.' + ) + +class StrategyModule(ansible_mitogen.strategy.StrategyMixin, Base): + pass diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_linear.py b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_linear.py new file mode 100644 index 0000000..1f96e5b --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/plugins/strategy/mitogen_linear.py @@ -0,0 +1,62 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import sys + +# +# This is not the real Strategy implementation module, it simply exists as a +# proxy to the real module, which is loaded using Python's regular import +# mechanism, to prevent Ansible's PluginLoader from making up a fake name that +# results in ansible_mitogen plugin modules being loaded twice: once by +# PluginLoader with a name like "ansible.plugins.strategy.mitogen", which is +# stuffed into sys.modules even though attempting to import it will trigger an +# ImportError, and once under its canonical name, "ansible_mitogen.strategy". +# +# Therefore we have a proxy module that imports it under the real name, and +# sets up the duff PluginLoader-imported module to just contain objects from +# the real module, so duplicate types don't exist in memory, and things like +# debuggers and isinstance() work predictably. +# + +try: + import ansible_mitogen +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..'))) + +import ansible_mitogen.loaders +import ansible_mitogen.strategy + + +Base = ansible_mitogen.loaders.strategy_loader.get('linear', class_only=True) + +class StrategyModule(ansible_mitogen.strategy.StrategyMixin, Base): + pass diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/process.py b/plugins/mitogen-0.3.44/ansible_mitogen/process.py new file mode 100644 index 0000000..5764477 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/process.py @@ -0,0 +1,710 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import atexit +import logging +import multiprocessing +import os +import resource +import socket +import signal +import sys + +if sys.version_info >= (3, 3): + import faulthandler +else: + faulthandler = None + +try: + import setproctitle +except ImportError: + setproctitle = None + +import mitogen +import mitogen.core +import mitogen.debug +import mitogen.fork +import mitogen.master +import mitogen.parent +import mitogen.service +import mitogen.unix +import mitogen.utils + +import ansible +import ansible.constants as C +import ansible.errors + +import ansible_mitogen.logging +import ansible_mitogen.services +import ansible_mitogen.affinity + + +LOG = logging.getLogger(__name__) + +ANSIBLE_PKG_OVERRIDE = ( + u"__version__ = %r\n" + u"__author__ = %r\n" +) + +MAX_MESSAGE_SIZE = 4096 * 1048576 + +worker_model_msg = ( + 'Mitogen connection types may only be instantiated when one of the ' + '"mitogen_*" or "operon_*" strategies are active.' +) + +shutting_down_msg = ( + 'The task worker cannot connect. Ansible may be shutting down, or ' + 'the maximum open files limit may have been exceeded. If this occurs ' + 'midway through a run, please retry after increasing the open file ' + 'limit (ulimit -n). Original error: %s' +) + + +#: The worker model as configured by the currently running strategy. This is +#: managed via :func:`get_worker_model` / :func:`set_worker_model` functions by +#: :class:`StrategyMixin`. +_worker_model = None + + +#: A copy of the sole :class:`ClassicWorkerModel` that ever exists during a +#: classic run, as return by :func:`get_classic_worker_model`. +_classic_worker_model = None + + +def set_worker_model(model): + """ + To remove process model-wiring from + :class:`ansible_mitogen.connection.Connection`, it is necessary to track + some idea of the configured execution environment outside the connection + plug-in. + + That is what :func:`set_worker_model` and :func:`get_worker_model` are for. + """ + global _worker_model + assert model is None or _worker_model is None + _worker_model = model + + +def get_worker_model(): + """ + Return the :class:`WorkerModel` currently configured by the running + strategy. + """ + if _worker_model is None: + raise ansible.errors.AnsibleConnectionFailure(worker_model_msg) + return _worker_model + + +def get_classic_worker_model(**kwargs): + """ + Return the single :class:`ClassicWorkerModel` instance, constructing it if + necessary. + """ + global _classic_worker_model + assert _classic_worker_model is None or (not kwargs), \ + "ClassicWorkerModel kwargs supplied but model already constructed" + + if _classic_worker_model is None: + _classic_worker_model = ClassicWorkerModel(**kwargs) + return _classic_worker_model + + +def getenv_int(key, default=0): + """ + Get an integer-valued environment variable `key`, if it exists and parses + as an integer, otherwise return `default`. + """ + try: + return int(os.environ.get(key, str(default))) + except ValueError: + return default + + +def save_pid(name): + """ + When debugging and profiling, it is very annoying to poke through the + process list to discover the currently running Ansible and MuxProcess IDs, + especially when trying to catch an issue during early startup. So here, if + a magic environment variable set, stash them in hidden files in the CWD:: + + alias muxpid="cat .ansible-mux.pid" + alias anspid="cat .ansible-controller.pid" + + gdb -p $(muxpid) + perf top -p $(anspid) + """ + if os.environ.get('MITOGEN_SAVE_PIDS'): + with open('.ansible-%s.pid' % (name,), 'w') as fp: + fp.write(str(os.getpid())) + + +def setup_pool(pool): + """ + Configure a connection multiplexer's :class:`mitogen.service.Pool` with + services accessed by clients and WorkerProcesses. + """ + pool.add(mitogen.service.FileService(router=pool.router)) + pool.add(mitogen.service.PushFileService(router=pool.router)) + pool.add(ansible_mitogen.services.ContextService(router=pool.router)) + pool.add(ansible_mitogen.services.ModuleDepService(pool.router)) + LOG.debug('Service pool configured: size=%d', pool.size) + + +def _setup_responder(responder): + """ + Configure :class:`mitogen.master.ModuleResponder` to only permit + certain packages, and to generate custom responses for certain modules. + """ + responder.whitelist_prefix('ansible') + responder.whitelist_prefix('ansible_collections') + responder.whitelist_prefix('ansible_mitogen') + + # Ansible 2.3 is compatible with Python 2.4 targets, however + # ansible/__init__.py is not. Instead, executor/module_common.py writes + # out a 2.4-compatible namespace package for unknown reasons. So we + # copy it here. + responder.add_source_override( + fullname='ansible', + path=ansible.__file__, + source=(ANSIBLE_PKG_OVERRIDE % ( + ansible.__version__, + ansible.__author__, + )).encode(), + is_pkg=True, + ) + + +def increase_open_file_limit(): + """ + #549: in order to reduce the possibility of hitting an open files limit, + increase :data:`resource.RLIMIT_NOFILE` from its soft limit to its hard + limit, if they differ. + + It is common that a low soft limit is configured by default, where the hard + limit is much higher. + """ + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + if hard == resource.RLIM_INFINITY: + hard_s = '(infinity)' + # cap in case of O(RLIMIT_NOFILE) algorithm in some subprocess. + hard = 524288 + else: + hard_s = str(hard) + + LOG.debug('inherited open file limits: soft=%d hard=%s', soft, hard_s) + if soft >= hard: + LOG.debug('max open files already set to hard limit: %d', hard) + return + + # OS X is limited by kern.maxfilesperproc sysctl, rather than the + # advertised unlimited hard RLIMIT_NOFILE. Just hard-wire known defaults + # for that sysctl, to avoid the mess of querying it. + for value in (hard, 10240): + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (value, hard)) + LOG.debug('raised soft open file limit from %d to %d', soft, value) + break + except ValueError as e: + LOG.debug('could not raise soft open file limit from %d to %d: %s', + soft, value, e) + + +def common_setup(enable_affinity=True, _init_logging=True): + save_pid('controller') + ansible_mitogen.logging.set_process_name('top') + + if _init_logging: + ansible_mitogen.logging.setup() + + if enable_affinity: + ansible_mitogen.affinity.policy.assign_controller() + + mitogen.utils.setup_gil() + if faulthandler is not None: + faulthandler.enable() + + MuxProcess.profiling = getenv_int('MITOGEN_PROFILING') > 0 + if MuxProcess.profiling: + mitogen.core.enable_profiling() + + MuxProcess.cls_original_env = dict(os.environ) + increase_open_file_limit() + + +def get_cpu_count(default=None): + """ + Get the multiplexer CPU count from the MITOGEN_CPU_COUNT environment + variable, returning `default` if one isn't set, or is out of range. + + :param int default: + Default CPU, or :data:`None` to use all available CPUs. + """ + max_cpus = multiprocessing.cpu_count() + if default is None: + default = max_cpus + + cpu_count = getenv_int('MITOGEN_CPU_COUNT', default=default) + if cpu_count < 1 or cpu_count > max_cpus: + cpu_count = default + + return cpu_count + + +class Broker(mitogen.master.Broker): + """ + WorkerProcess maintains fewer file descriptors, therefore does not need + the exuberant syscall expense of EpollPoller, so override it and restore + the poll() poller. + """ + poller_class = mitogen.parent.POLLER_LIGHTWEIGHT + + +class Binding(object): + """ + Represent a bound connection for a particular inventory hostname. When + operating in sharded mode, the actual MuxProcess implementing a connection + varies according to the target machine. Depending on the particular + implementation, this class represents a binding to the correct MuxProcess. + """ + def get_child_service_context(self): + """ + Return the :class:`mitogen.core.Context` to which children should + direct requests for services such as FileService, or :data:`None` for + the local process. + + This can be different from :meth:`get_service_context` where MuxProcess + and WorkerProcess are combined, and it is discovered a task is + delegated after being assigned to its initial worker for the original + un-delegated hostname. In that case, connection management and + expensive services like file transfer must be implemented by the + MuxProcess connected to the target, rather than routed to the + MuxProcess responsible for executing the task. + """ + raise NotImplementedError() + + def get_service_context(self): + """ + Return the :class:`mitogen.core.Context` to which this process should + direct ContextService requests, or :data:`None` for the local process. + """ + raise NotImplementedError() + + def close(self): + """ + Finalize any associated resources. + """ + raise NotImplementedError() + + +class WorkerModel(object): + """ + Interface used by StrategyMixin to manage various Mitogen services, by + default running in one or more connection multiplexer subprocesses spawned + off the top-level Ansible process. + """ + def on_strategy_start(self): + """ + Called prior to strategy start in the top-level process. Responsible + for preparing any worker/connection multiplexer state. + """ + raise NotImplementedError() + + def on_strategy_complete(self): + """ + Called after strategy completion in the top-level process. Must place + Ansible back in a "compatible" state where any other strategy plug-in + may execute. + """ + raise NotImplementedError() + + def get_binding(self, inventory_name): + """ + Return a :class:`Binding` to access Mitogen services for + `inventory_name`. Usually called from worker processes, but may also be + called from top-level process to handle "meta: reset_connection". + """ + raise NotImplementedError() + + +class ClassicBinding(Binding): + """ + Only one connection may be active at a time in a classic worker, so its + binding just provides forwarders back to :class:`ClassicWorkerModel`. + """ + def __init__(self, model): + self.model = model + + def get_service_context(self): + """ + See Binding.get_service_context(). + """ + return self.model.parent + + def get_child_service_context(self): + """ + See Binding.get_child_service_context(). + """ + return self.model.parent + + def close(self): + """ + See Binding.close(). + """ + self.model.on_binding_close() + + +class ClassicWorkerModel(WorkerModel): + #: In the top-level process, this references one end of a socketpair(), + #: whose other end child MuxProcesses block reading from to determine when + #: the master process dies. When the top-level exits abnormally, or + #: normally but where :func:`_on_process_exit` has been called, this socket + #: will be closed, causing all the children to wake. + parent_sock = None + + #: In the mux process, this is the other end of :attr:`cls_parent_sock`. + #: The main thread blocks on a read from it until :attr:`cls_parent_sock` + #: is closed. + child_sock = None + + #: mitogen.master.Router for this worker. + router = None + + #: mitogen.master.Broker for this worker. + broker = None + + #: Name of multiplexer process socket we are currently connected to. + listener_path = None + + #: mitogen.parent.Context representing the parent Context, which is the + #: connection multiplexer process when running in classic mode, or the + #: top-level process when running a new-style mode. + parent = None + + def __init__(self, _init_logging=True): + """ + Arrange for classic model multiplexers to be started. The parent choses + UNIX socket paths each child will use prior to fork, creates a + socketpair used essentially as a semaphore, then blocks waiting for the + child to indicate the UNIX socket is ready for use. + + :param bool _init_logging: + For testing, if :data:`False`, don't initialize logging. + """ + # #573: The process ID that installed the :mod:`atexit` handler. If + # some unknown Ansible plug-in forks the Ansible top-level process and + # later performs a graceful Python exit, it may try to wait for child + # PIDs it never owned, causing a crash. We want to avoid that. + self._pid = os.getpid() + + common_setup(_init_logging=_init_logging) + + self.parent_sock, self.child_sock = mitogen.core.socketpair() + mitogen.core.set_cloexec(self.parent_sock.fileno()) + mitogen.core.set_cloexec(self.child_sock.fileno()) + + self._muxes = [ + MuxProcess(self, index) + for index in range(get_cpu_count(default=1)) + ] + for mux in self._muxes: + mux.start() + + atexit.register(self._on_process_exit) + self.child_sock.close() + self.child_sock = None + + def _listener_for_name(self, name): + """ + Given an inventory hostname, return the UNIX listener that should + communicate with it. This is a simple hash of the inventory name. + """ + mux = self._muxes[abs(hash(name)) % len(self._muxes)] + LOG.debug('will use multiplexer %d (%s) to connect to "%s"', + mux.index, mux.path, name) + return mux.path + + def _reconnect(self, path): + if self.router is not None: + # Router can just be overwritten, but the previous parent + # connection must explicitly be removed from the broker first. + self.router.disconnect(self.parent) + self.parent = None + self.router = None + + try: + self.router, self.parent = mitogen.unix.connect( + path=path, + broker=self.broker, + ) + except mitogen.unix.ConnectError as e: + # This is not AnsibleConnectionFailure since we want to break + # with_items loops. + raise ansible.errors.AnsibleError(shutting_down_msg % (e,)) + + self.router.max_message_size = MAX_MESSAGE_SIZE + self.listener_path = path + + def _on_process_exit(self): + """ + This is an :mod:`atexit` handler installed in the top-level process. + + Shut the write end of `sock`, causing the receive side of the socket in + every :class:`MuxProcess` to return 0-byte reads, and causing their + main threads to wake and initiate shutdown. After shutting the socket + down, wait on each child to finish exiting. + + This is done using :mod:`atexit` since Ansible lacks any better hook to + run code during exit, and unless some synchronization exists with + MuxProcess, debug logs may appear on the user's terminal *after* the + prompt has been printed. + """ + if self._pid != os.getpid(): + return + + try: + self.parent_sock.shutdown(socket.SHUT_WR) + except socket.error: + # Already closed. This is possible when tests are running. + LOG.debug('_on_process_exit: ignoring duplicate call') + return + + mitogen.core.io_op(self.parent_sock.recv, 1) + self.parent_sock.close() + + for mux in self._muxes: + _, status = os.waitpid(mux.pid, 0) + status = mitogen.fork._convert_exit_status(status) + LOG.debug('multiplexer %d PID %d %s', mux.index, mux.pid, + mitogen.parent.returncode_to_str(status)) + + def _test_reset(self): + """ + Used to clean up in unit tests. + """ + self.on_binding_close() + self._on_process_exit() + set_worker_model(None) + + global _classic_worker_model + _classic_worker_model = None + + def on_strategy_start(self): + """ + See WorkerModel.on_strategy_start(). + """ + + def on_strategy_complete(self): + """ + See WorkerModel.on_strategy_complete(). + """ + + def get_binding(self, inventory_name): + """ + See WorkerModel.get_binding(). + """ + if self.broker is None: + self.broker = Broker() + + path = self._listener_for_name(inventory_name) + if path != self.listener_path: + self._reconnect(path) + + return ClassicBinding(self) + + def on_binding_close(self): + if not self.broker: + return + + self.broker.shutdown() + self.broker.join() + self.router = None + self.broker = None + self.parent = None + self.listener_path = 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 erroneously shared by children on subsequent + # WorkerProcess forks. To handle that, call on_fork() to ensure any + # shared state is discarded. + # #490: only attempt to clean up when it's known that some resources + # exist to cleanup, otherwise later __del__ double-call to close() due + # to GC at random moment may obliterate an unrelated Connection's + # related resources. + mitogen.fork.on_fork() + + +class MuxProcess(object): + """ + Implement a subprocess forked from the Ansible top-level, as a safe place + to contain the Mitogen IO multiplexer thread, keeping its use of the + logging package (and the logging package's heavy use of locks) far away + from os.fork(), which is used continuously by the multiprocessing package + in the top-level process. + + The problem with running the multiplexer in that process is that should the + multiplexer thread be in the process of emitting a log entry (and holding + its lock) at the point of fork, in the child, the first attempt to log any + log entry using the same handler will deadlock the child, as in the memory + image the child received, the lock will always be marked held. + + See https://bugs.python.org/issue6721 for a thorough description of the + class of problems this worker is intended to avoid. + """ + #: A copy of :data:`os.environ` at the time the multiplexer process was + #: started. It's used by mitogen_local.py to find changes made to the + #: top-level environment (e.g. vars plugins -- issue #297) that must be + #: applied to locally executed commands and modules. + cls_original_env = None + + def __init__(self, model, index): + #: :class:`ClassicWorkerModel` instance we were created by. + self.model = model + #: MuxProcess CPU index. + self.index = index + #: Individual path of this process. + self.path = mitogen.unix.make_socket_path() + + def start(self): + self.pid = os.fork() + if self.pid: + # Wait for child to boot before continuing. + mitogen.core.io_op(self.model.parent_sock.recv, 1) + return + + ansible_mitogen.logging.set_process_name('mux:' + str(self.index)) + if setproctitle: + setproctitle.setproctitle('mitogen mux:%s (%s)' % ( + self.index, + os.path.basename(self.path), + )) + + self.model.parent_sock.close() + self.model.parent_sock = None + try: + try: + self.worker_main() + except Exception: + LOG.exception('worker_main() crashed') + finally: + sys.exit() + + def worker_main(self): + """ + The main function of the mux process: setup the Mitogen broker thread + and ansible_mitogen services, then sleep waiting for the socket + connected to the parent to be closed (indicating the parent has died). + """ + save_pid('mux') + + # #623: MuxProcess ignores SIGINT because it wants to live until every + # Ansible worker process has been cleaned up by + # TaskQueueManager.cleanup(), otherwise harmles yet scary warnings + # about being unable connect to MuxProess could be printed. + signal.signal(signal.SIGINT, signal.SIG_IGN) + ansible_mitogen.logging.set_process_name('mux') + ansible_mitogen.affinity.policy.assign_muxprocess(self.index) + + self._setup_master() + self._setup_services() + + try: + # Let the parent know our listening socket is ready. + mitogen.core.io_op(self.model.child_sock.send, b'1') + # Block until the socket is closed, which happens on parent exit. + mitogen.core.io_op(self.model.child_sock.recv, 1) + finally: + self.broker.shutdown() + self.broker.join() + + # Test frameworks living somewhere higher on the stack of the + # original parent process may try to catch sys.exit(), so do a C + # level exit instead. + os._exit(0) + + def _enable_router_debug(self): + if 'MITOGEN_ROUTER_DEBUG' in os.environ: + self.router.enable_debug() + + def _enable_stack_dumps(self): + secs = getenv_int('MITOGEN_DUMP_THREAD_STACKS', default=0) + if secs: + mitogen.debug.dump_to_logger(secs=secs) + + def _setup_master(self): + """ + Construct a Router, Broker, and mitogen.unix listener + """ + self.broker = mitogen.master.Broker(install_watcher=False) + self.router = mitogen.master.Router( + broker=self.broker, + max_message_size=MAX_MESSAGE_SIZE, + ) + _setup_responder(self.router.responder) + mitogen.core.listen(self.broker, 'shutdown', self._on_broker_shutdown) + mitogen.core.listen(self.broker, 'exit', self._on_broker_exit) + self.listener = mitogen.unix.Listener.build_stream( + router=self.router, + path=self.path, + backlog=C.DEFAULT_FORKS, + ) + self._enable_router_debug() + self._enable_stack_dumps() + + def _setup_services(self): + """ + Construct a ContextService and a thread to service requests for it + arriving from worker processes. + """ + self.pool = mitogen.service.Pool( + router=self.router, + size=getenv_int('MITOGEN_POOL_SIZE', default=32), + ) + setup_pool(self.pool) + + def _on_broker_shutdown(self): + """ + Respond to broker shutdown by shutting down the pool. Do not join on it + yet, since that would block the broker thread which then cannot clean + up pending handlers and connections, which is required for the threads + to exit gracefully. + """ + self.pool.stop(join=False) + + def _on_broker_exit(self): + """ + Respond to the broker thread about to exit by finally joining on the + pool. This is safe since pools only block in connection attempts, and + connection attempts fail with CancelledError when broker shutdown + begins. + """ + self.pool.join() diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/runner.py b/plugins/mitogen-0.3.44/ansible_mitogen/runner.py new file mode 100644 index 0000000..60f5051 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/runner.py @@ -0,0 +1,1098 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +These classes implement execution for each style of Ansible module. They are +instantiated in the target context by way of target.py::run_module(). + +Each class in here has a corresponding Planner class in planners.py that knows +how to build arguments for it, preseed related data, etc. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import atexit +import ctypes +import json +import logging +import os +import re +import shlex +import shutil +import sys +import tempfile +import traceback +import types + +if sys.version_info >= (3, 4): + import importlib.machinery +else: + import imp + +import ansible.module_utils.common.warnings +from ansible.module_utils.six.moves import shlex_quote + +import mitogen.core +import ansible_mitogen.target # TODO: circular import +from mitogen.core import to_text + +try: + # Cannot use cStringIO as it does not support Unicode. + from StringIO import StringIO +except ImportError: + from io import StringIO + +# Prevent accidental import of an Ansible module from hanging on stdin read. +# FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None. +import ansible.module_utils.basic +ansible.module_utils.basic._ANSIBLE_ARGS = '{}' + +# For tasks that modify /etc/resolv.conf, non-Debian derivative glibcs cache +# resolv.conf at startup and never implicitly reload it. Cope with that via an +# explicit call to res_init() on each task invocation. BSD-alikes export it +# directly, Linux #defines it as "__res_init". +libc__res_init = None +libc = ctypes.CDLL(None) +for symbol in 'res_init', '__res_init': + try: + libc__res_init = getattr(libc, symbol) + except AttributeError: + pass + +LOG = logging.getLogger(__name__) + + +def shlex_split_b(s): + """ + Use shlex.split() to split characters in some single-byte encoding, without + knowing what that encoding is. The input is bytes, the output is a list of + bytes. + """ + assert isinstance(s, mitogen.core.BytesType) + if sys.version_info >= (3, 0): + return [ + t.encode('latin1') + for t in shlex.split(s.decode('latin1'), comments=True) + ] + + return [t for t in shlex.split(s, comments=True)] + + +class TempFileWatcher(object): + """ + Since Ansible 2.7.0, lineinfile leaks file descriptors returned by + :func:`tempfile.mkstemp` (ansible/ansible#57327). Handle this and all + similar cases by recording descriptors produced by mkstemp during module + execution, and cleaning up any leaked descriptors on completion. + """ + def __init__(self): + self._real_mkstemp = tempfile.mkstemp + # (fd, st.st_dev, st.st_ino) + self._fd_dev_inode = [] + tempfile.mkstemp = self._wrap_mkstemp + + def _wrap_mkstemp(self, *args, **kwargs): + fd, path = self._real_mkstemp(*args, **kwargs) + st = os.fstat(fd) + self._fd_dev_inode.append((fd, st.st_dev, st.st_ino)) + return fd, path + + def revert(self): + tempfile.mkstemp = self._real_mkstemp + for tup in self._fd_dev_inode: + self._revert_one(*tup) + + def _revert_one(self, fd, st_dev, st_ino): + try: + st = os.fstat(fd) + except OSError: + # FD no longer exists. + return + + if not (st.st_dev == st_dev and st.st_ino == st_ino): + # FD reused. + return + + LOG.info("a tempfile.mkstemp() FD was leaked during the last task") + os.close(fd) + + +class EnvironmentFileWatcher(object): + """ + Usually Ansible edits to /etc/environment and ~/.pam_environment are + reflected in subsequent tasks if become:true or SSH multiplexing is + disabled, due to sudo and/or SSH reinvoking pam_env. Rather than emulate + existing semantics, do our best to ensure edits are always reflected. + + This can't perfectly replicate the existing behaviour, but it can safely + update and remove keys that appear to originate in `path`, and that do not + conflict with any existing environment key inherited from elsewhere. + + A more robust future approach may simply be to arrange for the persistent + interpreter to restart when a change is detected. + """ + # We know nothing about the character set of /etc/environment or the + # process environment. + environ = getattr(os, 'environb', os.environ) + + def __init__(self, path): + self.path = os.path.expanduser(path) + #: Inode data at time of last check. + self._st = self._stat() + #: List of inherited keys appearing to originated from this file. + self._keys = [ + key for key, value in self._load() + if value == self.environ.get(key) + ] + LOG.debug('%r installed; existing keys: %r', self, self._keys) + + def __repr__(self): + return 'EnvironmentFileWatcher(%r)' % (self.path,) + + def _stat(self): + try: + return os.stat(self.path) + except OSError: + return None + + def _load(self): + try: + fp = open(self.path, 'rb') + try: + return list(self._parse(fp)) + finally: + fp.close() + except IOError: + return [] + + def _parse(self, fp): + """ + linux-pam-1.3.1/modules/pam_env/pam_env.c#L207 + """ + for line in fp: + # ' #export foo=some var ' -> ['#export', 'foo=some var '] + bits = shlex_split_b(line) + if (not bits) or bits[0].startswith(b'#'): + continue + + if bits[0] == b'export': + bits.pop(0) + + key, sep, value = b' '.join(bits).partition(b'=') + if key and sep: + yield key, value + + def _on_file_changed(self): + LOG.debug('%r: file changed, reloading', self) + for key, value in self._load(): + if key in self.environ: + LOG.debug('%r: existing key %r=%r exists, not setting %r', + self, key, self.environ[key], value) + else: + LOG.debug('%r: setting key %r to %r', self, key, value) + self._keys.append(key) + self.environ[key] = value + + def _remove_existing(self): + """ + When a change is detected, remove keys that existed in the old file. + """ + for key in self._keys: + if key in self.environ: + LOG.debug('%r: removing old key %r', self, key) + del self.environ[key] + self._keys = [] + + def check(self): + """ + Compare the :func:`os.stat` for the pam_env style environmnt file + `path` with the previous result `old_st`, which may be :data:`None` if + the previous stat attempt failed. Reload its contents if the file has + changed or appeared since last attempt. + + :returns: + New :func:`os.stat` result. The new call to :func:`reload_env` should + pass it as the value of `old_st`. + """ + st = self._stat() + if self._st == st: + return + + self._st = st + self._remove_existing() + + if st is None: + LOG.debug('%r: file has disappeared', self) + else: + self._on_file_changed() + +_pam_env_watcher = EnvironmentFileWatcher('~/.pam_environment') +_etc_env_watcher = EnvironmentFileWatcher('/etc/environment') + + +def utf8(s): + """ + Coerce an object to bytes if it is Unicode. + """ + if isinstance(s, mitogen.core.UnicodeType): + s = s.encode('utf-8') + return s + + +def reopen_readonly(fp): + """ + Replace the file descriptor belonging to the file object `fp` with one + open on the same file (`fp.name`), but opened with :py:data:`os.O_RDONLY`. + This enables temporary files to be executed on Linux, which usually throws + ``ETXTBUSY`` if any writeable handle exists pointing to a file passed to + `execve()`. + """ + fd = os.open(fp.name, os.O_RDONLY) + os.dup2(fd, fp.fileno()) + os.close(fd) + + +class Runner(object): + """ + Ansible module runner. After instantiation (with kwargs supplied by the + corresponding Planner), `.run()` is invoked, upon which `setup()`, + `_run()`, and `revert()` are invoked, with the return value of `_run()` + returned by `run()`. + + Subclasses may override `_run`()` and extend `setup()` and `revert()`. + + :param str module: + Name of the module to execute, e.g. "shell" + :param mitogen.core.Context service_context: + Context to which we should direct FileService calls. For now, always + the connection multiplexer process on the controller. + :param str json_args: + Ansible module arguments. A mixture of user and internal keys created + by :meth:`ansible.plugins.action.ActionBase._execute_module`. + + This is passed as a string rather than a dict in order to mimic the + implicit bytes/str conversion behaviour of a 2.x controller running + against a 3.x target. + :param str good_temp_dir: + The writeable temporary directory for this user account reported by + :func:`ansible_mitogen.target.init_child` passed via the controller. + This is specified explicitly to remain compatible with Ansible<2.5, and + for forked tasks where init_child never runs. + :param dict env: + Additional environment variables to set during the run. Keys with + :data:`None` are unset if present. + :param str cwd: + If not :data:`None`, change to this directory before executing. + :param mitogen.core.ExternalContext econtext: + When `detach` is :data:`True`, a reference to the ExternalContext the + runner is executing in. + :param bool detach: + When :data:`True`, indicate the runner should detach the context from + its parent after setup has completed successfully. + """ + def __init__(self, module, service_context, json_args, good_temp_dir, + extra_env=None, cwd=None, env=None, econtext=None, + detach=False): + self.module = module + self.service_context = service_context + self.econtext = econtext + self.detach = detach + 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 + self.cwd = cwd + #: If not :data:`None`, :meth:`get_temp_dir` had to create a temporary + #: directory for this run, because we're in an asynchronous task, or + #: because the originating action did not create a directory. + self._temp_dir = None + + def get_temp_dir(self): + path = self.args.get('_ansible_tmpdir') + if path is not None: + return path + + if self._temp_dir is None: + self._temp_dir = tempfile.mkdtemp( + prefix='ansible_mitogen_runner_', + dir=self.good_temp_dir, + ) + + return self._temp_dir + + def revert_temp_dir(self): + if self._temp_dir is not None: + ansible_mitogen.target.prune_tree(self._temp_dir) + self._temp_dir = None + + def setup(self): + """ + Prepare for running a module, including fetching necessary dependencies + from the parent, as :meth:`run` may detach prior to beginning + execution. The base implementation simply prepares the environment. + """ + self._setup_cwd() + self._setup_environ() + + def _setup_cwd(self): + """ + For situations like sudo to a non-privileged account, CWD could be + $HOME of the old account, which could have mode go=, which means it is + impossible to restore the old directory, so don't even try. + """ + if self.cwd: + os.chdir(self.cwd) + + def _setup_environ(self): + """ + Apply changes from /etc/environment files before creating a + TemporaryEnvironment to snapshot environment state prior to module run. + """ + _pam_env_watcher.check() + _etc_env_watcher.check() + env = dict(self.extra_env or {}) + if self.env: + env.update(self.env) + self._env = TemporaryEnvironment(env) + + def _revert_cwd(self): + """ + #591: make a best-effort attempt to return to :attr:`good_temp_dir`. + """ + try: + os.chdir(self.good_temp_dir) + except OSError: + LOG.debug('%r: could not restore CWD to %r', + self, self.good_temp_dir) + + def revert(self): + """ + Revert any changes made to the process after running a module. The base + implementation simply restores the original environment. + """ + self._revert_cwd() + self._env.revert() + self.revert_temp_dir() + + def _run(self): + """ + The _run() method is expected to return a dictionary in the form of + ActionBase._low_level_execute_command() output, i.e. having:: + + { + "rc": int, + "stdout": "stdout data", + "stderr": "stderr data" + } + """ + raise NotImplementedError() + + def run(self): + """ + Set up the process environment in preparation for running an Ansible + module. This monkey-patches the Ansible libraries in various places to + prevent it from trying to kill the process on completion, and to + prevent it from reading sys.stdin. + + :returns: + Module result dictionary. + """ + self.setup() + if self.detach: + self.econtext.detach() + + try: + return self._run() + finally: + self.revert() + + +class AtExitWrapper(object): + """ + issue #397, #454: Newer Ansibles use :func:`atexit.register` to trigger + tmpdir cleanup when AnsibleModule.tmpdir is responsible for creating its + own temporary directory, however with Mitogen processes are preserved + across tasks, meaning cleanup must happen earlier. + + Patch :func:`atexit.register`, catching :func:`shutil.rmtree` calls so they + can be executed on task completion, rather than on process shutdown. + """ + # Wrapped in a dict to avoid instance method decoration. + original = { + 'register': atexit.register + } + + def __init__(self): + assert atexit.register == self.original['register'], \ + "AtExitWrapper installed twice." + atexit.register = self._atexit__register + self.deferred = [] + + def revert(self): + """ + Restore the original :func:`atexit.register`. + """ + assert atexit.register == self._atexit__register, \ + "AtExitWrapper not installed." + atexit.register = self.original['register'] + + def run_callbacks(self): + while self.deferred: + func, targs, kwargs = self.deferred.pop() + try: + func(*targs, **kwargs) + except Exception: + LOG.exception('While running atexit callbacks') + + def _atexit__register(self, func, *targs, **kwargs): + """ + Intercept :func:`atexit.register` calls, diverting any to + :func:`shutil.rmtree` into a private list. + """ + if func == shutil.rmtree: + self.deferred.append((func, targs, kwargs)) + return + + self.original['register'](func, *targs, **kwargs) + + +class ModuleUtilsImporter(object): + """ + :param list module_utils: + List of `(fullname, path, is_pkg)` tuples. + """ + def __init__(self, context, module_utils): + self._context = context + self._by_fullname = dict( + (fullname, (path, is_pkg)) + for fullname, path, is_pkg in module_utils + ) + self._loaded = set() + sys.meta_path.insert(0, self) + + def revert(self): + sys.meta_path.remove(self) + for fullname in self._loaded: + sys.modules.pop(fullname, None) + + def find_module(self, fullname, path=None): + """ + Return a loader for the module with fullname, if we will load it. + + Implements importlib.abc.MetaPathFinder.find_module(). + Deprecrated in Python 3.4+, replaced by find_spec(). + Raises ImportWarning in Python 3.10+. Removed in Python 3.12. + """ + if fullname in self._by_fullname: + return self + + def find_spec(self, fullname, path, target=None): + """ + Return a `ModuleSpec` for module with `fullname` if we will load it. + Otherwise return `None`. + + Implements importlib.abc.MetaPathFinder.find_spec(). Python 3.4+. + """ + if fullname.endswith('.'): + return None + + try: + module_path, is_package = self._by_fullname[fullname] + except KeyError: + LOG.debug('Skipping %s: not present', fullname) + return None + + LOG.debug('Handling %s', fullname) + origin = 'master:%s' % (module_path,) + return importlib.machinery.ModuleSpec( + fullname, loader=self, origin=origin, is_package=is_package, + ) + + def create_module(self, spec): + """ + Return a module object for the given ModuleSpec. + + Implements PEP-451 importlib.abc.Loader API introduced in Python 3.4. + Unlike Loader.load_module() this shouldn't populate sys.modules or + set module attributes. Both are done by Python. + """ + module = types.ModuleType(spec.name) + # FIXME create_module() shouldn't initialise module attributes + module.__file__ = spec.origin + return module + + def exec_module(self, module): + """ + Execute the module to initialise it. Don't return anything. + + Implements PEP-451 importlib.abc.Loader API, introduced in Python 3.4. + """ + spec = module.__spec__ + path, _ = self._by_fullname[spec.name] + source = ansible_mitogen.target.get_small_file(self._context, path) + code = compile(source, path, 'exec', 0, 1) + exec(code, module.__dict__) + self._loaded.add(spec.name) + + def load_module(self, fullname): + """ + Return the loaded module specified by fullname. + + Implements PEP 302 importlib.abc.Loader.load_module(). + Deprecated in Python 3.4+, replaced by create_module() & exec_module(). + """ + path, is_pkg = self._by_fullname[fullname] + source = ansible_mitogen.target.get_small_file(self._context, path) + code = compile(source, path, 'exec', 0, 1) + mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) + mod.__file__ = "master:%s" % (path,) + mod.__loader__ = self + if is_pkg: + mod.__path__ = [] + mod.__package__ = str(fullname) + else: + mod.__package__ = str(to_text(fullname).rpartition('.')[0]) + exec(code, mod.__dict__) + self._loaded.add(fullname) + return mod + + +class TemporaryEnvironment(object): + """ + Apply environment changes from `env` until :meth:`revert` is called. Values + in the dict may be :data:`None` to indicate the relevant key should be + deleted. + """ + def __init__(self, env=None): + self.original = dict(os.environ) + self.env = env or {} + for key, value in mitogen.core.iteritems(self.env): + key = mitogen.core.to_text(key) + value = mitogen.core.to_text(value) + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = str(value) + + def revert(self): + """ + Revert changes made by the module to the process environment. This must + always run, as some modules (e.g. git.py) set variables like GIT_SSH + that must be cleared out between runs. + """ + os.environ.clear() + os.environ.update(self.original) + + +class TemporaryArgv(object): + def __init__(self, argv): + self.original = sys.argv[:] + sys.argv[:] = map(str, argv) + + def revert(self): + sys.argv[:] = self.original + + +class NewStyleStdio(object): + """ + Patch ansible.module_utils.basic argument globals. + """ + def __init__(self, args, temp_dir): + self.temp_dir = temp_dir + self.original_stdout = sys.stdout + self.original_stderr = sys.stderr + self.original_stdin = sys.stdin + sys.stdout = StringIO() + sys.stderr = StringIO() + encoded = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + ansible.module_utils.basic._ANSIBLE_ARGS = utf8(encoded) + ansible.module_utils.basic._ANSIBLE_PROFILE = 'legacy' + sys.stdin = StringIO(mitogen.core.to_text(encoded)) + + self.original_get_path = getattr(ansible.module_utils.basic, + 'get_module_path', None) + ansible.module_utils.basic.get_module_path = self._get_path + + def _get_path(self): + return self.temp_dir + + def revert(self): + ansible.module_utils.basic.get_module_path = self.original_get_path + sys.stdout = self.original_stdout + sys.stderr = self.original_stderr + sys.stdin = self.original_stdin + # FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None. + ansible.module_utils.basic._ANSIBLE_ARGS = '{}' + ansible.module_utils.basic._ANSIBLE_PROFILE = None + + +class ProgramRunner(Runner): + """ + Base class for runners that run external programs. + + :param str path: + Absolute path to the program file on the master, as it can be retrieved + via :class:`mitogen.service.FileService`. + :param bool emulate_tty: + If :data:`True`, execute the program with `stdout` and `stderr` merged + into a single pipe, emulating Ansible behaviour when an SSH TTY is in + use. + """ + def __init__(self, path, emulate_tty=None, **kwargs): + super(ProgramRunner, self).__init__(**kwargs) + self.emulate_tty = emulate_tty + self.path = path + + def setup(self): + super(ProgramRunner, self).setup() + self._setup_program() + + def _get_program_filename(self): + """ + Return the filename used for program on disk. Ansible uses the original + filename for non-Ansiballz runs, and "ansible_module_+filename for + Ansiballz runs. + """ + return os.path.basename(self.path) + + program_fp = None + + def _setup_program(self): + """ + Create a temporary file containing the program code. The code is + fetched via :meth:`_get_program`. + """ + filename = self._get_program_filename() + path = os.path.join(self.get_temp_dir(), filename) + self.program_fp = open(path, 'wb') + self.program_fp.write(self._get_program()) + self.program_fp.flush() + os.chmod(self.program_fp.name, int('0700', 8)) + reopen_readonly(self.program_fp) + + def _get_program(self): + """ + Fetch the module binary from the master if necessary. + """ + return ansible_mitogen.target.get_small_file( + context=self.service_context, + path=self.path, + ) + + def _get_program_args(self): + """ + Return any arguments to pass to the program. + """ + return [] + + def revert(self): + """ + Delete the temporary program file. + """ + if self.program_fp: + self.program_fp.close() + super(ProgramRunner, self).revert() + + def _get_argv(self): + """ + Return the final argument vector used to execute the program. + """ + return [ + self.args.get('_ansible_shell_executable', '/bin/sh'), + '-c', + self._get_shell_fragment(), + ] + + def _get_shell_fragment(self): + return "%s %s" % ( + shlex_quote(self.program_fp.name), + ' '.join(map(shlex_quote, self._get_program_args())), + ) + + def _run(self): + try: + rc, stdout, stderr = ansible_mitogen.target.exec_args( + args=self._get_argv(), + emulate_tty=self.emulate_tty, + ) + except Exception: + LOG.exception('While running %s', self._get_argv()) + e = sys.exc_info()[1] + return { + u'rc': 1, + u'stdout': u'', + u'stderr': u'%s: %s' % (type(e), e), + } + + return { + u'rc': rc, + u'stdout': mitogen.core.to_text(stdout), + u'stderr': mitogen.core.to_text(stderr), + } + + +class ArgsFileRunner(Runner): + def setup(self): + super(ArgsFileRunner, self).setup() + self._setup_args() + + def _setup_args(self): + """ + Create a temporary file containing the module's arguments. The + arguments are formatted via :meth:`_get_args`. + """ + self.args_fp = tempfile.NamedTemporaryFile( + prefix='ansible_mitogen', + suffix='-args', + dir=self.get_temp_dir(), + ) + self.args_fp.write(utf8(self._get_args_contents())) + self.args_fp.flush() + reopen_readonly(self.program_fp) + + def _get_args_contents(self): + """ + Return the module arguments formatted as JSON. + """ + return json.dumps(self.args) + + def _get_program_args(self): + return [self.args_fp.name] + + def revert(self): + """ + Delete the temporary argument file. + """ + self.args_fp.close() + super(ArgsFileRunner, self).revert() + + +class BinaryRunner(ArgsFileRunner, ProgramRunner): + pass + + +class ScriptRunner(ProgramRunner): + def __init__(self, interpreter_fragment, is_python, **kwargs): + super(ScriptRunner, self).__init__(**kwargs) + self.interpreter_fragment = interpreter_fragment + self.is_python = is_python + + b_ENCODING_STRING = b'# -*- coding: utf-8 -*-' + + def _get_program(self): + return self._rewrite_source( + super(ScriptRunner, self)._get_program() + ) + + def _get_argv(self): + return [ + self.args.get('_ansible_shell_executable', '/bin/sh'), + '-c', + self._get_shell_fragment(), + ] + + def _get_shell_fragment(self): + """ + Scripts are eligible for having their hashbang line rewritten, and to + be executed via /bin/sh using the ansible_*_interpreter value used as a + shell fragment prefixing to the invocation. + """ + return "%s %s %s" % ( + self.interpreter_fragment, + shlex_quote(self.program_fp.name), + ' '.join(map(shlex_quote, self._get_program_args())), + ) + + def _rewrite_source(self, s): + """ + Mutate the source according to the per-task parameters. + """ + # While Ansible rewrites the #! using ansible_*_interpreter, it is + # never actually used to execute the script, instead it is a shell + # fragment consumed by shell/__init__.py::build_module_command(). + new = [b'#!' + utf8(self.interpreter_fragment)] + if self.is_python: + new.append(self.b_ENCODING_STRING) + + _, _, rest = s.partition(b'\n') + new.append(rest) + return b'\n'.join(new) + + +class NewStyleRunner(ScriptRunner): + """ + Execute a new-style Ansible module, where Module Replacer-related tricks + aren't required. + """ + #: path => new-style module bytecode. + _code_by_path = {} + + def __init__(self, module_map, py_module_name, **kwargs): + super(NewStyleRunner, self).__init__(**kwargs) + self.module_map = module_map + self.py_module_name = py_module_name + + def _setup_imports(self): + """ + Ensure the local importer and PushFileService has everything for the + Ansible module before setup() completes, but before detach() is called + in an asynchronous task. + + The master automatically streams modules towards us concurrent to the + runner invocation, however there is no public API to synchronize on the + completion of those preloads. Instead simply reuse the importer's + synchronization mechanism by importing everything the module will need + prior to detaching. + """ + # I think "custom" means "found in custom module_utils search path", + # e.g. playbook relative dir, ~/.ansible/..., Ansible collection. + for fullname, _, _ in self.module_map['custom']: + mitogen.core.import_module(fullname) + + # I think "builtin" means "part of ansible/ansible-base/ansible-core", + # as opposed to Python builtin modules such as sys. + for fullname in self.module_map['builtin']: + try: + mitogen.core.import_module(fullname) + except ImportError as exc: + # #590: Ansible 2.8 module_utils.distro is a package that + # replaces itself in sys.modules with a non-package during + # import. Prior to replacement, it is a real package containing + # a '_distro' submodule which is used on 2.x. Given a 2.x + # controller and 3.x target, the import hook never needs to run + # again before this replacement occurs, and 'distro' is + # replaced with a module from the stdlib. In this case as this + # loop progresses to the next entry and attempts to preload + # 'distro._distro', the import mechanism will fail. So here we + # silently ignore any failure for it. + if fullname == 'ansible.module_utils.distro._distro': + continue + + # ansible.module_utils.compat.selinux raises ImportError if it + # can't load libselinux.so. The importer would usually catch + # this & skip selinux operations. We don't care about selinux, + # we're using import to get a copy of the module. + if (fullname == 'ansible.module_utils.compat.selinux' + and exc.msg == 'unable to load libselinux.so'): + continue + + raise + + def _setup_module_utils_globals(self): + "Remove lingering global state from previous task." + _global_warnings = ansible.module_utils.common.warnings._global_warnings + _global_deprecations = ansible.module_utils.common.warnings._global_deprecations + if isinstance(_global_warnings, list): + del _global_warnings[:] + del _global_deprecations[:] + else: + _global_warnings.clear() + _global_deprecations.clear() + + def _setup_excepthook(self): + """ + Starting with Ansible 2.6, some modules (file.py) install a + sys.excepthook and never clean it up. So we must preserve the original + excepthook and restore it after the run completes. + """ + self.original_excepthook = sys.excepthook + + def setup(self): + super(NewStyleRunner, self).setup() + + self._stdio = NewStyleStdio(self.args, self.get_temp_dir()) + # It is possible that not supplying the script filename will break some + # module, but this has never been a bug report. Instead act like an + # interpreter that had its script piped on stdin. + self._argv = TemporaryArgv(['']) + self._temp_watcher = TempFileWatcher() + self._importer = ModuleUtilsImporter( + context=self.service_context, + module_utils=self.module_map['custom'], + ) + self._setup_imports() + self._setup_module_utils_globals() + self._setup_excepthook() + self.atexit_wrapper = AtExitWrapper() + if libc__res_init: + libc__res_init() + + def _revert_excepthook(self): + sys.excepthook = self.original_excepthook + + def revert(self): + self.atexit_wrapper.revert() + self._temp_watcher.revert() + self._argv.revert() + self._stdio.revert() + self._revert_excepthook() + super(NewStyleRunner, self).revert() + + def _get_program_filename(self): + """ + See ProgramRunner._get_program_filename(). + """ + return 'ansible_module_' + os.path.basename(self.path) + + def _setup_args(self): + pass + + # issue #555: in old times it was considered good form to reload sys and + # change the default encoding. This hack was removed from Ansible long ago, + # but not before permeating into many third party modules. + PREHISTORIC_HACK_RE = re.compile( + br'reload\s*\(\s*sys\s*\)\s*sys\s*\.\s*setdefaultencoding\([^)]+\)', + ) + + def _setup_program(self): + source = ansible_mitogen.target.get_small_file( + context=self.service_context, + path=self.path, + ) + self.source = self.PREHISTORIC_HACK_RE.sub(b'', source) + + def _get_code(self): + try: + return self._code_by_path[self.path] + except KeyError: + return self._code_by_path.setdefault(self.path, compile( + # Py2.4 doesn't support kwargs. + self.source, # source + "master:" + self.path, # filename + 'exec', # mode + 0, # flags + True, # dont_inherit + )) + + if sys.version_info >= (3, 0): + main_module_name = '__main__' + else: + main_module_name = b'__main__' + + def _handle_magic_exception(self, mod, exc): + """ + Beginning with Ansible >2.6, some modules (file.py) install a + sys.excepthook which is a closure over AnsibleModule, redirecting the + magical exception to AnsibleModule.fail_json(). + + For extra special needs bonus points, the class is not defined in + module_utils, but is defined in the module itself, meaning there is no + type for isinstance() that outlasts the invocation. + """ + klass = getattr(mod, 'AnsibleModuleError', None) + if klass and isinstance(exc, klass): + mod.module.fail_json(**exc.results) + + def _run_code(self, code, mod): + try: + if sys.version_info >= (3, 0): + exec(code, vars(mod)) + else: + exec('exec code in vars(mod)') + except Exception: + self._handle_magic_exception(mod, sys.exc_info()[1]) + raise + + def _get_module_package(self): + """ + Since Ansible 2.9 __package__ must be set in accordance with an + approximation of the original package hierarchy, so that relative + imports function correctly. + """ + pkg, sep, _ = self.py_module_name.rpartition('.') + if not sep: + return None + if sys.version_info >= (3, 0): + return pkg + return pkg.encode() + + def _run(self): + mod = types.ModuleType(self.main_module_name) + mod.__package__ = self._get_module_package() + # Some Ansible modules use __file__ to find the Ansiballz temporary + # directory. We must provide some temporary path in __file__, but we + # don't want to pointlessly write the module to disk when it never + # actually needs to exist. So just pass the filename as it would exist. + mod.__file__ = os.path.join( + self.get_temp_dir(), + 'ansible_module_' + os.path.basename(self.path), + ) + + code = self._get_code() + rc = 2 + try: + try: + self._run_code(code, mod) + except SystemExit: + exc = sys.exc_info()[1] + rc = exc.args[0] + except Exception: + # This writes to stderr by default. + traceback.print_exc() + rc = 1 + + finally: + self.atexit_wrapper.run_callbacks() + + return { + u'rc': rc, + u'stdout': mitogen.core.to_text(sys.stdout.getvalue()), + u'stderr': mitogen.core.to_text(sys.stderr.getvalue()), + } + + +class JsonArgsRunner(ScriptRunner): + JSON_ARGS = b'<>' + + def _get_args_contents(self): + return json.dumps(self.args).encode() + + def _rewrite_source(self, s): + return ( + super(JsonArgsRunner, self)._rewrite_source(s) + .replace(self.JSON_ARGS, self._get_args_contents()) + ) + + +class WantJsonRunner(ArgsFileRunner, ScriptRunner): + pass + + +class OldStyleRunner(ArgsFileRunner, ScriptRunner): + def _get_args_contents(self): + """ + Mimic the argument formatting behaviour of + ActionBase._execute_module(). + """ + return ' '.join( + '%s=%s' % (key, shlex_quote(str(self.args[key]))) + for key in self.args + ) + ' ' # Bug-for-bug :( diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/services.py b/plugins/mitogen-0.3.44/ansible_mitogen/services.py new file mode 100644 index 0000000..a48ab75 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/services.py @@ -0,0 +1,559 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +Classes in this file define Mitogen 'services' that run (initially) within the +connection multiplexer process that is forked off the top-level controller +process. + +Once a worker process connects to a multiplexer process +(Connection._connect()), it communicates with these services to establish new +connections, grant access to files by children, and register for notification +when a child has completed a job. +""" + +from __future__ import absolute_import, division, print_function +from __future__ import unicode_literals +__metaclass__ = type + +import logging +import os +import sys +import threading + +import ansible.constants + +from ansible.module_utils.six import reraise + +import mitogen.core +import mitogen.service +import ansible_mitogen.loaders +import ansible_mitogen.module_finder +import ansible_mitogen.target +import ansible_mitogen.utils +import ansible_mitogen.utils.unsafe + + +LOG = logging.getLogger(__name__) + +# Force load of plugin to ensure ConfigManager has definitions loaded. Done +# during module import to ensure a single-threaded environment; PluginLoader +# is not thread-safe. +ansible_mitogen.loaders.shell_loader.get('sh') + + +def _get_candidate_temp_dirs(): + try: + # >=2.5 + options = ansible.constants.config.get_plugin_options('shell', 'sh') + remote_tmp = options.get('remote_tmp') or ansible.constants.DEFAULT_REMOTE_TMP + system_tmpdirs = options.get('system_tmpdirs', ('/var/tmp', '/tmp')) + except AttributeError: + # 2.3 + remote_tmp = ansible.constants.DEFAULT_REMOTE_TMP + system_tmpdirs = ('/var/tmp', '/tmp') + + return ansible_mitogen.utils.unsafe.cast([remote_tmp] + list(system_tmpdirs)) + + +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 + + +class ContextService(mitogen.service.Service): + """ + Used by workers to fetch the single Context instance corresponding to a + connection configuration, creating the matching connection if it does not + exist. + + For connection methods and their parameters, see: + https://mitogen.readthedocs.io/en/latest/api.html#context-factories + + This concentrates connections in the top-level process, which may become a + bottleneck. The bottleneck can be removed using per-CPU connection + processes and arranging for the worker to select one according to a hash of + the connection parameters (sharding). + """ + max_interpreters = int(os.getenv('MITOGEN_MAX_INTERPRETERS', '20')) + + def __init__(self, *args, **kwargs): + 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_dict`. + self._response_by_key = {} + #: List of :class:`mitogen.core.Latch` awaiting the result for a + #: particular key. + self._latches_by_key = {} + #: Mapping of :class:`mitogen.core.Context` -> reference count. Each + #: call to :meth:`get` increases this by one. Calls to :meth:`put` + #: decrease it by one. + self._refs_by_context = {} + #: List of contexts in creation order by via= parameter. When + #: :attr:`max_interpreters` is reached, the most recently used context + #: is destroyed to make room for any additional context. + 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({ + 'stack': list, + }) + def reset(self, stack): + """ + Return a reference, forcing close and discard of the underlying + connection. Used for 'meta: reset_connection' or when some other error + is detected. + + :returns: + :data:`True` if a connection was found to discard, otherwise + :data:`False`. + """ + LOG.debug('%r.reset(%r)', self, stack) + + # this could happen if we have a `shutdown -r` shell command + # and then a `wait_for_connection` right afterwards + # in this case, we have no stack to disconnect from + if not stack: + return False + + l = mitogen.core.Latch() + context = None + with self._lock: + for i, spec in enumerate(stack): + key = key_from_dict(via=context, **spec) + response = self._response_by_key.get(key) + if response is None: + LOG.debug('%r: could not find connection to shut down; ' + 'failed at hop %d', self, i) + return False + + context = response['context'] + + mitogen.core.listen(context, 'disconnect', l.put) + self._shutdown_unlocked(context) + + # The timeout below is to turn a hang into a crash in case there is any + # possible race between 'disconnect' signal subscription, and the child + # abruptly disconnecting. + l.get(timeout=30.0) + return True + + @mitogen.service.expose(mitogen.service.AllowParents()) + @mitogen.service.arg_spec({ + 'context': mitogen.core.Context + }) + def put(self, context): + """ + Return a reference, making it eligable for recycling once its reference + count reaches zero. + """ + LOG.debug('decrementing reference count for %r', context) + self._lock.acquire() + try: + if self._refs_by_context.get(context, 0) == 0: + LOG.warning('%r.put(%r): refcount was 0. shutdown_all called?', + self, context) + return + self._refs_by_context[context] -= 1 + finally: + self._lock.release() + + 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_dict` + :param dict response: + Response dictionary + :returns: + Number of waiters that were replied to. + """ + self._lock.acquire() + try: + latches = self._latches_by_key.pop(key) + count = len(latches) + for latch in latches: + latch.put(response) + finally: + 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` + to the LRU list while holding the lock. + """ + LOG.info('%r._shutdown_unlocked(): shutting down %r', self, context) + context.shutdown() + 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): + """ + Update the LRU ("MRU"?) list associated with the connection described + 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) + return + + for context in reversed(lru): + if self._refs_by_context[context] == 0: + break + else: + LOG.warning('via=%r reached maximum number of interpreters, ' + 'but they are all marked as in-use.', via) + return + + self._shutdown_unlocked(context, lru=lru, new_context=new_context) + + def _update_lru(self, new_context, spec, via): + self._lock.acquire() + try: + self._update_lru_unlocked(new_context, spec, via) + 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): + """ + For testing use, arrange for all connections to be shut down. + """ + self._lock.acquire() + try: + for context in list(self._key_by_context): + self._shutdown_unlocked(context) + finally: + self._lock.release() + + def _on_context_disconnect(self, context): + """ + 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. + """ + self._lock.acquire() + try: + LOG.info('%r: Forgetting %r due to stream disconnect', self, context) + self._forget_context_unlocked(context) + finally: + self._lock.release() + + ALWAYS_PRELOAD = ( + 'ansible.module_utils.basic', + 'ansible.module_utils.json_utils', + 'ansible.release', + 'ansible_mitogen.runner', + 'ansible_mitogen.target', + 'mitogen.fork', + 'mitogen.service', + ) + (( + 'ansible.module_utils._internal._json._profiles._module_legacy_c2m', + 'ansible.module_utils._internal._json._profiles._module_legacy_m2c', + 'ansible.module_utils._internal._json._profiles._module_modern_c2m', + 'ansible.module_utils._internal._json._profiles._module_legacy_m2c', + ) if ansible_mitogen.utils.ansible_version[:2] >= (2, 19) else ()) + + def _send_module_forwards(self, context): + if hasattr(self.router.responder, 'forward_modules'): + self.router.responder.forward_modules(context, self.ALWAYS_PRELOAD) + + _candidate_temp_dirs = None + + def _get_candidate_temp_dirs(self): + """ + Return a list of locations to try to create the single temporary + directory used by the run. This simply caches the (expensive) plugin + load of :func:`_get_candidate_temp_dirs`. + """ + if self._candidate_temp_dirs is None: + self._candidate_temp_dirs = _get_candidate_temp_dirs() + return self._candidate_temp_dirs + + def _connect(self, key, spec, via=None): + """ + Actual connect implementation. Arranges for the Mitogen connection to + be created and enqueues an asynchronous call to start the forked task + parent in the remote context. + + :param key: + Deduplication key representing the connection configuration. + :param spec: + Connection specification. + :returns: + Dict like:: + + { + 'context': mitogen.core.Context or None, + 'via': mitogen.core.Context or None, + 'init_child_result': { + 'fork_context': mitogen.core.Context, + 'home_dir': str or None, + }, + 'msg': str or None + } + + Where `context` is a reference to the newly constructed context, + `init_child_result` is the result of executing + :func:`ansible_mitogen.target.init_child` in that context, `msg` is + an error message and the remaining fields are :data:`None`, or + `msg` is :data:`None` and the remaining fields are set. + """ + try: + method = getattr(self.router, spec['method']) + except AttributeError: + raise Error('unsupported method: %(method)s' % spec) + + context = method(via=via, unidirectional=True, **spec['kwargs']) + if via and spec.get('enable_lru'): + self._update_lru(context, spec, via) + + # 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( + ansible_mitogen.target.init_child, + log_level=LOG.getEffectiveLevel(), + candidate_temp_dirs=self._get_candidate_temp_dirs(), + ) + + if os.environ.get('MITOGEN_DUMP_THREAD_STACKS'): + from mitogen import debug + context.call(debug.dump_to_logger) + + self._key_by_context[context] = key + self._refs_by_context[context] = 0 + return { + 'context': context, + 'via': via, + 'init_child_result': init_child_result, + 'msg': None, + } + + def _wait_or_start(self, spec, via=None): + latch = mitogen.core.Latch() + key = key_from_dict(via=via, **spec) + self._lock.acquire() + try: + response = self._response_by_key.get(key) + if response is not None: + self._refs_by_context[response['context']] += 1 + latch.put(response) + return latch + + latches = self._latches_by_key.setdefault(key, []) + first = len(latches) == 0 + latches.append(latch) + finally: + self._lock.release() + + if first: + # I'm the first requestee, so I will create the connection. + try: + response = self._connect(key, spec, via=via) + count = self._produce_response(key, response) + # Only record the response for non-error results. + self._response_by_key[key] = response + # Set the reference count to the number of waiters. + self._refs_by_context[response['context']] += count + except Exception: + self._produce_response(key, sys.exc_info()) + + return latch + + disconnect_msg = ( + 'Channel was disconnected while connection attempt was in progress; ' + 'this may be caused by an abnormal Ansible exit, or due to an ' + 'unreliable target.' + ) + + @mitogen.service.expose(mitogen.service.AllowParents()) + @mitogen.service.arg_spec({ + 'stack': list + }) + def get(self, stack): + """ + Return a Context referring to an established connection with the given + configuration, establishing new connections as necessary. + + :param list stack: + Connection descriptions. Each element is a dict containing 'method' + and 'kwargs' keys describing the Router method and arguments. + Subsequent elements are proxied via the previous. + + :returns dict: + * context: mitogen.parent.Context or None. + * init_child_result: Result of :func:`init_child`. + * msg: StreamError exception text or None. + * method_name: string failing method name. + """ + via = None + for spec in stack: + try: + result = self._wait_or_start(spec, via=via).get() + if isinstance(result, tuple): # exc_info() + reraise(*result) + via = result['context'] + except mitogen.core.ChannelError: + return { + 'context': None, + 'init_child_result': None, + 'method_name': spec['method'], + 'msg': self.disconnect_msg, + } + except mitogen.core.StreamError as e: + return { + 'context': None, + 'init_child_result': None, + 'method_name': spec['method'], + 'msg': str(e), + } + + return result + + +class ModuleDepService(mitogen.service.Service): + """ + Scan a new-style module and produce a cached mapping of module_utils names + to their resolved filesystem paths. + """ + invoker_class = mitogen.service.SerializedInvoker + + def __init__(self, *args, **kwargs): + super(ModuleDepService, self).__init__(*args, **kwargs) + self._cache = {} + + def _get_builtin_names(self, builtin_path, resolved): + return [ + mitogen.core.to_text(fullname) + for fullname, path, is_pkg in resolved + if os.path.abspath(path).startswith(builtin_path) + ] + + def _get_custom_tups(self, builtin_path, resolved): + return [ + (mitogen.core.to_text(fullname), + mitogen.core.to_text(path), + is_pkg) + for fullname, path, is_pkg in resolved + if not os.path.abspath(path).startswith(builtin_path) + ] + + @mitogen.service.expose(policy=mitogen.service.AllowParents()) + @mitogen.service.arg_spec({ + 'module_name': mitogen.core.UnicodeType, + 'module_path': mitogen.core.FsPathTypes, + 'search_path': tuple, + 'builtin_path': mitogen.core.FsPathTypes, + 'context': mitogen.core.Context, + }) + def scan(self, module_name, module_path, search_path, builtin_path, context): + key = (module_name, search_path) + if key not in self._cache: + resolved = ansible_mitogen.module_finder.scan( + module_name=module_name, + module_path=module_path, + search_path=tuple(search_path) + (builtin_path,), + ) + builtin_path = os.path.abspath(builtin_path) + builtin = self._get_builtin_names(builtin_path, resolved) + custom = self._get_custom_tups(builtin_path, resolved) + self._cache[key] = { + 'builtin': builtin, + 'custom': custom, + } + return self._cache[key] diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/strategy.py b/plugins/mitogen-0.3.44/ansible_mitogen/strategy.py new file mode 100644 index 0000000..c0b2ef8 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/strategy.py @@ -0,0 +1,397 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import signal +import threading + +try: + import setproctitle +except ImportError: + setproctitle = None + +import mitogen.core +import ansible_mitogen.affinity +import ansible_mitogen.loaders +import ansible_mitogen.logging +import ansible_mitogen.mixins +import ansible_mitogen.process + +import ansible.executor.process.worker +import ansible.template +import ansible.utils.sentinel +import ansible.playbook.play_context +import ansible.plugins.loader + + +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: + import awx_display_callback.events + except ImportError: + return + + if hasattr(awx_display_callback.events.EventContext(), '_local'): + # Patched version. + return + + def patch_add_local(self, **kwargs): + tls = vars(self._local) + ctx = tls.setdefault('_ctx', {}) + ctx.update(kwargs) + + awx_display_callback.events.EventContext._local = threading.local() + awx_display_callback.events.EventContext.add_local = patch_add_local + +_patch_awx_callback() + + +def wrap_action_loader__get_with_context(name, *args, **kwargs): + """ + While the mitogen strategy is active, trap action_loader.get_with_context() + calls, augmenting any fetched class with ActionModuleMixin, which replaces + various helper methods inherited from ActionBase with implementations that + avoid the use of shell fragments wherever possible. + + This is used instead of static subclassing as it generalizes to third party + action plugins outside the Ansible tree. + """ + get_kwargs = {'class_only': True} + if name in ('fetch',): + name = 'mitogen_' + name + get_kwargs['collection_list'] = kwargs.pop('collection_list', None) + + (klass, context) = ansible_mitogen.loaders.action_loader__get_with_context( + name, + **get_kwargs + ) + + if klass: + bases = (ansible_mitogen.mixins.ActionModuleMixin, klass) + adorned_klass = type(str(name), bases, {}) + if kwargs.get('class_only'): + return ansible.plugins.loader.get_with_context_result( + adorned_klass, + context + ) + + return ansible.plugins.loader.get_with_context_result( + adorned_klass(*args, **kwargs), + context + ) + + return ansible.plugins.loader.get_with_context_result(None, context) + + +REDIRECTED_CONNECTION_PLUGINS = ( + 'buildah', + 'docker', + 'kubectl', + 'jail', + 'local', + 'lxc', + 'lxd', + 'machinectl', + 'podman', + 'setns', + 'ssh', +) + + +def wrap_connection_loader__get_with_context(name, *args, **kwargs): + """ + While a Mitogen strategy is active, rewrite + connection_loader.get_with_context() calls for some transports into + requests for a compatible Mitogen transport. + """ + is_play_using_mitogen_connection = None + if len(args) > 0 and isinstance(args[0], ansible.playbook.play_context.PlayContext): + play_context = args[0] + is_play_using_mitogen_connection = play_context.connection in REDIRECTED_CONNECTION_PLUGINS + + # assume true if we're not in a play context since we're using a Mitogen strategy + if is_play_using_mitogen_connection is None: + is_play_using_mitogen_connection = True + + redirect_connection = name in REDIRECTED_CONNECTION_PLUGINS and is_play_using_mitogen_connection + if redirect_connection: + name = 'mitogen_' + name + + return ansible_mitogen.loaders.connection_loader__get_with_context(name, *args, **kwargs) + + +def wrap_worker__run(self): + """ + While a Mitogen strategy is active, trap WorkerProcess.run() calls and use + the opportunity to set the worker's name in the process list and log + output, activate profiling if requested, and bind the worker to a specific + CPU. + """ + if setproctitle: + setproctitle.setproctitle('worker:%s task:%s' % ( + self._host.name, + self._task.action, + )) + + # Ignore parent's attempts to murder us when we still need to write + # profiling output. + if mitogen.core._profile_hook.__name__ != '_profile_hook': + signal.signal(signal.SIGTERM, signal.SIG_IGN) + + ansible_mitogen.logging.set_process_name('task') + ansible_mitogen.affinity.policy.assign_worker() + return mitogen.core._profile_hook('WorkerProcess', + lambda: worker__run(self) + ) + + +class AnsibleWrappers(object): + """ + Manage add/removal of various Ansible runtime hooks. + """ + def _add_plugin_paths(self): + """ + Add the Mitogen plug-in directories to the ModuleLoader path, avoiding + the need for manual configuration. + """ + base_dir = os.path.join(os.path.dirname(__file__), 'plugins') + ansible_mitogen.loaders.connection_loader.add_directory( + os.path.join(base_dir, 'connection') + ) + ansible_mitogen.loaders.action_loader.add_directory( + os.path.join(base_dir, 'action') + ) + + def _install_wrappers(self): + """ + Install our PluginLoader monkey patches and update global variables + with references to the real functions. + """ + ansible_mitogen.loaders.action_loader.get_with_context = wrap_action_loader__get_with_context + ansible_mitogen.loaders.connection_loader.get_with_context = wrap_connection_loader__get_with_context + + global worker__run + worker__run = ansible.executor.process.worker.WorkerProcess.run + ansible.executor.process.worker.WorkerProcess.run = wrap_worker__run + + def _remove_wrappers(self): + """ + Uninstall the PluginLoader monkey patches. + """ + ansible_mitogen.loaders.action_loader.get_with_context = ( + ansible_mitogen.loaders.action_loader__get_with_context + ) + ansible_mitogen.loaders.connection_loader.get_with_context = ( + ansible_mitogen.loaders.connection_loader__get_with_context + ) + ansible.executor.process.worker.WorkerProcess.run = worker__run + + def install(self): + self._add_plugin_paths() + self._install_wrappers() + + def remove(self): + self._remove_wrappers() + + +class StrategyMixin(object): + """ + This mix-in enhances any built-in strategy by arranging for an appropriate + WorkerModel instance to be constructed as necessary, or for the existing + one to be reused. + + The WorkerModel in turn arranges for a connection multiplexer to be started + somewhere (by default in an external process), and for WorkerProcesses to + grow support for using those top-level services to communicate with remote + hosts. + + Mitogen: + + A private Broker IO multiplexer thread is created to dispatch IO + between the local Router and any connected streams, including streams + connected to Ansible WorkerProcesses, and SSH commands implementing + connections to remote machines. + + A Router is created that implements message dispatch to any locally + registered handlers, and message routing for remote streams. Router is + the junction point through which WorkerProceses and remote SSH contexts + can communicate. + + Router additionally adds message handlers for a variety of base + services, review the Standard Handles section of the How It Works guide + in the documentation. + + A ContextService is installed as a message handler in the connection + mutliplexer subprocess and run on a private thread. It is responsible + for accepting requests to establish new SSH connections from worker + processes, and ensuring precisely one connection exists and is reused + for subsequent playbook steps. The service presently runs in a single + thread, so to begin with, new SSH connections are serialized. + + Finally a mitogen.unix listener is created through which WorkerProcess + can establish a connection back into the connection multiplexer, in + order to avail of ContextService. A UNIX listener socket is necessary + as there is no more sane mechanism to arrange for IPC between the + Router in the connection multiplexer, and the corresponding Router in + the worker process. + + Ansible: + + PluginLoader monkey patches are installed to catch attempts to create + connection and action plug-ins. + + For connection plug-ins, if the desired method is "local" or "ssh", it + is redirected to one of the "mitogen_*" connection plug-ins. That + plug-in implements communication via a UNIX socket connection to the + connection multiplexer process, and uses ContextService running there + to establish a persistent connection to the target. + + For action plug-ins, the original class is looked up as usual, but a + new subclass is created dynamically in order to mix-in + ansible_mitogen.target.ActionModuleMixin, which overrides many of the + methods usually inherited from ActionBase in order to replace them with + pure-Python equivalents that avoid the use of shell. + + In particular, _execute_module() is overridden with an implementation + that uses ansible_mitogen.target.run_module() executed in the target + Context. run_module() implements module execution by importing the + module as if it were a normal Python module, and capturing its output + in the remote process. Since the Mitogen module loader is active in the + remote process, all the heavy lifting of transferring the action module + and its dependencies are automatically handled by Mitogen. + """ + + def _queue_task(self, host, task, task_vars, play_context): + """ + Many PluginLoader caches are defective as they are only populated in + the ephemeral WorkerProcess. Touch each plug-in path before forking to + ensure all workers receive a hot cache. + """ + ansible_mitogen.loaders.module_loader.find_plugin( + name=task.action, + mod_type='', + ) + ansible_mitogen.loaders.action_loader.get( + name=task.action, + class_only=True, + ) + if play_context.connection is not ansible.utils.sentinel.Sentinel: + # 2.8 appears to defer computing this until inside the worker. + # TODO: figure out where it has moved. + ansible_mitogen.loaders.connection_loader.get( + name=play_context.connection, + class_only=True, + ) + + return super(StrategyMixin, self)._queue_task( + host=host, + task=task, + task_vars=task_vars, + play_context=play_context, + ) + + def _get_worker_model(self): + """ + In classic mode a single :class:`WorkerModel` exists, which manages + references and configuration of the associated connection multiplexer + process. + """ + return ansible_mitogen.process.get_classic_worker_model() + + def run(self, iterator, play_context, result=0): + """ + Wrap :meth:`run` to ensure requisite infrastructure and modifications + are configured for the duration of the call. + """ + wrappers = AnsibleWrappers() + self._worker_model = self._get_worker_model() + ansible_mitogen.process.set_worker_model(self._worker_model) + try: + self._worker_model.on_strategy_start() + try: + wrappers.install() + try: + run = super(StrategyMixin, self).run + return mitogen.core._profile_hook('Strategy', + lambda: run(iterator, play_context) + ) + finally: + wrappers.remove() + finally: + self._worker_model.on_strategy_complete() + finally: + ansible_mitogen.process.set_worker_model(None) + + def _smuggle_to_connection_reset(self, task, play_context, iterator, target_host): + """ + Create a templar and make it available for use in Connection.reset(). + This allows templated connection variables to be used when Mitogen + reconstructs its connection stack. + """ + variables = self._variable_manager.get_vars( + play=iterator._play, host=target_host, task=task, + _hosts=self._hosts_cache, _hosts_all=self._hosts_cache_all, + ) + templar = ansible.template.Templar( + loader=self._loader, variables=variables, + ) + + # Required for remote_user option set by variable (e.g. ansible_user). + # Without it remote_user in ansible.cfg gets used. + play_context = play_context.set_task_and_variable_override( + task=task, variables=variables, templar=templar, + ) + play_context.post_validate(templar=templar) + + # Required for timeout option set by variable (e.g. ansible_timeout). + # Without it the task timeout keyword (default: 0) gets used. + play_context.update_vars(variables) + + # Stash the task and templar somewhere Connection.reset() can find it + play_context.vars.update({ + '_mitogen.smuggled.reset_connection': (task, templar), + }) + return play_context + + def _execute_meta(self, task, play_context, iterator, target_host): + if task.args['_raw_params'] == 'reset_connection': + play_context = self._smuggle_to_connection_reset( + task, play_context, iterator, target_host, + ) + + return super(StrategyMixin, self)._execute_meta( + task, play_context, iterator, target_host, + ) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/target.py b/plugins/mitogen-0.3.44/ansible_mitogen/target.py new file mode 100644 index 0000000..199f3a1 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/target.py @@ -0,0 +1,755 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +Helper functions intended to be executed on the target. These are entrypoints +for file transfer, module execution and sundry bits like changing file modes. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import errno +import grp +import json +import logging +import os +import pty +import pwd +import re +import signal +import stat +import subprocess +import sys +import tempfile +import traceback +import types + +import mitogen.core +import mitogen.parent +import mitogen.service + +# Ansible since PR #41749 inserts "import __main__" into +# ansible.module_utils.basic. Mitogen's importer will refuse such an import, so +# we must setup a fake "__main__" before that module is ever imported. The +# str() is to cast Unicode to bytes on Python 2.6. +if not sys.modules.get(str('__main__')): + sys.modules[str('__main__')] = types.ModuleType(str('__main__')) + +import ansible.module_utils.json_utils + +import ansible_mitogen.runner + + +LOG = logging.getLogger(__name__) + +MAKE_TEMP_FAILED_MSG = ( + u"Unable to find a useable temporary directory. This likely means no\n" + u"system-supplied TMP directory can be written to, or all directories\n" + u"were mounted on 'noexec' filesystems.\n" + u"\n" + u"The following paths were tried:\n" + u" %(paths)s\n" + u"\n" + u"Please check '-vvv' output for a log of individual path errors." +) + +# Python 2.4/2.5 cannot support fork+threads whatsoever, it doesn't even fix up +# interpreter state. So 2.4/2.5 interpreters start .local() contexts for +# isolation instead. Since we don't have any crazy memory sharing problems to +# avoid, there is no virginal fork parent either. The child is started directly +# from the login/become process. In future this will be default everywhere, +# fork is brainwrong from the stone age. +FORK_SUPPORTED = sys.version_info >= (2, 6) + +#: Initialized to an econtext.parent.Context pointing at a pristine fork of +#: the target Python interpreter before it executes any code or imports. +_fork_parent = None + +#: Set by :func:`init_child` to the name of a writeable and executable +#: temporary directory accessible by the active user account. +good_temp_dir = None + + +def subprocess__Popen__close_fds(self, but): + """ + issue #362, #435: subprocess.Popen(close_fds=True) aka. + AnsibleModule.run_command() loops the entire FD space on Python<3.2. + CentOS>5 ships with 1,048,576 FDs by default, resulting in huge (>500ms) + latency starting children. Therefore replace Popen._close_fds on Linux with + a version that is O(fds) rather than O(_SC_OPEN_MAX). + """ + try: + names = os.listdir(u'/proc/self/fd') + except OSError: + # May fail if acting on a container that does not have /proc mounted. + self._original_close_fds(but) + return + + for name in names: + if not name.isdigit(): + continue + + fd = int(name, 10) + if fd > pty.STDERR_FILENO and fd != but: + try: + os.close(fd) + except OSError: + pass + + +if ( + sys.platform.startswith(u'linux') and + sys.version_info < (3,) and + hasattr(subprocess.Popen, u'_close_fds') and + not mitogen.is_master +): + subprocess.Popen._original_close_fds = subprocess.Popen._close_fds + subprocess.Popen._close_fds = subprocess__Popen__close_fds + + +def get_small_file(context, path): + """ + Basic in-memory caching module fetcher. This generates one roundtrip for + every previously unseen file, so it is only a temporary solution. + + :param context: + Context we should direct FileService requests to. For now (and probably + forever) this is just the top-level Mitogen connection manager process. + :param path: + Path to fetch from FileService, must previously have been registered by + a privileged context using the `register` command. + :returns: + Bytestring file data. + """ + pool = mitogen.service.get_or_create_pool(router=context.router) + service = pool.get_service(u'mitogen.service.PushFileService') + return service.get(path) + + +def transfer_file(context, in_path, out_path, sync=False, set_owner=False): + """ + Streamily download a file from the connection multiplexer process in the + controller. + + :param mitogen.core.Context context: + Reference to the context hosting the FileService that will transmit the + file. + :param bytes in_path: + FileService registered name of the input file. + :param bytes out_path: + Name of the output path on the local disk. + :param bool sync: + If :data:`True`, ensure the file content and metadat are fully on disk + before renaming the temporary file over the existing file. This should + ensure in the case of system crash, either the entire old or new file + are visible post-reboot. + :param bool set_owner: + If :data:`True`, look up the metadata username and group on the local + system and file the file owner using :func:`os.fchmod`. + """ + out_path = os.path.abspath(out_path) + fd, tmp_path = tempfile.mkstemp(suffix='.tmp', + prefix='.ansible_mitogen_transfer-', + dir=os.path.dirname(out_path)) + fp = os.fdopen(fd, 'wb', mitogen.core.CHUNK_SIZE) + LOG.debug('transfer_file(%r) temporary file: %s', out_path, tmp_path) + + try: + try: + ok, metadata = mitogen.service.FileService.get( + context=context, + path=in_path, + out_fp=fp, + ) + if not ok: + raise IOError('transfer of %r was interrupted.' % (in_path,)) + + set_file_mode(tmp_path, metadata['mode'], fd=fp.fileno()) + if set_owner: + set_file_owner(tmp_path, metadata['owner'], metadata['group'], + fd=fp.fileno()) + finally: + fp.close() + + if sync: + os.fsync(fp.fileno()) + os.rename(tmp_path, out_path) + except BaseException: + os.unlink(tmp_path) + raise + + os.utime(out_path, (metadata['atime'], metadata['mtime'])) + + +def prune_tree(path): + """ + Like shutil.rmtree(), but log errors rather than discard them, and do not + waste multiple os.stat() calls discovering whether the object can be + deleted, just try deleting it instead. + """ + try: + os.unlink(path) + return + except OSError: + e = sys.exc_info()[1] + if not (os.path.isdir(path) and + e.args[0] in (errno.EPERM, errno.EISDIR)): + LOG.error('prune_tree(%r): %s', path, e) + return + + try: + # Ensure write access for readonly directories. Ignore error in case + # path is on a weird filesystem (e.g. vfat). + os.chmod(path, int('0700', 8)) + except OSError: + e = sys.exc_info()[1] + LOG.warning('prune_tree(%r): %s', path, e) + + try: + for name in os.listdir(path): + if name not in ('.', '..'): + prune_tree(os.path.join(path, name)) + os.rmdir(path) + except OSError: + e = sys.exc_info()[1] + LOG.error('prune_tree(%r): %s', path, e) + + +def is_good_temp_dir(path): + """ + Return :data:`True` if `path` can be used as a temporary directory, logging + any failures that may cause it to be unsuitable. If the directory doesn't + exist, we attempt to create it using :func:`os.makedirs`. + """ + if not os.path.exists(path): + try: + os.makedirs(path, mode=int('0700', 8)) + except OSError: + e = sys.exc_info()[1] + LOG.debug('temp dir %r unusable: did not exist and attempting ' + 'to create it failed: %s', path, e) + return False + + try: + tmp = tempfile.NamedTemporaryFile( + prefix='ansible_mitogen_is_good_temp_dir', + dir=path, + ) + except (OSError, IOError): + e = sys.exc_info()[1] + LOG.debug('temp dir %r unusable: %s', path, e) + return False + + try: + try: + os.chmod(tmp.name, int('0700', 8)) + except OSError: + e = sys.exc_info()[1] + LOG.debug('temp dir %r unusable: chmod failed: %s', path, e) + return False + + try: + # access(.., X_OK) is sufficient to detect noexec. + if not os.access(tmp.name, os.X_OK): + raise OSError('filesystem appears to be mounted noexec') + except OSError: + e = sys.exc_info()[1] + LOG.debug('temp dir %r unusable: %s', path, e) + return False + finally: + tmp.close() + + return True + + +def find_good_temp_dir(candidate_temp_dirs): + """ + Given a list of candidate temp directories extracted from ``ansible.cfg``, + combine it with the Python-builtin list of candidate directories used by + :mod:`tempfile`, then iteratively try each until one is found that is both + writeable and executable. + + :param list candidate_temp_dirs: + List of candidate $variable-expanded and tilde-expanded directory paths + that may be usable as a temporary directory. + """ + paths = [os.path.expandvars(os.path.expanduser(p)) + for p in candidate_temp_dirs] + paths.extend(tempfile._candidate_tempdir_list()) + + for path in paths: + if is_good_temp_dir(path): + LOG.debug('Selected temp directory: %r (from %r)', path, paths) + return path + + raise IOError(MAKE_TEMP_FAILED_MSG % { + 'paths': '\n '.join(paths), + }) + + +@mitogen.core.takes_econtext +def init_child(econtext, log_level, candidate_temp_dirs): + """ + Called by ContextService immediately after connection; arranges for the + (presently) spotless Python interpreter to be forked, where the newly + forked interpreter becomes the parent of any newly forked future + interpreters. + + This is necessary to prevent modules that are executed in-process from + polluting the global interpreter state in a way that effects explicitly + isolated modules. + + :param int log_level: + Logging package level active in the master. + :param list[str] candidate_temp_dirs: + List of $variable-expanded and tilde-expanded directory names to add to + candidate list of temporary directories. + + :returns: + Dict like:: + + { + 'fork_context': mitogen.core.Context or None, + 'good_temp_dir': ... + 'home_dir': str + } + + Where `fork_context` refers to the newly forked 'fork parent' context + the controller will use to start forked jobs, and `home_dir` is the + home directory for the active user account. + """ + # Copying the master's log level causes log messages to be filtered before + # they reach LogForwarder, thus reducing an influx of tiny messges waking + # the connection multiplexer process in the master. + LOG.setLevel(log_level) + logging.getLogger('ansible_mitogen').setLevel(log_level) + + global _fork_parent + if FORK_SUPPORTED: + mitogen.parent.upgrade_router(econtext) + _fork_parent = econtext.router.fork() + + global good_temp_dir + good_temp_dir = find_good_temp_dir(candidate_temp_dirs) + + return { + u'fork_context': _fork_parent, + u'home_dir': mitogen.core.to_text(os.path.expanduser('~')), + u'good_temp_dir': good_temp_dir, + } + + +@mitogen.core.takes_econtext +def spawn_isolated_child(econtext): + """ + For helper functions executed in the fork parent context, arrange for + the context's router to be upgraded as necessary and for a new child to be + prepared. + + The actual fork occurs from the 'virginal fork parent', which does not have + any Ansible modules loaded prior to fork, to avoid conflicts resulting from + custom module_utils paths. + """ + mitogen.parent.upgrade_router(econtext) + if FORK_SUPPORTED: + context = econtext.router.fork() + else: + context = econtext.router.local() + LOG.debug('create_fork_child() -> %r', context) + return context + + +def run_module(kwargs): + """ + Set up the process environment in preparation for running an Ansible + module. This monkey-patches the Ansible libraries in various places to + prevent it from trying to kill the process on completion, and to prevent it + from reading sys.stdin. + """ + runner_name = kwargs.pop('runner_name') + klass = getattr(ansible_mitogen.runner, runner_name) + impl = klass(**mitogen.core.Kwargs(kwargs)) + return impl.run() + + +def _get_async_dir(): + return os.path.expanduser( + os.environ.get('ANSIBLE_ASYNC_DIR', '~/.ansible_async') + ) + + +class AsyncRunner(object): + def __init__(self, job_id, timeout_secs, started_sender, econtext, kwargs): + self.job_id = job_id + self.timeout_secs = timeout_secs + self.started_sender = started_sender + self.econtext = econtext + self.kwargs = kwargs + self._timed_out = False + self._init_path() + + def _init_path(self): + async_dir = _get_async_dir() + if not os.path.exists(async_dir): + os.makedirs(async_dir) + self.path = os.path.join(async_dir, self.job_id) + + def _update(self, dct): + """ + Update an async job status file. + """ + LOG.info('%r._update(%r, %r)', self, self.job_id, dct) + dct.setdefault('ansible_job_id', self.job_id) + dct.setdefault('data', '') + + fp = open(self.path + '.tmp', 'w') + try: + fp.write(json.dumps(dct)) + finally: + fp.close() + os.rename(self.path + '.tmp', self.path) + + def _on_sigalrm(self, signum, frame): + """ + Respond to SIGALRM (job timeout) by updating the job file and killing + the process. + """ + msg = "Job reached maximum time limit of %d seconds." % ( + self.timeout_secs, + ) + self._update({ + "failed": 1, + "finished": 1, + "msg": msg, + }) + self._timed_out = True + self.econtext.broker.shutdown() + + def _install_alarm(self): + signal.signal(signal.SIGALRM, self._on_sigalrm) + signal.alarm(self.timeout_secs) + + def _run_module(self): + kwargs = dict(self.kwargs, **{ + 'detach': True, + 'econtext': self.econtext, + 'emulate_tty': False, + }) + return run_module(kwargs) + + def _parse_result(self, dct): + filtered, warnings = ( + ansible.module_utils.json_utils. + _filter_non_json_lines(dct['stdout']) + ) + result = json.loads(filtered) + result.setdefault('warnings', []).extend(warnings) + result['stderr'] = dct['stderr'] or result.get('stderr', '') + self._update(result) + + def _run(self): + """ + 1. Immediately updates the status file to mark the job as started. + 2. Installs a timer/signal handler to implement the time limit. + 3. Runs as with run_module(), writing the result to the status file. + + :param dict kwargs: + Runner keyword arguments. + :param str job_id: + String job ID. + :param int timeout_secs: + If >0, limit the task's maximum run time. + """ + self._update({ + 'started': 1, + 'finished': 0, + 'pid': os.getpid() + }) + self.started_sender.send(True) + + if self.timeout_secs > 0: + self._install_alarm() + + dct = self._run_module() + if not self._timed_out: + # After SIGALRM fires, there is a window between broker responding + # to shutdown() by killing the process, and work continuing on the + # main thread. If main thread was asleep in at least + # basic.py/select.select(), an EINTR will be raised. We want to + # discard that exception. + try: + self._parse_result(dct) + except Exception: + self._update({ + "failed": 1, + "msg": traceback.format_exc(), + "data": dct['stdout'], # temporary notice only + "stderr": dct['stderr'] + }) + + def run(self): + try: + try: + self._run() + except Exception: + self._update({ + "failed": 1, + "msg": traceback.format_exc(), + }) + finally: + self.econtext.broker.shutdown() + + +@mitogen.core.takes_econtext +def run_module_async(kwargs, job_id, timeout_secs, started_sender, econtext): + """ + Execute a module with its run status and result written to a file, + terminating on the process on completion. This function must run in a child + forked using :func:`create_fork_child`. + + @param mitogen.core.Sender started_sender: + A sender that will receive :data:`True` once the job has reached a + point where its initial job file has been written. This is required to + avoid a race where an overly eager controller can check for a task + before it has reached that point in execution, which is possible at + least on Python 2.4, where forking is not available for async tasks. + """ + arunner = AsyncRunner( + job_id, + timeout_secs, + started_sender, + econtext, + kwargs + ) + arunner.run() + + +def get_user_shell(): + """ + For commands executed directly via an SSH command-line, SSH looks up the + user's shell via getpwuid() and only defaults to /bin/sh if that field is + missing or empty. + """ + try: + pw_shell = pwd.getpwuid(os.geteuid()).pw_shell + except KeyError: + pw_shell = None + + return pw_shell or '/bin/sh' + + +def exec_args(args, in_data='', chdir=None, shell=None, emulate_tty=False): + """ + Run a command in a subprocess, emulating the argument handling behaviour of + SSH. + + :param list[str]: + Argument vector. + :param bytes in_data: + Optional standard input for the command. + :param bool emulate_tty: + If :data:`True`, arrange for stdout and stderr to be merged into the + stdout pipe and for LF to be translated into CRLF, emulating the + behaviour of a TTY. + :return: + (return code, stdout bytes, stderr bytes) + """ + LOG.debug('exec_args(%r, ..., chdir=%r)', args, chdir) + assert isinstance(args, list) + + if emulate_tty: + stderr = subprocess.STDOUT + else: + stderr = subprocess.PIPE + + proc = subprocess.Popen( + args=args, + stdout=subprocess.PIPE, + stderr=stderr, + stdin=subprocess.PIPE, + cwd=chdir, + ) + stdout, stderr = proc.communicate(in_data) + + if emulate_tty: + stdout = stdout.replace(b'\n', b'\r\n') + return proc.returncode, stdout, stderr or b'' + + +def exec_command(cmd, in_data='', chdir=None, shell=None, emulate_tty=False): + """ + Run a command in a subprocess, emulating the argument handling behaviour of + SSH. + + :param bytes cmd: + String command line, passed to user's shell. + :param bytes in_data: + Optional standard input for the command. + :return: + (return code, stdout bytes, stderr bytes) + """ + assert isinstance(cmd, mitogen.core.UnicodeType) + return exec_args( + args=[get_user_shell(), '-c', cmd], + in_data=in_data, + chdir=chdir, + shell=shell, + emulate_tty=emulate_tty, + ) + + +def read_path(path): + """ + Fetch the contents of a filesystem `path` as bytes. + """ + with open(path, 'rb') as f: + return f.read() + + +def set_file_owner(path, owner, group=None, fd=None): + if owner: + uid = pwd.getpwnam(owner).pw_uid + else: + uid = os.geteuid() + + if group: + gid = grp.getgrnam(group).gr_gid + else: + gid = os.getegid() + + if fd is not None: + os.fchown(fd, uid, gid) + else: + os.chown(path, uid, gid) + + +def write_path(path, s, owner=None, group=None, mode=None, + utimes=None, sync=False): + """ + Writes bytes `s` to a filesystem `path`. + """ + path = os.path.abspath(path) + fd, tmp_path = tempfile.mkstemp(suffix='.tmp', + prefix='.ansible_mitogen_transfer-', + dir=os.path.dirname(path)) + fp = os.fdopen(fd, 'wb', mitogen.core.CHUNK_SIZE) + LOG.debug('write_path(path=%r) temporary file: %s', path, tmp_path) + + try: + try: + if mode: + set_file_mode(tmp_path, mode, fd=fp.fileno()) + if owner or group: + set_file_owner(tmp_path, owner, group, fd=fp.fileno()) + fp.write(s) + finally: + fp.close() + + if sync: + os.fsync(fp.fileno()) + os.rename(tmp_path, path) + except BaseException: + os.unlink(tmp_path) + raise + + if utimes: + os.utime(path, utimes) + + +CHMOD_CLAUSE_PAT = re.compile(r'([uoga]*)([+\-=])([ugo]|[rwx]*)') +CHMOD_MASKS = { + 'u': stat.S_IRWXU, + 'g': stat.S_IRWXG, + 'o': stat.S_IRWXO, + 'a': (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO), +} +CHMOD_BITS = { + 'u': {'r': stat.S_IRUSR, 'w': stat.S_IWUSR, 'x': stat.S_IXUSR}, + 'g': {'r': stat.S_IRGRP, 'w': stat.S_IWGRP, 'x': stat.S_IXGRP}, + 'o': {'r': stat.S_IROTH, 'w': stat.S_IWOTH, 'x': stat.S_IXOTH}, + 'a': { + 'r': (stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH), + 'w': (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH), + 'x': (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + } +} + + +def apply_mode_spec(spec, mode): + """ + Given a symbolic file mode change specification in the style of chmod(1) + `spec`, apply changes in the specification to the numeric file mode `mode`. + """ + for clause in mitogen.core.to_text(spec).split(','): + match = CHMOD_CLAUSE_PAT.match(clause) + who, op, perms = match.groups() + for ch in who or 'a': + mask = CHMOD_MASKS[ch] + bits = CHMOD_BITS[ch] + cur_perm_bits = mode & mask + new_perm_bits = 0 + for perm in perms: + new_perm_bits |= bits[perm] + mode &= ~mask + if op == '=': + mode |= new_perm_bits + elif op == '+': + mode |= new_perm_bits | cur_perm_bits + else: + mode |= cur_perm_bits & ~new_perm_bits + return mode + + +def set_file_mode(path, spec, fd=None): + """ + Update the permissions of a file using the same syntax as chmod(1). + """ + if isinstance(spec, mitogen.core.integer_types): + new_mode = spec + elif spec.isdigit(): + new_mode = int(spec, 8) + else: + mode = os.stat(path).st_mode + new_mode = apply_mode_spec(spec, mode) + + if fd is not None: + os.fchmod(fd, new_mode) + else: + os.chmod(path, new_mode) + + +def file_exists(path): + """ + Return :data:`True` if `path` exists. This is a wrapper function over + :func:`os.path.exists`, since its implementation module varies across + Python versions. + """ + return os.path.exists(path) diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/transport_config.py b/plugins/mitogen-0.3.44/ansible_mitogen/transport_config.py new file mode 100644 index 0000000..97a2db8 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/transport_config.py @@ -0,0 +1,891 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Mitogen extends Ansible's target configuration mechanism in several ways that +require some care: + +* Per-task configurables in Ansible like ansible_python_interpreter are + connection-layer configurables in Mitogen. They must be extracted during each + task execution to form the complete connection-layer configuration. + +* Mitogen has extra configurables not supported by Ansible at all, such as + mitogen_ssh_debug_level. These are extracted the same way as + ansible_python_interpreter. + +* Mitogen allows connections to be delegated to other machines. Ansible has no + internal framework for this, and so Mitogen must figure out a delegated + connection configuration all on its own. It cannot reuse much of the Ansible + machinery for building a connection configuration, as that machinery is + deeply spread out and hard-wired to expect Ansible's usual mode of operation. + +For normal and delegate_to connections, Ansible's PlayContext is reused where +possible to maximize compatibility, but for proxy hops, configurations are +built up using the HostVars magic class to call VariableManager.get_vars() +behind the scenes on our behalf. Where Ansible has multiple sources of a +configuration item, for example, ansible_ssh_extra_args, Mitogen must (ideally +perfectly) reproduce how Ansible arrives at its value, without using mechanisms +that are hard-wired or change across Ansible versions. + +That is what this file is for. It exports two spec classes, one that takes all +information from PlayContext, and another that takes (almost) all information +from HostVars. +""" + +from __future__ import absolute_import, division, print_function +from __future__ import unicode_literals +__metaclass__ = type + +import abc +import logging +import os + +import ansible.utils.shlex +import ansible.constants as C +import ansible.executor.interpreter_discovery +import ansible.utils.unsafe_proxy + +from ansible.module_utils.six import with_metaclass +from ansible.module_utils.parsing.convert_bool import boolean + +import ansible_mitogen.utils +import mitogen.core + + +LOG = logging.getLogger(__name__) + +if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + _FALLBACK_INTERPRETER = ansible.executor.interpreter_discovery._FALLBACK_INTERPRETER +elif ansible_mitogen.utils.ansible_version[:2] >= (2, 17): + _FALLBACK_INTERPRETER = u'/usr/bin/python3' +else: + _FALLBACK_INTERPRETER = u'/usr/bin/python' + + +def run_interpreter_discovery_if_necessary(s, candidates, task_vars, action, rediscover_python): + """ + Triggers ansible python interpreter discovery if requested. + Caches this value the same way Ansible does it. + For connections like `docker`, we want to rediscover the python interpreter because + it could be different than what's ran on the host + """ + # keep trying different interpreters until we don't error + if action._mitogen_discovering_interpreter: + return action._mitogen_interpreter_candidate + + if s in ['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent']: + # python is the only supported interpreter_name as of Ansible 2.8.8 + interpreter_name = 'python' + discovered_interpreter_config = u'discovered_interpreter_%s' % interpreter_name + + if task_vars.get('ansible_facts') is None: + task_vars['ansible_facts'] = {} + + if rediscover_python and task_vars.get('ansible_facts', {}).get(discovered_interpreter_config): + # if we're rediscovering python then chances are we're running something like a docker connection + # this will handle scenarios like running a playbook that does stuff + then dynamically creates a docker container, + # then runs the rest of the playbook inside that container, and then rerunning the playbook again + action._mitogen_rediscovered_interpreter = True + + # blow away the discovered_interpreter_config cache and rediscover + del task_vars['ansible_facts'][discovered_interpreter_config] + + try: + s = task_vars[u'ansible_facts'][discovered_interpreter_config] + except KeyError: + action._mitogen_discovering_interpreter = True + action._mitogen_interpreter_candidates = candidates + # fake pipelining so discover_interpreter can be happy + action._connection.has_pipelining = True + s = ansible.executor.interpreter_discovery.discover_interpreter( + action=action, + interpreter_name=interpreter_name, + discovery_mode=s, + task_vars=task_vars, + ) + s = ansible.utils.unsafe_proxy.AnsibleUnsafeText(s) + # cache discovered interpreter + task_vars['ansible_facts'][discovered_interpreter_config] = s + action._connection.has_pipelining = False + + # propagate discovered interpreter as fact + action._discovered_interpreter_key = discovered_interpreter_config + action._discovered_interpreter = s + + action._mitogen_discovering_interpreter = False + action._mitogen_interpreter_candidates = None + return s + + +def parse_python_path(s, candidates, task_vars, action, rediscover_python): + """ + Given the string set for ansible_python_interpeter, parse it using shell + syntax and return an appropriate argument vector. If the value detected is + one of interpreter discovery then run that first. Caches python interpreter + discovery value in `facts_from_task_vars` like how Ansible handles this. + """ + if not s: + # if python_path doesn't exist, default to `auto` and attempt to discover it + s = 'auto' + + s = run_interpreter_discovery_if_necessary(s, candidates, task_vars, action, rediscover_python) + if not s: + s = _FALLBACK_INTERPRETER + + return ansible.utils.shlex.shlex_split(s) + + +def optional_secret(value): + """ + Wrap `value` in :class:`mitogen.core.Secret` if it is not :data:`None`, + otherwise return :data:`None`. + """ + if value is not None: + return mitogen.core.Secret(value) + + +def first_true(it, default=None): + """ + Return the first truthy element from `it`. + """ + for elem in it: + if elem: + return elem + return default + + +class Spec(with_metaclass(abc.ABCMeta, object)): + """ + A source for variables that comprise a connection configuration. + """ + + @abc.abstractmethod + def transport(self): + """ + The name of the Ansible plug-in implementing the connection. + """ + + @abc.abstractmethod + def inventory_name(self): + """ + The name of the target being connected to as it appears in Ansible's + inventory. + """ + + @abc.abstractmethod + def remote_addr(self): + """ + The network address of the target, or for container and other special + targets, some other unique identifier. + """ + + @abc.abstractmethod + def remote_user(self): + """ + The username of the login account on the target. + """ + + @abc.abstractmethod + def password(self): + """ + The password of the login account on the target. + """ + + @abc.abstractmethod + def become(self): + """ + :data:`True` if privilege escalation should be active. + """ + + @abc.abstractmethod + def become_flags(self): + """ + The command line arguments passed to the become executable. + """ + + @abc.abstractmethod + def become_method(self): + """ + The name of the Ansible become method to use. + """ + + @abc.abstractmethod + def become_user(self): + """ + The username of the target account for become. + """ + + @abc.abstractmethod + def become_pass(self): + """ + The password of the target account for become. + """ + + @abc.abstractmethod + def port(self): + """ + The port of the login service on the target machine. + """ + + @abc.abstractmethod + def python_path(self): + """ + Path to the Python interpreter on the target machine. + """ + + @abc.abstractmethod + def host_key_checking(self): + """ + Whether or not to check the keys of the target machine + """ + + @abc.abstractmethod + def private_key_file(self): + """ + Path to the SSH private key file to use to login. + """ + + @abc.abstractmethod + def ssh_executable(self): + """ + Path to the SSH executable. + """ + + @abc.abstractmethod + def timeout(self): + """ + The generic timeout for all connections. + """ + + @abc.abstractmethod + def ansible_ssh_timeout(self): + """ + The SSH-specific timeout for a connection. + """ + + @abc.abstractmethod + def ssh_args(self): + """ + The list of additional arguments that should be included in an SSH + invocation. + """ + + @abc.abstractmethod + def become_exe(self): + """ + The path to the executable implementing the become method on the remote + machine. + """ + + @abc.abstractmethod + def sudo_args(self): + """ + The list of additional arguments that should be included in a sudo + invocation. + """ + + @abc.abstractmethod + def mitogen_via(self): + """ + The value of the mitogen_via= variable for this connection. Indicates + the connection should be established via an intermediary. + """ + + @abc.abstractmethod + def mitogen_kind(self): + """ + The type of container to use with the "setns" transport. + """ + + @abc.abstractmethod + def mitogen_mask_remote_name(self): + """ + Specifies whether to set a fixed "remote_name" field. The remote_name + is the suffix of `argv[0]` for remote interpreters. By default it + includes identifying information from the local process, which may be + undesirable in some circumstances. + """ + + @abc.abstractmethod + def mitogen_buildah_path(self): + """ + The path to the "buildah" program for the 'buildah' transport. + """ + + @abc.abstractmethod + def mitogen_docker_path(self): + """ + The path to the "docker" program for the 'docker' transport. + """ + + @abc.abstractmethod + def mitogen_kubectl_path(self): + """ + The path to the "kubectl" program for the 'docker' transport. + """ + + @abc.abstractmethod + def mitogen_incus_path(self): + """ + The path to the "incus" program for the 'incus' transport. + """ + + @abc.abstractmethod + def mitogen_lxc_path(self): + """ + The path to the "lxc" program for the 'lxd' transport. + """ + + @abc.abstractmethod + def mitogen_lxc_attach_path(self): + """ + The path to the "lxc-attach" program for the 'lxc' transport. + """ + + @abc.abstractmethod + def mitogen_lxc_info_path(self): + """ + The path to the "lxc-info" program for the 'lxc' transport. + """ + + @abc.abstractmethod + def mitogen_machinectl_path(self): + """ + The path to the "machinectl" program for the 'setns' transport. + """ + + @abc.abstractmethod + def mitogen_podman_path(self): + """ + The path to the "podman" program for the 'podman' transport. + """ + + @abc.abstractmethod + def mitogen_ssh_keepalive_interval(self): + """ + The SSH ServerAliveInterval. + """ + + @abc.abstractmethod + def mitogen_ssh_keepalive_count(self): + """ + The SSH ServerAliveCount. + """ + + @abc.abstractmethod + def mitogen_ssh_debug_level(self): + """ + The SSH debug level. + """ + + @abc.abstractmethod + def mitogen_ssh_compression(self): + """ + Whether SSH compression is enabled. + """ + + @abc.abstractmethod + def extra_args(self): + """ + Connection-specific arguments. + """ + + @abc.abstractmethod + def ansible_doas_exe(self): + """ + Value of "ansible_doas_exe" variable. + """ + + @abc.abstractmethod + def verbosity(self): + """ + How verbose to make logging or diagnostics output. + """ + + +class PlayContextSpec(Spec): + """ + PlayContextSpec takes almost all its information as-is from Ansible's + PlayContext. It is used for normal connections and delegate_to connections, + and should always be accurate. + """ + def __init__(self, connection, play_context, transport, inventory_name): + self._connection = connection + self._play_context = play_context + self._transport = transport + self._inventory_name = inventory_name + self._task_vars = self._connection._get_task_vars() + # used to run interpreter discovery + self._action = connection._action + + def _become_option(self, name): + plugin = self._connection.become + try: + return plugin.get_option(name, self._task_vars, self._play_context) + except AttributeError: + # A few ansible_mitogen connection plugins look more like become + # plugins. They don't quite fit Ansible's plugin.get_option() API. + # https://github.com/mitogen-hq/mitogen/issues/1173 + fallback_plugins = {'mitogen_doas', 'mitogen_sudo', 'mitogen_su'} + if self._connection.transport not in fallback_plugins: + raise + + fallback_options = { + 'become_exe', + 'become_flags', + } + if name not in fallback_options: + raise + + LOG.info( + 'Used fallback=PlayContext.%s for plugin=%r, option=%r', + name, self._connection, name, + ) + return getattr(self._play_context, name) + + def _connection_option(self, name, fallback_attr=None): + try: + return self._connection.get_option(name, hostvars=self._task_vars) + except KeyError: + if fallback_attr is None: + fallback_attr = name + LOG.info( + 'Used fallback=PlayContext.%s for plugin=%r, option=%r', + fallback_attr, self._connection, name, + ) + return getattr(self._play_context, fallback_attr) + + def transport(self): + return self._transport + + def inventory_name(self): + return self._inventory_name + + def remote_addr(self): + return self._connection_option('host', fallback_attr='remote_addr') + + def remote_user(self): + return self._connection_option('remote_user') + + def become(self): + return self._connection.become + + def become_flags(self): + return self._become_option('become_flags') + + def become_method(self): + return self._connection.become.name + + def become_user(self): + return self._become_option('become_user') + + def become_pass(self): + return optional_secret(self._become_option('become_pass')) + + def password(self): + return optional_secret(self._connection_option('password')) + + def port(self): + return self._connection_option('port') + + def python_path(self, rediscover_python=False): + # See also + # - ansible_mitogen.connecton.Connection.get_task_var() + try: + delegated_vars = self._task_vars['ansible_delegated_vars'] + variables = delegated_vars[self._connection.delegate_to_hostname] + except KeyError: + variables = self._task_vars + + interpreter_python = C.config.get_config_value( + 'INTERPRETER_PYTHON', variables=variables, + ) + interpreter_python_fallback = C.config.get_config_value( + 'INTERPRETER_PYTHON_FALLBACK', variables=variables, + ) + + if '{{' in interpreter_python or '{%' in interpreter_python: + templar = self._connection.templar + interpreter_python = templar.template(interpreter_python) + + return parse_python_path( + interpreter_python, + candidates=interpreter_python_fallback, + task_vars=self._task_vars, + action=self._action, + rediscover_python=rediscover_python) + + def host_key_checking(self): + return self._connection_option('host_key_checking') + + def private_key_file(self): + return self._connection_option('private_key_file') + + def ssh_executable(self): + return self._connection_option('ssh_executable') + + def timeout(self): + return self._connection_option('timeout') + + def ansible_ssh_timeout(self): + return self.timeout() + + def ssh_args(self): + return [ + mitogen.core.to_text(term) + for s in ( + self._connection_option('ssh_args'), + self._connection_option('ssh_common_args'), + self._connection_option('ssh_extra_args'), + ) + for term in ansible.utils.shlex.shlex_split(s or '') + ] + + def become_exe(self): + return self._become_option('become_exe') + + def sudo_args(self): + return ansible.utils.shlex.shlex_split(self.become_flags() or '') + + def mitogen_via(self): + return self._connection.get_task_var('mitogen_via') + + def mitogen_kind(self): + return self._connection.get_task_var('mitogen_kind') + + def mitogen_mask_remote_name(self): + return self._connection.get_task_var('mitogen_mask_remote_name') + + def mitogen_buildah_path(self): + return self._connection.get_task_var('mitogen_buildah_path') + + def mitogen_docker_path(self): + return self._connection.get_task_var('mitogen_docker_path') + + def mitogen_kubectl_path(self): + return self._connection.get_task_var('mitogen_kubectl_path') + + def mitogen_incus_path(self): + return self._connection.get_task_var('mitogen_incus_path') + + def mitogen_lxc_path(self): + return self._connection.get_task_var('mitogen_lxc_path') + + def mitogen_lxc_attach_path(self): + return self._connection.get_task_var('mitogen_lxc_attach_path') + + def mitogen_lxc_info_path(self): + return self._connection.get_task_var('mitogen_lxc_info_path') + + def mitogen_podman_path(self): + return self._connection.get_task_var('mitogen_podman_path') + + def mitogen_ssh_keepalive_interval(self): + return self._connection.get_task_var('mitogen_ssh_keepalive_interval') + + def mitogen_ssh_keepalive_count(self): + return self._connection.get_task_var('mitogen_ssh_keepalive_count') + + def mitogen_machinectl_path(self): + return self._connection.get_task_var('mitogen_machinectl_path') + + def mitogen_ssh_debug_level(self): + return self._connection.get_task_var('mitogen_ssh_debug_level') + + def mitogen_ssh_compression(self): + return self._connection.get_task_var('mitogen_ssh_compression') + + def extra_args(self): + return self._connection.get_extra_args() + + def ansible_doas_exe(self): + return ( + self._connection.get_task_var('ansible_doas_exe') or + os.environ.get('ANSIBLE_DOAS_EXE') + ) + + def verbosity(self): + try: + verbosity = self._connection.get_option('verbosity', hostvars=self._task_vars) + except KeyError: + verbosity = self.mitogen_ssh_debug_level() + + if verbosity: + return int(verbosity) + + return 0 + + +class MitogenViaSpec(Spec): + """ + MitogenViaSpec takes most of its information from the HostVars of the + running task. HostVars is a lightweight wrapper around VariableManager, so + it is better to say that VariableManager.get_vars() is the ultimate source + of MitogenViaSpec's information. + + Due to this, mitogen_via= hosts must have all their configuration + information represented as host and group variables. We cannot use any + per-task configuration, as all that data belongs to the real target host. + + Ansible uses all kinds of strange historical logic for calculating + variables, including making their precedence configurable. MitogenViaSpec + must ultimately reimplement all of that logic. It is likely that if you are + having a configruation problem with connection delegation, the answer to + your problem lies in the method implementations below! + """ + def __init__(self, inventory_name, host_vars, task_vars, become_method, become_user, + play_context, action): + """ + :param str inventory_name: + The inventory name of the intermediary machine, i.e. not the target + machine. + :param dict host_vars: + The HostVars magic dictionary provided by Ansible in task_vars. + :param dict task_vars: + Task vars provided by Ansible. + :param str become_method: + If the mitogen_via= spec included a become method, the method it + specifies. + :param str become_user: + If the mitogen_via= spec included a become user, the user it + specifies. + :param PlayContext play_context: + For some global values **only**, the PlayContext used to describe + the real target machine. Values from this object are **strictly + restricted** to values that are Ansible-global, e.g. the passwords + specified interactively. + :param ActionModuleMixin action: + Backref to the ActionModuleMixin required for ansible interpreter discovery + """ + self._inventory_name = inventory_name + self._host_vars = host_vars + self._task_vars = task_vars + self._become_method = become_method + self._become_user = become_user + # Dangerous! You may find a variable you want in this object, but it's + # almost certainly for the wrong machine! + self._dangerous_play_context = play_context + self._action = action + + def transport(self): + return ( + self._host_vars.get('ansible_connection') or + C.DEFAULT_TRANSPORT + ) + + def inventory_name(self): + return self._inventory_name + + def remote_addr(self): + # play_context.py::MAGIC_VARIABLE_MAPPING + return ( + self._host_vars.get('ansible_ssh_host') or + self._host_vars.get('ansible_host') or + self._inventory_name + ) + + def remote_user(self): + return ( + self._host_vars.get('ansible_ssh_user') or + self._host_vars.get('ansible_user') or + C.DEFAULT_REMOTE_USER + ) + + def become(self): + return bool(self._become_user) + + def become_flags(self): + return self._host_vars.get('ansible_become_flags') + + def become_method(self): + return ( + self._become_method or + self._host_vars.get('ansible_become_method') or + C.DEFAULT_BECOME_METHOD + ) + + def become_user(self): + return self._become_user + + def become_pass(self): + return optional_secret( + self._host_vars.get('ansible_become_pass') or + self._host_vars.get('ansible_become_password') + ) + + def password(self): + return optional_secret( + self._host_vars.get('ansible_ssh_password') or + self._host_vars.get('ansible_ssh_pass') or + self._host_vars.get('ansible_password') + ) + + def port(self): + return ( + self._host_vars.get('ansible_ssh_port') or + self._host_vars.get('ansible_port') or + C.DEFAULT_REMOTE_PORT + ) + + def python_path(self, rediscover_python=False): + s = self._host_vars.get('ansible_python_interpreter') + interpreter_python_fallback = self._host_vars.get( + 'ansible_interpreter_python_fallback', [], + ) + return parse_python_path( + s, + candidates=interpreter_python_fallback, + task_vars=self._task_vars, + action=self._action, + rediscover_python=rediscover_python) + + def host_key_checking(self): + def candidates(): + yield self._host_vars.get('ansible_ssh_host_key_checking') + yield self._host_vars.get('ansible_host_key_checking') + yield C.HOST_KEY_CHECKING + val = next((v for v in candidates() if v is not None), True) + return boolean(val) + + def private_key_file(self): + # TODO: must come from PlayContext too. + return ( + self._host_vars.get('ansible_ssh_private_key_file') or + self._host_vars.get('ansible_private_key_file') or + C.DEFAULT_PRIVATE_KEY_FILE + ) + + def ssh_executable(self): + return C.config.get_config_value("ssh_executable", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {})) + + def timeout(self): + # TODO: must come from PlayContext too. + return C.DEFAULT_TIMEOUT + + def ansible_ssh_timeout(self): + return ( + self._host_vars.get('ansible_timeout') or + self._host_vars.get('ansible_ssh_timeout') or + self.timeout() + ) + + def ssh_args(self): + local_vars = self._task_vars.get("hostvars", {}).get(self._inventory_name, {}) + return [ + mitogen.core.to_text(term) + for s in ( + C.config.get_config_value("ssh_args", plugin_type="connection", plugin_name="ssh", variables=local_vars), + C.config.get_config_value("ssh_common_args", plugin_type="connection", plugin_name="ssh", variables=local_vars), + C.config.get_config_value("ssh_extra_args", plugin_type="connection", plugin_name="ssh", variables=local_vars) + ) + for term in ansible.utils.shlex.shlex_split(s) + if s + ] + + def become_exe(self): + return ( + self._host_vars.get('ansible_become_exe') or + C.DEFAULT_BECOME_EXE + ) + + def sudo_args(self): + return [ + mitogen.core.to_text(term) + for s in ( + self._host_vars.get('ansible_sudo_flags') or '', + self.become_flags() or '', + ) + for term in ansible.utils.shlex.shlex_split(s) + ] + + def mitogen_via(self): + return self._host_vars.get('mitogen_via') + + def mitogen_kind(self): + return self._host_vars.get('mitogen_kind') + + def mitogen_mask_remote_name(self): + return self._host_vars.get('mitogen_mask_remote_name') + + def mitogen_buildah_path(self): + return self._host_vars.get('mitogen_buildah_path') + + def mitogen_docker_path(self): + return self._host_vars.get('mitogen_docker_path') + + def mitogen_kubectl_path(self): + return self._host_vars.get('mitogen_kubectl_path') + + def mitogen_incus_path(self): + return self._host_vars.get('mitogen_incus_path') + + def mitogen_lxc_path(self): + return self._host_vars.get('mitogen_lxc_path') + + def mitogen_lxc_attach_path(self): + return self._host_vars.get('mitogen_lxc_attach_path') + + def mitogen_lxc_info_path(self): + return self._host_vars.get('mitogen_lxc_info_path') + + def mitogen_podman_path(self): + return self._host_vars.get('mitogen_podman_path') + + def mitogen_ssh_keepalive_interval(self): + return self._host_vars.get('mitogen_ssh_keepalive_interval') + + def mitogen_ssh_keepalive_count(self): + return self._host_vars.get('mitogen_ssh_keepalive_count') + + def mitogen_machinectl_path(self): + return self._host_vars.get('mitogen_machinectl_path') + + def mitogen_ssh_debug_level(self): + return self._host_vars.get('mitogen_ssh_debug_level') + + def mitogen_ssh_compression(self): + return self._host_vars.get('mitogen_ssh_compression') + + def extra_args(self): + return [] # TODO + + def ansible_doas_exe(self): + return ( + self._host_vars.get('ansible_doas_exe') or + os.environ.get('ANSIBLE_DOAS_EXE') + ) + + def verbosity(self): + verbosity = self._host_vars.get('ansible_ssh_verbosity') + if verbosity is None: + verbosity = self.mitogen_ssh_debug_level() + + if verbosity: + return int(verbosity) + + return 0 diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/utils/__init__.py b/plugins/mitogen-0.3.44/ansible_mitogen/utils/__init__.py new file mode 100644 index 0000000..a01b261 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/utils/__init__.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re + +import ansible + +__all__ = [ + 'ansible_version', +] + + +def _parse(v_string): + # Adapted from distutils.version.LooseVersion.parse() + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + for component in component_re.split(v_string): + if not component or component == '.': + continue + try: + yield int(component) + except ValueError: + yield component + + +ansible_version = tuple(_parse(ansible.__version__)) + +del _parse +del re +del ansible diff --git a/plugins/mitogen-0.3.44/ansible_mitogen/utils/unsafe.py b/plugins/mitogen-0.3.44/ansible_mitogen/utils/unsafe.py new file mode 100644 index 0000000..a3aed46 --- /dev/null +++ b/plugins/mitogen-0.3.44/ansible_mitogen/utils/unsafe.py @@ -0,0 +1,123 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import ansible +import ansible.utils.unsafe_proxy + +import ansible_mitogen.utils + +import mitogen +import mitogen.core +import mitogen.utils + +__all__ = [ + 'cast', +] + +def _cast_to_dict(obj): return {cast(k): cast(v) for k, v in obj.items()} +def _cast_to_list(obj): return [cast(v) for v in obj] +def _cast_to_set(obj): return set(cast(v) for v in obj) +def _cast_to_tuple(obj): return tuple(cast(v) for v in obj) +def _cast_unsafe(obj): return obj._strip_unsafe() +def _passthrough(obj): return obj +def _untag(obj): return obj._native_copy() + + +# A dispatch table to cast objects based on their exact type. +# This is an optimisation, reliable fallbacks are required (e.g. isinstance()) +_CAST_DISPATCH = { + bytes: bytes, + dict: _cast_to_dict, + list: _cast_to_list, + mitogen.core.UnicodeType: mitogen.core.UnicodeType, +} +_CAST_DISPATCH.update({t: _passthrough for t in mitogen.utils.PASSTHROUGH}) + +_CAST_SUBTYPES = [ + dict, + list, +] + +if hasattr(ansible.utils.unsafe_proxy, 'TrustedAsTemplate'): + import datetime + import ansible.module_utils._internal._datatag + _CAST_DISPATCH.update({ + set: _cast_to_set, + tuple: _cast_to_tuple, + ansible.module_utils._internal._datatag._AnsibleTaggedBytes: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDate: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDateTime: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDict: _cast_to_dict, + ansible.module_utils._internal._datatag._AnsibleTaggedFloat: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedInt: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedList: _cast_to_list, + ansible.module_utils._internal._datatag._AnsibleTaggedSet: _cast_to_set, + ansible.module_utils._internal._datatag._AnsibleTaggedStr: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedTime: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedTuple: _cast_to_tuple, + ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes, + ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType, + datetime.date: _passthrough, + datetime.datetime: _passthrough, + datetime.time: _passthrough, + }) + _CAST_SUBTYPES.extend([ + set, + tuple, + ]) +elif hasattr(ansible.utils.unsafe_proxy.AnsibleUnsafeText, '_strip_unsafe'): + _CAST_DISPATCH.update({ + tuple: _cast_to_list, + ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: _cast_unsafe, + ansible.utils.unsafe_proxy.AnsibleUnsafeText: _cast_unsafe, + ansible.utils.unsafe_proxy.NativeJinjaUnsafeText: _cast_unsafe, + }) + _CAST_SUBTYPES.extend([ + tuple, + ]) +elif ansible_mitogen.utils.ansible_version[:2] <= (2, 16): + _CAST_DISPATCH.update({ + tuple: _cast_to_list, + ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes, + ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType, + }) + _CAST_SUBTYPES.extend([ + tuple, + ]) +else: + mitogen_ver = '.'.join(str(v) for v in mitogen.__version__) + raise ImportError("Mitogen %s can't cast Ansible %s objects" + % (mitogen_ver, ansible.__version__)) + + +def cast(obj): + """ + Return obj (or a copy) with subtypes of builtins cast to their supertype. + + This is an enhanced version of :func:`mitogen.utils.cast`. In addition it + handles ``ansible.utils.unsafe_proxy.AnsibleUnsafeText`` and variants. + + There are types handled by :func:`ansible.utils.unsafe_proxy.wrap_var()` + that this function currently does not handle (e.g. `set()`), or preserve + preserve (e.g. `tuple()`). Future enhancements may change this. + + :param obj: + Object to undecorate. + :returns: + Undecorated object. + """ + # Fast path: obj is a known type, dispatch directly + try: + unwrapper = _CAST_DISPATCH[type(obj)] + except KeyError: + pass + else: + return unwrapper(obj) + + # Slow path: obj is some unknown subclass + for typ_ in _CAST_SUBTYPES: + if isinstance(obj, typ_): + unwrapper = _CAST_DISPATCH[typ_] + return unwrapper(obj) + + return mitogen.utils.cast(obj) diff --git a/plugins/mitogen-0.3.44/mitogen/__init__.py b/plugins/mitogen-0.3.44/mitogen/__init__.py new file mode 100644 index 0000000..8ed95e1 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/__init__.py @@ -0,0 +1,121 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +On the Mitogen master, this is imported from ``mitogen/__init__.py`` as would +be expected. On the slave, it is built dynamically during startup. +""" + + +#: Library version as a tuple. +__version__ = (0, 3, 44) + + +#: This is :data:`False` in slave contexts. Previously it was used to prevent +#: re-execution of :mod:`__main__` in single file programs, however that now +#: happens automatically. +is_master = True + + +#: This is `0` in a master, otherwise it is the master-assigned ID unique to +#: the slave context used for message routing. +context_id = 0 + + +#: This is :data:`None` in a master, otherwise it is the master-assigned ID +#: unique to the slave's parent context. +parent_id = None + + +#: This is an empty list in a master, otherwise it is a list of parent context +#: IDs ordered from most direct to least direct. +parent_ids = [] + + +import os +_default_profiling = os.environ.get('MITOGEN_PROFILING') is not None +del os + + +def main(log_level='INFO', profiling=_default_profiling): + """ + Convenience decorator primarily useful for writing discardable test + scripts. + + In the master process, when `func` is defined in the :mod:`__main__` + module, arranges for `func(router)` to be invoked immediately, with + :py:class:`mitogen.master.Router` construction and destruction handled just + as in :py:func:`mitogen.utils.run_with_router`. In slaves, this function + does nothing. + + :param str log_level: + Logging package level to configure via + :py:func:`mitogen.utils.log_to_file`. + + :param bool profiling: + If :py:data:`True`, equivalent to setting + :py:attr:`mitogen.master.Router.profiling` prior to router + construction. This causes ``/tmp`` files to be created everywhere at + the end of a successful run with :py:mod:`cProfile` output for every + thread. + + Example: + + :: + + import mitogen + import requests + + def get_url(url): + return requests.get(url).text + + @mitogen.main() + def main(router): + z = router.ssh(hostname='k3') + print(z.call(get_url, 'https://example.org/'))))) + + """ + + def wrapper(func): + if func.__module__ != '__main__': + return func + import mitogen.core + import mitogen.master + import mitogen.utils + if profiling: + mitogen.core.enable_profiling() + mitogen.master.Router.profiling = profiling + mitogen.utils.log_to_file(level=log_level) + return mitogen.core._profile_hook( + 'app.main', + mitogen.utils.run_with_router, + func, + ) + return wrapper diff --git a/plugins/mitogen-0.3.44/mitogen/buildah.py b/plugins/mitogen-0.3.44/mitogen/buildah.py new file mode 100644 index 0000000..7a1e3f8 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/buildah.py @@ -0,0 +1,72 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import logging + +import mitogen.parent + + +LOG = logging.getLogger(__name__) + + +class Options(mitogen.parent.Options): + container = None + username = None + buildah_path = 'buildah' + + def __init__(self, container=None, buildah_path=None, username=None, + **kwargs): + super(Options, self).__init__(**kwargs) + assert container is not None + self.container = container + if buildah_path: + self.buildah_path = buildah_path + if username: + self.username = username + + +class Connection(mitogen.parent.Connection): + options_class = Options + child_is_immediate_subprocess = False + + # TODO: better way of capturing errors such as "No such container." + create_child_args = { + 'merge_stdio': True + } + + def _get_name(self): + return u'buildah.' + self.options.container + + def get_boot_command(self): + args = [self.options.buildah_path, 'run'] + if self.options.username: + args += ['--user=' + self.options.username] + args += ['--', self.options.container] + return args + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/compat/__init__.py b/plugins/mitogen-0.3.44/mitogen/compat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/mitogen-0.3.44/mitogen/compat/pkgutil.py b/plugins/mitogen-0.3.44/mitogen/compat/pkgutil.py new file mode 100644 index 0000000..15eb2af --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/compat/pkgutil.py @@ -0,0 +1,594 @@ +"""Utilities to support packages.""" + +# !mitogen: minify_safe + +# NOTE: This module must remain compatible with Python 2.3, as it is shared +# by setuptools for distribution with Python 2.3 and up. + +import os +import sys +import imp +import os.path +from types import ModuleType + +__all__ = [ + 'get_importer', 'iter_importers', 'get_loader', 'find_loader', + 'walk_packages', 'iter_modules', 'get_data', + 'ImpImporter', 'ImpLoader', 'read_code', 'extend_path', +] + +def read_code(stream): + # This helper is needed in order for the PEP 302 emulation to + # correctly handle compiled files + import marshal + + magic = stream.read(4) + if magic != imp.get_magic(): + return None + + stream.read(4) # Skip timestamp + return marshal.load(stream) + + +def simplegeneric(func): + """Make a trivial single-dispatch generic function""" + registry = {} + def wrapper(*args, **kw): + ob = args[0] + try: + cls = ob.__class__ + except AttributeError: + cls = type(ob) + try: + mro = cls.__mro__ + except AttributeError: + try: + class cls(cls, object): + pass + mro = cls.__mro__[1:] + except TypeError: + mro = object, # must be an ExtensionClass or some such :( + for t in mro: + if t in registry: + return registry[t](*args, **kw) + else: + return func(*args, **kw) + try: + wrapper.__name__ = func.__name__ + except (TypeError, AttributeError): + pass # Python 2.3 doesn't allow functions to be renamed + + def register(typ, func=None): + if func is None: + return lambda f: register(typ, f) + registry[typ] = func + return func + + wrapper.__dict__ = func.__dict__ + wrapper.__doc__ = func.__doc__ + wrapper.register = register + return wrapper + + +def walk_packages(path=None, prefix='', onerror=None): + """Yields (module_loader, name, ispkg) for all modules recursively + on path, or, if path is None, all accessible modules. + + 'path' should be either None or a list of paths to look for + modules in. + + 'prefix' is a string to output on the front of every module name + on output. + + Note that this function must import all *packages* (NOT all + modules!) on the given path, in order to access the __path__ + attribute to find submodules. + + 'onerror' is a function which gets called with one argument (the + name of the package which was being imported) if any exception + occurs while trying to import a package. If no onerror function is + supplied, ImportErrors are caught and ignored, while all other + exceptions are propagated, terminating the search. + + Examples: + + # list all modules python can access + walk_packages() + + # list all submodules of ctypes + walk_packages(ctypes.__path__, ctypes.__name__+'.') + """ + + def seen(p, m={}): + if p in m: + return True + m[p] = True + + for importer, name, ispkg in iter_modules(path, prefix): + yield importer, name, ispkg + + if ispkg: + try: + __import__(name) + except ImportError: + if onerror is not None: + onerror(name) + except Exception: + if onerror is not None: + onerror(name) + else: + raise + else: + path = getattr(sys.modules[name], '__path__', None) or [] + + # don't traverse path items we've seen before + path = [p for p in path if not seen(p)] + + for item in walk_packages(path, name+'.', onerror): + yield item + + +def iter_modules(path=None, prefix=''): + """Yields (module_loader, name, ispkg) for all submodules on path, + or, if path is None, all top-level modules on sys.path. + + 'path' should be either None or a list of paths to look for + modules in. + + 'prefix' is a string to output on the front of every module name + on output. + """ + + if path is None: + importers = iter_importers() + else: + importers = map(get_importer, path) + + yielded = {} + for i in importers: + for name, ispkg in iter_importer_modules(i, prefix): + if name not in yielded: + yielded[name] = 1 + yield i, name, ispkg + + +#@simplegeneric +def iter_importer_modules(importer, prefix=''): + if not hasattr(importer, 'iter_modules'): + return [] + return importer.iter_modules(prefix) + +iter_importer_modules = simplegeneric(iter_importer_modules) + + +class ImpImporter: + """PEP 302 Importer that wraps Python's "classic" import algorithm + + ImpImporter(dirname) produces a PEP 302 importer that searches that + directory. ImpImporter(None) produces a PEP 302 importer that searches + the current sys.path, plus any modules that are frozen or built-in. + + Note that ImpImporter does not currently support being used by placement + on sys.meta_path. + """ + + def __init__(self, path=None): + self.path = path + + def find_module(self, fullname, path=None): + # Note: we ignore 'path' argument since it is only used via meta_path + subname = fullname.split(".")[-1] + if subname != fullname and self.path is None: + return None + if self.path is None: + path = None + else: + path = [os.path.realpath(self.path)] + try: + file, filename, etc = imp.find_module(subname, path) + except ImportError: + return None + return ImpLoader(fullname, file, filename, etc) + + def iter_modules(self, prefix=''): + if self.path is None or not os.path.isdir(self.path): + return + + yielded = {} + import inspect + try: + filenames = os.listdir(self.path) + except OSError: + # ignore unreadable directories like import does + filenames = [] + filenames.sort() # handle packages before same-named modules + + for fn in filenames: + modname = inspect.getmodulename(fn) + if modname=='__init__' or modname in yielded: + continue + + path = os.path.join(self.path, fn) + ispkg = False + + if not modname and os.path.isdir(path) and '.' not in fn: + modname = fn + try: + dircontents = os.listdir(path) + except OSError: + # ignore unreadable directories like import does + dircontents = [] + for fn in dircontents: + subname = inspect.getmodulename(fn) + if subname=='__init__': + ispkg = True + break + else: + continue # not a package + + if modname and '.' not in modname: + yielded[modname] = 1 + yield prefix + modname, ispkg + + +class ImpLoader: + """PEP 302 Loader that wraps Python's "classic" import algorithm + """ + code = source = None + + def __init__(self, fullname, file, filename, etc): + self.file = file + self.filename = filename + self.fullname = fullname + self.etc = etc + + def load_module(self, fullname): + self._reopen() + try: + mod = imp.load_module(fullname, self.file, self.filename, self.etc) + finally: + if self.file: + self.file.close() + # Note: we don't set __loader__ because we want the module to look + # normal; i.e. this is just a wrapper for standard import machinery + return mod + + def get_data(self, pathname): + return open(pathname, "rb").read() + + def _reopen(self): + if self.file and self.file.closed: + mod_type = self.etc[2] + if mod_type==imp.PY_SOURCE: + self.file = open(self.filename, 'rU') + elif mod_type in (imp.PY_COMPILED, imp.C_EXTENSION): + self.file = open(self.filename, 'rb') + + def _fix_name(self, fullname): + if fullname is None: + fullname = self.fullname + elif fullname != self.fullname: + raise ImportError("Loader for module %s cannot handle " + "module %s" % (self.fullname, fullname)) + return fullname + + def is_package(self, fullname): + fullname = self._fix_name(fullname) + return self.etc[2]==imp.PKG_DIRECTORY + + def get_code(self, fullname=None): + fullname = self._fix_name(fullname) + if self.code is None: + mod_type = self.etc[2] + if mod_type==imp.PY_SOURCE: + source = self.get_source(fullname) + self.code = compile(source, self.filename, 'exec') + elif mod_type==imp.PY_COMPILED: + self._reopen() + try: + self.code = read_code(self.file) + finally: + self.file.close() + elif mod_type==imp.PKG_DIRECTORY: + self.code = self._get_delegate().get_code() + return self.code + + def get_source(self, fullname=None): + fullname = self._fix_name(fullname) + if self.source is None: + mod_type = self.etc[2] + if mod_type==imp.PY_SOURCE: + self._reopen() + try: + self.source = self.file.read() + finally: + self.file.close() + elif mod_type==imp.PY_COMPILED: + if os.path.exists(self.filename[:-1]): + f = open(self.filename[:-1], 'rU') + self.source = f.read() + f.close() + elif mod_type==imp.PKG_DIRECTORY: + self.source = self._get_delegate().get_source() + return self.source + + + def _get_delegate(self): + return ImpImporter(self.filename).find_module('__init__') + + def get_filename(self, fullname=None): + fullname = self._fix_name(fullname) + mod_type = self.etc[2] + if self.etc[2]==imp.PKG_DIRECTORY: + return self._get_delegate().get_filename() + elif self.etc[2] in (imp.PY_SOURCE, imp.PY_COMPILED, imp.C_EXTENSION): + return self.filename + return None + + +try: + import zipimport + from zipimport import zipimporter + + def iter_zipimport_modules(importer, prefix=''): + dirlist = zipimport._zip_directory_cache[importer.archive].keys() + dirlist.sort() + _prefix = importer.prefix + plen = len(_prefix) + yielded = {} + import inspect + for fn in dirlist: + if not fn.startswith(_prefix): + continue + + fn = fn[plen:].split(os.sep) + + if len(fn)==2 and fn[1].startswith('__init__.py'): + if fn[0] not in yielded: + yielded[fn[0]] = 1 + yield fn[0], True + + if len(fn)!=1: + continue + + modname = inspect.getmodulename(fn[0]) + if modname=='__init__': + continue + + if modname and '.' not in modname and modname not in yielded: + yielded[modname] = 1 + yield prefix + modname, False + + iter_importer_modules.register(zipimporter, iter_zipimport_modules) + +except ImportError: + pass + + +def get_importer(path_item): + """Retrieve a PEP 302 importer for the given path item + + The returned importer is cached in sys.path_importer_cache + if it was newly created by a path hook. + + If there is no importer, a wrapper around the basic import + machinery is returned. This wrapper is never inserted into + the importer cache (None is inserted instead). + + The cache (or part of it) can be cleared manually if a + rescan of sys.path_hooks is necessary. + """ + try: + importer = sys.path_importer_cache[path_item] + except KeyError: + for path_hook in sys.path_hooks: + try: + importer = path_hook(path_item) + break + except ImportError: + pass + else: + importer = None + sys.path_importer_cache.setdefault(path_item, importer) + + if importer is None: + try: + importer = ImpImporter(path_item) + except ImportError: + importer = None + return importer + + +def iter_importers(fullname=""): + """Yield PEP 302 importers for the given module name + + If fullname contains a '.', the importers will be for the package + containing fullname, otherwise they will be importers for sys.meta_path, + sys.path, and Python's "classic" import machinery, in that order. If + the named module is in a package, that package is imported as a side + effect of invoking this function. + + Non PEP 302 mechanisms (e.g. the Windows registry) used by the + standard import machinery to find files in alternative locations + are partially supported, but are searched AFTER sys.path. Normally, + these locations are searched BEFORE sys.path, preventing sys.path + entries from shadowing them. + + For this to cause a visible difference in behaviour, there must + be a module or package name that is accessible via both sys.path + and one of the non PEP 302 file system mechanisms. In this case, + the emulation will find the former version, while the builtin + import mechanism will find the latter. + + Items of the following types can be affected by this discrepancy: + imp.C_EXTENSION, imp.PY_SOURCE, imp.PY_COMPILED, imp.PKG_DIRECTORY + """ + if fullname.startswith('.'): + raise ImportError("Relative module names not supported") + if '.' in fullname: + # Get the containing package's __path__ + pkg = '.'.join(fullname.split('.')[:-1]) + if pkg not in sys.modules: + __import__(pkg) + path = getattr(sys.modules[pkg], '__path__', None) or [] + else: + for importer in sys.meta_path: + yield importer + path = sys.path + for item in path: + yield get_importer(item) + if '.' not in fullname: + yield ImpImporter() + +def get_loader(module_or_name): + """Get a PEP 302 "loader" object for module_or_name + + If the module or package is accessible via the normal import + mechanism, a wrapper around the relevant part of that machinery + is returned. Returns None if the module cannot be found or imported. + If the named module is not already imported, its containing package + (if any) is imported, in order to establish the package __path__. + + This function uses iter_importers(), and is thus subject to the same + limitations regarding platform-specific special import locations such + as the Windows registry. + """ + if module_or_name in sys.modules: + module_or_name = sys.modules[module_or_name] + if isinstance(module_or_name, ModuleType): + module = module_or_name + loader = getattr(module, '__loader__', None) + if loader is not None: + return loader + fullname = module.__name__ + else: + fullname = module_or_name + return find_loader(fullname) + +def find_loader(fullname): + """Find a PEP 302 "loader" object for fullname + + If fullname contains dots, path must be the containing package's __path__. + Returns None if the module cannot be found or imported. This function uses + iter_importers(), and is thus subject to the same limitations regarding + platform-specific special import locations such as the Windows registry. + """ + for importer in iter_importers(fullname): + loader = importer.find_module(fullname) + if loader is not None: + return loader + + return None + + +def extend_path(path, name): + """Extend a package's path. + + Intended use is to place the following code in a package's __init__.py: + + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) + + This will add to the package's __path__ all subdirectories of + directories on sys.path named after the package. This is useful + if one wants to distribute different parts of a single logical + package as multiple directories. + + It also looks for *.pkg files beginning where * matches the name + argument. This feature is similar to *.pth files (see site.py), + except that it doesn't special-case lines starting with 'import'. + A *.pkg file is trusted at face value: apart from checking for + duplicates, all entries found in a *.pkg file are added to the + path, regardless of whether they are exist the filesystem. (This + is a feature.) + + If the input path is not a list (as is the case for frozen + packages) it is returned unchanged. The input path is not + modified; an extended copy is returned. Items are only appended + to the copy at the end. + + It is assumed that sys.path is a sequence. Items of sys.path that + are not (unicode or 8-bit) strings referring to existing + directories are ignored. Unicode items of sys.path that cause + errors when used as filenames may cause this function to raise an + exception (in line with os.path.isdir() behavior). + """ + + if not isinstance(path, list): + # This could happen e.g. when this is called from inside a + # frozen package. Return the path unchanged in that case. + return path + + pname = os.path.join(*name.split('.')) # Reconstitute as relative path + # Just in case os.extsep != '.' + sname = os.extsep.join(name.split('.')) + sname_pkg = sname + os.extsep + "pkg" + init_py = "__init__" + os.extsep + "py" + + path = path[:] # Start with a copy of the existing path + + for dir in sys.path: + if not isinstance(dir, basestring) or not os.path.isdir(dir): + continue + subdir = os.path.join(dir, pname) + # XXX This may still add duplicate entries to path on + # case-insensitive filesystems + initfile = os.path.join(subdir, init_py) + if subdir not in path and os.path.isfile(initfile): + path.append(subdir) + # XXX Is this the right thing for subpackages like zope.app? + # It looks for a file named "zope.app.pkg" + pkgfile = os.path.join(dir, sname_pkg) + if os.path.isfile(pkgfile): + try: + f = open(pkgfile) + except IOError: + msg = sys.exc_info()[1] + sys.stderr.write("Can't open %s: %s\n" % + (pkgfile, msg)) + else: + for line in f: + line = line.rstrip('\n') + if not line or line.startswith('#'): + continue + path.append(line) # Don't check for existence! + f.close() + + return path + +def get_data(package, resource): + """Get a resource from a package. + + This is a wrapper round the PEP 302 loader get_data API. The package + argument should be the name of a package, in standard module format + (foo.bar). The resource argument should be in the form of a relative + filename, using '/' as the path separator. The parent directory name '..' + is not allowed, and nor is a rooted name (starting with a '/'). + + The function returns a binary string, which is the contents of the + specified resource. + + For packages located in the filesystem, which have already been imported, + this is the rough equivalent of + + d = os.path.dirname(sys.modules[package].__file__) + data = open(os.path.join(d, resource), 'rb').read() + + If the package cannot be located or loaded, or it uses a PEP 302 loader + which does not support get_data(), then None is returned. + """ + + loader = get_loader(package) + if loader is None or not hasattr(loader, 'get_data'): + return None + mod = sys.modules.get(package) or loader.load_module(package) + if mod is None or not hasattr(mod, '__file__'): + return None + + # Modify the resource name to be compatible with the loader.get_data + # signature - an os.path format "filename" starting with the dirname of + # the package's __file__ + parts = resource.split('/') + parts.insert(0, os.path.dirname(mod.__file__)) + resource_name = os.path.join(*parts) + return loader.get_data(resource_name) diff --git a/plugins/mitogen-0.3.44/mitogen/compat/tokenize.py b/plugins/mitogen-0.3.44/mitogen/compat/tokenize.py new file mode 100644 index 0000000..0473c6a --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/compat/tokenize.py @@ -0,0 +1,453 @@ +"""Tokenization help for Python programs. + +generate_tokens(readline) is a generator that breaks a stream of +text into Python tokens. It accepts a readline-like method which is called +repeatedly to get the next line of input (or "" for EOF). It generates +5-tuples with these members: + + the token type (see token.py) + the token (a string) + the starting (row, column) indices of the token (a 2-tuple of ints) + the ending (row, column) indices of the token (a 2-tuple of ints) + the original line (string) + +It is designed to match the working of the Python tokenizer exactly, except +that it produces COMMENT tokens for comments and gives type OP for all +operators + +Older entry points + tokenize_loop(readline, tokeneater) + tokenize(readline, tokeneater=printtoken) +are the same, except instead of generating tokens, tokeneater is a callback +function to which the 5 fields described above are passed as 5 arguments, +each time a new token is found.""" + +# !mitogen: minify_safe + +__author__ = 'Ka-Ping Yee ' +__credits__ = ('GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, ' + 'Skip Montanaro, Raymond Hettinger') + +from itertools import chain +import string, re +from token import * + +import token +__all__ = [x for x in dir(token) if not x.startswith("_")] +__all__ += ["COMMENT", "tokenize", "generate_tokens", "NL", "untokenize"] +del token + +COMMENT = N_TOKENS +tok_name[COMMENT] = 'COMMENT' +NL = N_TOKENS + 1 +tok_name[NL] = 'NL' +N_TOKENS += 2 + +def group(*choices): return '(' + '|'.join(choices) + ')' +def any(*choices): return group(*choices) + '*' +def maybe(*choices): return group(*choices) + '?' + +Whitespace = r'[ \f\t]*' +Comment = r'#[^\r\n]*' +Ignore = Whitespace + any(r'\\\r?\n' + Whitespace) + maybe(Comment) +Name = r'[a-zA-Z_]\w*' + +Hexnumber = r'0[xX][\da-fA-F]+[lL]?' +Octnumber = r'(0[oO][0-7]+)|(0[0-7]*)[lL]?' +Binnumber = r'0[bB][01]+[lL]?' +Decnumber = r'[1-9]\d*[lL]?' +Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber) +Exponent = r'[eE][-+]?\d+' +Pointfloat = group(r'\d+\.\d*', r'\.\d+') + maybe(Exponent) +Expfloat = r'\d+' + Exponent +Floatnumber = group(Pointfloat, Expfloat) +Imagnumber = group(r'\d+[jJ]', Floatnumber + r'[jJ]') +Number = group(Imagnumber, Floatnumber, Intnumber) + +# Tail end of ' string. +Single = r"[^'\\]*(?:\\.[^'\\]*)*'" +# Tail end of " string. +Double = r'[^"\\]*(?:\\.[^"\\]*)*"' +# Tail end of ''' string. +Single3 = r"[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*'''" +# Tail end of """ string. +Double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""' +Triple = group("[uUbB]?[rR]?'''", '[uUbB]?[rR]?"""') +# Single-line ' or " string. +String = group(r"[uUbB]?[rR]?'[^\n'\\]*(?:\\.[^\n'\\]*)*'", + r'[uUbB]?[rR]?"[^\n"\\]*(?:\\.[^\n"\\]*)*"') + +# Because of leftmost-then-longest match semantics, be sure to put the +# longest operators first (e.g., if = came before ==, == would get +# recognized as two instances of =). +Operator = group(r"\*\*=?", r">>=?", r"<<=?", r"<>", r"!=", + r"//=?", + r"[+\-*/%&|^=<>]=?", + r"~") + +Bracket = '[][(){}]' +Special = group(r'\r?\n', r'[:;.,`@]') +Funny = group(Operator, Bracket, Special) + +PlainToken = group(Number, Funny, String, Name) +Token = Ignore + PlainToken + +# First (or only) line of ' or " string. +ContStr = group(r"[uUbB]?[rR]?'[^\n'\\]*(?:\\.[^\n'\\]*)*" + + group("'", r'\\\r?\n'), + r'[uUbB]?[rR]?"[^\n"\\]*(?:\\.[^\n"\\]*)*' + + group('"', r'\\\r?\n')) +PseudoExtras = group(r'\\\r?\n|\Z', Comment, Triple) +PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name) + +tokenprog, pseudoprog, single3prog, double3prog = map( + re.compile, (Token, PseudoToken, Single3, Double3)) +endprogs = {"'": re.compile(Single), '"': re.compile(Double), + "'''": single3prog, '"""': double3prog, + "r'''": single3prog, 'r"""': double3prog, + "u'''": single3prog, 'u"""': double3prog, + "ur'''": single3prog, 'ur"""': double3prog, + "R'''": single3prog, 'R"""': double3prog, + "U'''": single3prog, 'U"""': double3prog, + "uR'''": single3prog, 'uR"""': double3prog, + "Ur'''": single3prog, 'Ur"""': double3prog, + "UR'''": single3prog, 'UR"""': double3prog, + "b'''": single3prog, 'b"""': double3prog, + "br'''": single3prog, 'br"""': double3prog, + "B'''": single3prog, 'B"""': double3prog, + "bR'''": single3prog, 'bR"""': double3prog, + "Br'''": single3prog, 'Br"""': double3prog, + "BR'''": single3prog, 'BR"""': double3prog, + 'r': None, 'R': None, 'u': None, 'U': None, + 'b': None, 'B': None} + +triple_quoted = {} +for t in ("'''", '"""', + "r'''", 'r"""', "R'''", 'R"""', + "u'''", 'u"""', "U'''", 'U"""', + "ur'''", 'ur"""', "Ur'''", 'Ur"""', + "uR'''", 'uR"""', "UR'''", 'UR"""', + "b'''", 'b"""', "B'''", 'B"""', + "br'''", 'br"""', "Br'''", 'Br"""', + "bR'''", 'bR"""', "BR'''", 'BR"""'): + triple_quoted[t] = t +single_quoted = {} +for t in ("'", '"', + "r'", 'r"', "R'", 'R"', + "u'", 'u"', "U'", 'U"', + "ur'", 'ur"', "Ur'", 'Ur"', + "uR'", 'uR"', "UR'", 'UR"', + "b'", 'b"', "B'", 'B"', + "br'", 'br"', "Br'", 'Br"', + "bR'", 'bR"', "BR'", 'BR"' ): + single_quoted[t] = t + +tabsize = 8 + +class TokenError(Exception): pass + +class StopTokenizing(Exception): pass + +def printtoken(type, token, srow_scol, erow_ecol, line): # for testing + srow, scol = srow_scol + erow, ecol = erow_ecol + print("%d,%d-%d,%d:\t%s\t%s" % \ + (srow, scol, erow, ecol, tok_name[type], repr(token))) + +def tokenize(readline, tokeneater=printtoken): + """ + The tokenize() function accepts two parameters: one representing the + input stream, and one providing an output mechanism for tokenize(). + + The first parameter, readline, must be a callable object which provides + the same interface as the readline() method of built-in file objects. + Each call to the function should return one line of input as a string. + + The second parameter, tokeneater, must also be a callable object. It is + called once for each token, with five arguments, corresponding to the + tuples generated by generate_tokens(). + """ + try: + tokenize_loop(readline, tokeneater) + except StopTokenizing: + pass + +# backwards compatible interface +def tokenize_loop(readline, tokeneater): + for token_info in generate_tokens(readline): + tokeneater(*token_info) + +class Untokenizer: + + def __init__(self): + self.tokens = [] + self.prev_row = 1 + self.prev_col = 0 + + def add_whitespace(self, start): + row, col = start + if row < self.prev_row or row == self.prev_row and col < self.prev_col: + raise ValueError("start ({},{}) precedes previous end ({},{})" + .format(row, col, self.prev_row, self.prev_col)) + row_offset = row - self.prev_row + if row_offset: + self.tokens.append("\\\n" * row_offset) + self.prev_col = 0 + col_offset = col - self.prev_col + if col_offset: + self.tokens.append(" " * col_offset) + + def untokenize(self, iterable): + it = iter(iterable) + indents = [] + startline = False + for t in it: + if len(t) == 2: + self.compat(t, it) + break + tok_type, token, start, end, line = t + if tok_type == ENDMARKER: + break + if tok_type == INDENT: + indents.append(token) + continue + elif tok_type == DEDENT: + indents.pop() + self.prev_row, self.prev_col = end + continue + elif tok_type in (NEWLINE, NL): + startline = True + elif startline and indents: + indent = indents[-1] + if start[1] >= len(indent): + self.tokens.append(indent) + self.prev_col = len(indent) + startline = False + self.add_whitespace(start) + self.tokens.append(token) + self.prev_row, self.prev_col = end + if tok_type in (NEWLINE, NL): + self.prev_row += 1 + self.prev_col = 0 + return "".join(self.tokens) + + def compat(self, token, iterable): + indents = [] + toks_append = self.tokens.append + startline = token[0] in (NEWLINE, NL) + prevstring = False + + for tok in chain([token], iterable): + toknum, tokval = tok[:2] + + if toknum in (NAME, NUMBER): + tokval += ' ' + + # Insert a space between two consecutive strings + if toknum == STRING: + if prevstring: + tokval = ' ' + tokval + prevstring = True + else: + prevstring = False + + if toknum == INDENT: + indents.append(tokval) + continue + elif toknum == DEDENT: + indents.pop() + continue + elif toknum in (NEWLINE, NL): + startline = True + elif startline and indents: + toks_append(indents[-1]) + startline = False + toks_append(tokval) + +def untokenize(iterable): + """Transform tokens back into Python source code. + + Each element returned by the iterable must be a token sequence + with at least two elements, a token number and token value. If + only two tokens are passed, the resulting output is poor. + + Round-trip invariant for full input: + Untokenized source will match input source exactly + + Round-trip invariant for limited intput: + # Output text will tokenize the back to the input + t1 = [tok[:2] for tok in generate_tokens(f.readline)] + newcode = untokenize(t1) + readline = iter(newcode.splitlines(1)).next + t2 = [tok[:2] for tok in generate_tokens(readline)] + assert t1 == t2 + """ + ut = Untokenizer() + return ut.untokenize(iterable) + +def generate_tokens(readline): + """ + The generate_tokens() generator requires one argument, readline, which + must be a callable object which provides the same interface as the + readline() method of built-in file objects. Each call to the function + should return one line of input as a string. Alternately, readline + can be a callable function terminating with StopIteration: + readline = open(myfile).next # Example of alternate readline + + The generator produces 5-tuples with these members: the token type; the + token string; a 2-tuple (srow, scol) of ints specifying the row and + column where the token begins in the source; a 2-tuple (erow, ecol) of + ints specifying the row and column where the token ends in the source; + and the line on which the token was found. The line passed is the + logical line; continuation lines are included. + """ + lnum = parenlev = continued = 0 + namechars, numchars = string.ascii_letters + '_', '0123456789' + contstr, needcont = '', 0 + contline = None + indents = [0] + + while 1: # loop over lines in stream + try: + line = readline() + except StopIteration: + line = '' + lnum += 1 + pos, max = 0, len(line) + + if contstr: # continued string + if not line: + raise TokenError("EOF in multi-line string", strstart) + endmatch = endprog.match(line) + if endmatch: + pos = end = endmatch.end(0) + yield (STRING, contstr + line[:end], + strstart, (lnum, end), contline + line) + contstr, needcont = '', 0 + contline = None + elif needcont and line[-2:] != '\\\n' and line[-3:] != '\\\r\n': + yield (ERRORTOKEN, contstr + line, + strstart, (lnum, len(line)), contline) + contstr = '' + contline = None + continue + else: + contstr = contstr + line + contline = contline + line + continue + + elif parenlev == 0 and not continued: # new statement + if not line: break + column = 0 + while pos < max: # measure leading whitespace + if line[pos] == ' ': + column += 1 + elif line[pos] == '\t': + column = (column//tabsize + 1)*tabsize + elif line[pos] == '\f': + column = 0 + else: + break + pos += 1 + if pos == max: + break + + if line[pos] in '#\r\n': # skip comments or blank lines + if line[pos] == '#': + comment_token = line[pos:].rstrip('\r\n') + nl_pos = pos + len(comment_token) + yield (COMMENT, comment_token, + (lnum, pos), (lnum, pos + len(comment_token)), line) + yield (NL, line[nl_pos:], + (lnum, nl_pos), (lnum, len(line)), line) + else: + yield ((NL, COMMENT)[line[pos] == '#'], line[pos:], + (lnum, pos), (lnum, len(line)), line) + continue + + if column > indents[-1]: # count indents or dedents + indents.append(column) + yield (INDENT, line[:pos], (lnum, 0), (lnum, pos), line) + while column < indents[-1]: + if column not in indents: + raise IndentationError( + "unindent does not match any outer indentation level", + ("", lnum, pos, line)) + indents = indents[:-1] + yield (DEDENT, '', (lnum, pos), (lnum, pos), line) + + else: # continued statement + if not line: + raise TokenError("EOF in multi-line statement", (lnum, 0)) + continued = 0 + + while pos < max: + pseudomatch = pseudoprog.match(line, pos) + if pseudomatch: # scan for tokens + start, end = pseudomatch.span(1) + spos, epos, pos = (lnum, start), (lnum, end), end + if start == end: + continue + token, initial = line[start:end], line[start] + + if initial in numchars or \ + (initial == '.' and token != '.'): # ordinary number + yield (NUMBER, token, spos, epos, line) + elif initial in '\r\n': + if parenlev > 0: + n = NL + else: + n = NEWLINE + yield (n, token, spos, epos, line) + elif initial == '#': + assert not token.endswith("\n") + yield (COMMENT, token, spos, epos, line) + elif token in triple_quoted: + endprog = endprogs[token] + endmatch = endprog.match(line, pos) + if endmatch: # all on one line + pos = endmatch.end(0) + token = line[start:pos] + yield (STRING, token, spos, (lnum, pos), line) + else: + strstart = (lnum, start) # multiple lines + contstr = line[start:] + contline = line + break + elif initial in single_quoted or \ + token[:2] in single_quoted or \ + token[:3] in single_quoted: + if token[-1] == '\n': # continued string + strstart = (lnum, start) + endprog = (endprogs[initial] or endprogs[token[1]] or + endprogs[token[2]]) + contstr, needcont = line[start:], 1 + contline = line + break + else: # ordinary string + yield (STRING, token, spos, epos, line) + elif initial in namechars: # ordinary name + yield (NAME, token, spos, epos, line) + elif initial == '\\': # continued stmt + continued = 1 + else: + if initial in '([{': + parenlev += 1 + elif initial in ')]}': + parenlev -= 1 + yield (OP, token, spos, epos, line) + else: + yield (ERRORTOKEN, line[pos], + (lnum, pos), (lnum, pos+1), line) + pos += 1 + + for indent in indents[1:]: # pop remaining indent levels + yield (DEDENT, '', (lnum, 0), (lnum, 0), '') + yield (ENDMARKER, '', (lnum, 0), (lnum, 0), '') + +if __name__ == '__main__': # testing + import sys + if len(sys.argv) > 1: + tokenize(open(sys.argv[1]).readline) + else: + tokenize(sys.stdin.readline) diff --git a/plugins/mitogen-0.3.44/mitogen/core.py b/plugins/mitogen-0.3.44/mitogen/core.py new file mode 100644 index 0000000..cfda0a9 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/core.py @@ -0,0 +1,4349 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +This module implements most package functionality, but remains separate from +non-essential code in order to reduce its size, since it is also serves as the +bootstrap implementation sent to every new slave context. +""" + +import sys +try: + import _frozen_importlib_external +except ImportError: + pass +else: + class MonkeyPatchedPathFinder(_frozen_importlib_external.PathFinder): + """ + Meta path finder for sys.path and package __path__ attributes. + + Patched for https://github.com/python/cpython/issues/115911. + """ + @classmethod + def _path_importer_cache(cls, path): + if path == '': + try: + path = _frozen_importlib_external._os.getcwd() + except (FileNotFoundError, PermissionError): + return None + return super()._path_importer_cache(path) + + if sys.version_info[:2] <= (3, 12): + for i, mpf in enumerate(sys.meta_path): + if mpf is _frozen_importlib_external.PathFinder: + sys.meta_path[i] = MonkeyPatchedPathFinder + del i, mpf + + +import _codecs +import binascii +import collections +import errno +import fcntl +import itertools +import logging +import os +import pstats +import pty +import re +import signal +import socket +import struct +import syslog +import threading +import time +import traceback +import types +import warnings +import weakref +import zlib + +if sys.version_info >= (3, 6): + ModuleNotFoundError = ModuleNotFoundError +else: + ModuleNotFoundError = ImportError + +if sys.version_info >= (3, 5): + from os import get_blocking, set_blocking +else: + def get_blocking(fd): + return not fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK + + def set_blocking(fd, blocking): + fl = fcntl.fcntl(fd, fcntl.F_GETFL) + if blocking: fcntl.fcntl(fd, fcntl.F_SETFL, fl & ~os.O_NONBLOCK) + else: fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) + +if sys.version_info >= (3, 4): + import importlib.machinery + import importlib.util +else: + import imp + +if sys.version_info >= (3, 3): + now = time.monotonic +else: + now = time.time + +if sys.version_info >= (3, 0): + from pickle import PicklingError, Unpickler as _Unpickler, UnpicklingError + def find_deny(module, name): + raise UnpicklingError('Denied: %s.%s' % (module, name)) + class Unpickler(_Unpickler): + def __init__(self, file, find_class=find_deny): + self.find_class = find_class + super().__init__(file, encoding='bytes') +else: + from cPickle import PicklingError, Unpickler as _Unpickler, UnpicklingError + def find_deny(module, name): + raise UnpicklingError('Denied: %s.%s' % (module, name)) + def Unpickler(file, find_class=find_deny): + unpickler = _Unpickler(file) + unpickler.find_global = find_class + return unpickler + +if sys.version_info >= (3, 0): + from pickle import Pickler as _Pickler + class Pickler(_Pickler): + def __init__(self, file, protocol): + self._file = file + self._protocol = protocol + super().__init__(file, protocol) + def dump(self, obj): + if self._protocol == 2 and type(obj) == bytes: + self._file.write(struct.pack('= (2, 5): + from cPickle import Pickler + str_partition, str_rpartition = unicode.partition, unicode.rpartition + bytes_partition = str.partition +else: + import pickle + class Pickler(pickle.Pickler): + def save_exc_inst(self, obj): + if isinstance(obj, CallError): + func, args = obj.__reduce__() + self.save(func) + self.save(args) + self.write(pickle.REDUCE) + else: + pickle.Pickler.save_inst(self, obj) + + dispatch = pickle.Pickler.dispatch.copy() + dispatch[pickle.InstanceType] = save_exc_inst + + def _part(s, sep, find): + "(str|unicode).(partition|rpartition) polyfill for Python 2.4" + idx = find(sep) + if idx != -1: + left = s[0:idx] + return left, sep, s[len(left)+len(sep):] + def str_partition(s, sep): return _part(s, sep, s.find) or (s, u'', u'') + def str_rpartition(s, sep): return _part(s, sep, s.rfind) or (u'', u'', s) + def bytes_partition(s, sep): return _part(s, sep, s.find) or (s, '', '') + +if sys.version_info >= (2, 6): + next = next + threading__current_thread = threading.current_thread + def threading__thread_name(thread): return thread.name +else: + threading__current_thread = threading.currentThread + def next(it): return it.next() + def threading__thread_name(thread): return thread.getName() + +if sys.version_info >= (2, 5): + all, any = all, any + BaseException = BaseException + def _update_linecache(path, data): pass +else: + import linecache + BaseException = Exception + def _update_linecache(path, data): + """ + Directly populate the linecache cache for modules loaded by Mitogen. + In Python 2.4 the linecache module, does not support PEP-302. + """ + if 'mitogen' not in path: + return + linecache.cache[path] = (len(data), 0.0, data.splitlines(True), path) + + def all(it): + for elem in it: + if not elem: + return False + return True + + def any(it): + for elem in it: + if elem: + return True + return False + +# Absolute imports for <2.5. +select = __import__('select') + +try: + import cProfile +except ImportError: + cProfile = None + +# TODO: usage of 'import' after setting __name__, but before fixing up +# sys.modules generates a warning. This happens when profiling = True. +warnings.filterwarnings('ignore', + "Parent module 'mitogen' not found while handling absolute import") + +LOG = logging.getLogger('mitogen') +IOLOG = logging.getLogger('mitogen.io') +IOLOG.setLevel(logging.INFO) + +_v = False +_vv = False + +GET_MODULE = 100 +CALL_FUNCTION = 101 +FORWARD_LOG = 102 +ADD_ROUTE = 103 +DEL_ROUTE = 104 +ALLOCATE_ID = 105 +SHUTDOWN = 106 +LOAD_MODULE = 107 +FORWARD_MODULE = 108 +DETACHING = 109 +CALL_SERVICE = 110 +STUB_CALL_SERVICE = 111 +GET_RESOURCE = 112 +LOAD_RESOURCE = 113 + +#: Special value used to signal disconnection or the inability to route a +#: message, when it appears in the `reply_to` field. Usually causes +#: :class:`mitogen.core.ChannelError` to be raised when it is received. +#: +#: It indicates the sender did not know how to process the message, or wishes +#: no further messages to be delivered to it. It is used when: +#: +#: * 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 +#: :meth:`mitogen.core.Router.add_handler` callbacks to clean up. +IS_DEAD = 999 + +PY24 = sys.version_info < (2, 5) +PY3 = sys.version_info > (3,) +if sys.version_info >= (3, 0): + import _thread as thread + from io import BytesIO + b = str.encode + BytesType = bytes + UnicodeType = str + FsPathTypes = (str,) + BufferType = lambda buf, start: memoryview(buf)[start:] + integer_types = (int,) + iteritems, iterkeys, itervalues = dict.items, dict.keys, dict.values + range = range +else: + import thread + from cStringIO import StringIO as BytesIO + b = str + BytesType = str + FsPathTypes = (str, unicode) + BufferType = buffer + UnicodeType = unicode + integer_types = (int, long) + iteritems, iterkeys, itervalues = dict.iteritems, dict.iterkeys, dict.itervalues + range = xrange + +AnyTextType = (BytesType, UnicodeType) + +# #550: prehistoric WSL did not advertise itself in uname output. +try: + fp = open('/proc/sys/kernel/osrelease') + IS_WSL = 'Microsoft' in fp.read() + fp.close() +except IOError: + IS_WSL = False + + +#: Default size for calls to :meth:`Side.read` or :meth:`Side.write`, and the +#: size of buffers configured by :func:`mitogen.parent.create_socketpair`. This +#: value has many performance implications, 128KiB seems to be a sweet spot. +#: +#: * When set low, large messages cause many :class:`Broker` IO loop +#: iterations, burning CPU and reducing throughput. +#: * When set high, excessive RAM is reserved by the OS for socket buffers (2x +#: per child), and an identically sized temporary userspace buffer is +#: allocated on each read that requires zeroing, and over a particular size +#: may require two system calls to allocate/deallocate. +#: +#: Care must be taken to ensure the underlying kernel object and receiving +#: program support the desired size. For example, +#: +#: * Most UNIXes have TTYs with fixed 2KiB-4KiB buffers, making them unsuitable +#: for efficient IO. +#: * Different UNIXes have varying presets for pipes, which may not be +#: configurable. On recent Linux the default pipe buffer size is 64KiB, but +#: under memory pressure may be as low as 4KiB for unprivileged processes. +#: * When communication is via an intermediary process, its internal buffers +#: effect the speed OS buffers will drain. For example OpenSSH uses 64KiB +#: reads. +#: +#: An ideal :class:`Message` has a size that is a multiple of +#: :data:`CHUNK_SIZE` inclusive of headers, to avoid wasting IO loop iterations +#: writing small trailer chunks. +CHUNK_SIZE = 131072 + +_tls = threading.local() + + +if __name__ == 'mitogen.core': + # When loaded using import mechanism, ExternalContext.main() will not have + # a chance to set the synthetic mitogen global, so just import it here. + import mitogen +else: + # When loaded as __main__, ensure classes and functions gain a __module__ + # attribute consistent with the host process, so that pickling succeeds. + __name__ = 'mitogen.core' + + +class Error(Exception): + """ + Base for all exceptions raised by Mitogen. + + :param str fmt: + Exception text, or format string if `args` is non-empty. + :param tuple args: + Format string arguments. + """ + def __init__(self, fmt=None, *args): + if args: + fmt %= args + if fmt and not isinstance(fmt, UnicodeType): + fmt = fmt.decode('utf-8') + Exception.__init__(self, fmt) + + +class LatchError(Error): + """ + Raised when an attempt is made to use a :class:`mitogen.core.Latch` that + has been marked closed. + """ + pass + + +class ModuleDeniedByOverridesError(ModuleNotFoundError): + fmt = "Mitogen won't serve %s, it's not in the overrides list" + + +class ModuleDeniedByBlocksError(ModuleNotFoundError): + fmt = "Mitogen won't serve %s, it's in the blocks list" + + +class ModuleUnsuitableError(ModuleNotFoundError): + fmt = "Mitogen won't serve %s, it's e.g. binary, legacy, part of stdlib" + + +class Blob(BytesType): + """ + A serializable bytes subclass whose content is summarized in repr() output, + making it suitable for logging binary data. + """ + def __repr__(self): + return '[blob: %d bytes]' % len(self) + + def __reduce__(self): + return (Blob, (BytesType(self),)) + + +class Secret(UnicodeType): + """ + A serializable unicode subclass whose content is masked in repr() output, + making it suitable for logging passwords. + """ + def __repr__(self): + return '[secret]' + + if sys.version_info < (3, 0): + # TODO: what is this needed for in 2.x? + def __str__(self): + return UnicodeType(self) + + def __reduce__(self): + return (Secret, (UnicodeType(self),)) + + +class Kwargs(dict): + """ + A serializable dict subclass that indicates its keys should be coerced to + Unicode on Python 3 and bytes on Python<2.6. + + Python 2 produces keyword argument dicts whose keys are bytes, requiring a + helper to ensure compatibility with Python 3 where Unicode is required, + whereas Python 3 produces keyword argument dicts whose keys are Unicode, + requiring a helper for Python 2.4/2.5, where bytes are required. + """ + if sys.version_info >= (3, 0): + def __init__(self, dct): + for k, v in dct.items(): + if type(k) is bytes: + self[k.decode()] = v + else: + self[k] = v + elif sys.version_info < (2, 6, 5): + def __init__(self, dct): + for k, v in dct.iteritems(): + if type(k) is unicode: + k, _ = _codecs.utf_8_encode(k) + self[k] = v + + def __repr__(self): + return 'Kwargs(%s)' % (dict.__repr__(self),) + + def __reduce__(self): + return (Kwargs, (dict(self),)) + + +class CallError(Error): + """ + Serializable :class:`Error` subclass raised when :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): + if not isinstance(fmt, BaseException): + Error.__init__(self, fmt, *args) + else: + e = fmt + cls = e.__class__ + fmt = '%s.%s: %s' % (cls.__module__, cls.__name__, e) + tb = sys.exc_info()[2] + if tb: + fmt += '\n' + fmt += ''.join(traceback.format_tb(tb)) + Error.__init__(self, fmt) + + def __reduce__(self): + return (_unpickle_call_error, (self.args[0],)) + + +def _unpickle_call_error(s): + if not (type(s) is UnicodeType and len(s) < 10000): + raise TypeError('cannot unpickle CallError: bad input') + return CallError(s) + + +class ChannelError(Error): + """ + Raised when a channel dies or has been closed. + """ + remote_msg = 'Channel closed by remote end.' + local_msg = 'Channel closed by local end.' + + +class StreamError(Error): + """ + Raised when a stream cannot be established. + """ + pass + + +class TimeoutError(Error): + """ + Raised when a timeout occurs on a stream. + """ + pass + + +def to_text(o): + """ + Coerce `o` to Unicode by decoding it from UTF-8 if it is an instance of + :class:`bytes`, otherwise pass it to the :class:`str` constructor. The + returned object is always a plain :class:`str`, any subclass is removed. + """ + if isinstance(o, BytesType): + return o.decode('utf-8') + return UnicodeType(o) + + +def _has_parent_authority(context_id): + return ( + (context_id == mitogen.context_id) or + (context_id in mitogen.parent_ids) + ) + +def has_parent_authority(msg, _stream=None): + """ + Policy function for use with :class:`Receiver` and + :meth:`Router.add_handler` that requires incoming messages to originate + from a parent context, or on a :class:`Stream` whose :attr:`auth_id + ` has been set to that of a parent context or the current + context. + """ + return _has_parent_authority(msg.auth_id) + + +def module_lineage(fullname): + "Return an iterator of a module's parent fullnames and its own" + return (fullname[:m.start()] for m in re.finditer(r'\.|\Z', fullname)) + + +def _signals(obj, signal): + return ( + obj.__dict__ + .setdefault('_signals', {}) + .setdefault(signal, []) + ) + + +def listen(obj, name, func): + """ + Arrange for `func()` to be invoked when signal `name` is fired on `obj`. + """ + _signals(obj, name).append(func) + + +def unlisten(obj, name, func): + """ + Remove `func()` from the list of functions invoked when signal `name` is + fired by `obj`. + + :raises ValueError: + `func()` was not on the list. + """ + _signals(obj, name).remove(func) + + +def fire(obj, name, *args, **kwargs): + """ + Arrange for `func(*args, **kwargs)` to be invoked for every function + registered for signal `name` on `obj`. + """ + for func in _signals(obj, name): + func(*args, **kwargs) + + +def takes_econtext(func): + """ + Decorator that marks a function or class method to automatically receive a + kwarg named `econtext`, referencing the + :class:`mitogen.core.ExternalContext` active in the context in which the + function is being invoked in. The decorator is only meaningful when the + function is invoked via :data:`CALL_FUNCTION `. + + When the function is invoked directly, `econtext` must still be passed to + it explicitly. + """ + func.mitogen_takes_econtext = True + return func + + +def takes_router(func): + """ + Decorator that marks a function or class method to automatically receive a + kwarg named `router`, referencing the :class:`mitogen.core.Router` active + in the context in which the function is being invoked in. The decorator is + only meaningful when the function is invoked via :data:`CALL_FUNCTION + `. + + When the function is invoked directly, `router` must still be passed to it + explicitly. + """ + func.mitogen_takes_router = True + return func + + +def set_cloexec(fd): + """ + Set the file descriptor `fd` to automatically close on :func:`os.execve`. + This has no effect on file descriptors inherited across :func:`os.fork`, + they must be explicitly closed through some other means, such as + :func:`mitogen.fork.on_fork`. + """ + stdfds = [ + stdfd + for stdio, stdfd in [ + (sys.stdin, pty.STDIN_FILENO), + (sys.stdout, pty.STDOUT_FILENO), + (sys.stderr, pty.STDERR_FILENO), + ] + if stdio is not None and not stdio.closed + ] + assert fd not in stdfds, 'fd %r is one of the stdio fds: %r' % (fd, stdfds) + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + +def io_op(func, *args): + """ + Wrap `func(*args)` that may raise :class:`select.error`, :class:`IOError`, + or :class:`OSError`, trapping UNIX error codes relating to disconnection + and retry events in various subsystems: + + * When a signal is delivered to the process on Python 2, system call retry + is signalled through :data:`errno.EINTR`. The invocation is automatically + restarted. + * When performing IO against a TTY, disconnection of the remote end is + signalled by :data:`errno.EIO`. + * When performing IO against a socket, disconnection of the remote end is + signalled by :data:`errno.ECONNRESET`. + * When performing IO against a pipe, disconnection of the remote end is + signalled by :data:`errno.EPIPE`. + + :returns: + Tuple of `(return_value, disconnect_reason)`, where `return_value` is + the return value of `func(*args)`, and `disconnected` is an exception + instance when disconnection was detected, otherwise :data:`None`. + """ + while True: + try: + return func(*args), None + except (select.error, OSError, IOError): + e = sys.exc_info()[1] + _vv and IOLOG.debug('io_op(%r) -> OSError: %s', func, e) + if e.args[0] == errno.EINTR: + continue + if e.args[0] in (errno.EIO, errno.ECONNRESET, errno.EPIPE): + return None, e + raise + + +class PidfulStreamHandler(logging.StreamHandler): + """ + A :class:`logging.StreamHandler` subclass used when + :meth:`Router.enable_debug() ` has been + called, or the `debug` parameter was specified during context construction. + Verifies the process ID has not changed on each call to :meth:`emit`, + reopening the associated log file when a change is detected. + + This ensures logging to the per-process output files happens correctly even + when uncooperative third party components call :func:`os.fork`. + """ + #: PID that last opened the log file. + open_pid = None + + #: Output path template. + template = '/tmp/mitogen.%s.%s.log' + + def _reopen(self): + self.acquire() + try: + if self.open_pid == os.getpid(): + return + ts = time.strftime('%Y%m%d_%H%M%S') + path = self.template % (os.getpid(), ts) + self.stream = open(path, 'w', 1) + set_cloexec(self.stream.fileno()) + self.stream.write('Parent PID: %s\n' % (os.getppid(),)) + self.stream.write('Created by:\n\n%s\n' % ( + ''.join(traceback.format_stack()), + )) + self.open_pid = os.getpid() + finally: + self.release() + + def emit(self, record): + if self.open_pid != os.getpid(): + self._reopen() + logging.StreamHandler.emit(self, record) + + +def enable_debug_logging(): + global _v, _vv + _v = True + _vv = True + root = logging.getLogger() + root.setLevel(logging.DEBUG) + IOLOG.setLevel(logging.DEBUG) + handler = PidfulStreamHandler() + handler.formatter = logging.Formatter( + '%(asctime)s %(levelname).1s %(name)s: %(message)s', + '%H:%M:%S' + ) + root.handlers.insert(0, handler) + + +_profile_hook = lambda name, func, *args: func(*args) +_profile_fmt = os.environ.get( + 'MITOGEN_PROFILE_FMT', + '/tmp/mitogen.stats.%(pid)s.%(identity)s.%(now)s.%(ext)s', +) + + +def _profile_hook(name, func, *args): + """ + Call `func(*args)` and return its result. This function is replaced by + :func:`_real_profile_hook` when :func:`enable_profiling` is called. This + interface is obsolete and will be replaced by a signals-based integration + later on. + """ + return func(*args) + + +def _real_profile_hook(name, func, *args): + profiler = cProfile.Profile() + profiler.enable() + try: + return func(*args) + finally: + path = _profile_fmt % { + 'now': int(1e6 * now()), + 'identity': name, + 'pid': os.getpid(), + 'ext': '%s' + } + profiler.dump_stats(path % ('pstats',)) + profiler.create_stats() + fp = open(path % ('log',), 'w') + try: + stats = pstats.Stats(profiler, stream=fp) + stats.sort_stats('cumulative') + stats.print_stats() + finally: + fp.close() + + +def enable_profiling(econtext=None): + global _profile_hook + _profile_hook = _real_profile_hook + + +def import_module(modname): + """ + Import `module` and return the attribute named `attr`. + """ + return __import__(modname, None, None, ['']) + + +def pipe(blocking=None): + """ + Create a UNIX pipe pair using :func:`os.pipe`, wrapping the returned + descriptors in Python file objects in order to manage their lifetime and + ensure they are closed when their last reference is discarded and they have + not been closed explicitly. + """ + rfd, wfd = os.pipe() + for fd in rfd, wfd: + if blocking is not None: set_blocking(fd, blocking) # noqa: E701 + return ( + os.fdopen(rfd, 'rb', 0), + os.fdopen(wfd, 'wb', 0) + ) + + +def socketpair(blocking=None): + fp1, fp2 = socket.socketpair() + for fp in fp1, fp2: + fd = fp.fileno() + if blocking is not None: set_blocking(fd, blocking) # noqa: E701 + return fp1, fp2 + + +def iter_split(buf, delim, func): + """ + Invoke `func(s)` for each `delim`-delimited chunk in the potentially large + `buf`, avoiding intermediate lists and quadratic string operations. Return + the trailing undelimited portion of `buf`, or any unprocessed portion of + `buf` after `func(s)` returned :data:`False`. + + :returns: + `(trailer, cont)`, where `cont` is :data:`False` if the last call to + `func(s)` returned :data:`False`. + """ + dlen = len(delim) + start = 0 + cont = True + while cont: + nl = buf.find(delim, start) + if nl == -1: + break + cont = not func(buf[start:nl]) is False + start = nl + dlen + return buf[start:], cont + + +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. + """ + ENCS = frozenset(range(0x4d49, 0x4d49+3)) + ENC_MGC, ENC_PKL, ENC_BIN = sorted(ENCS) + + #: 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('') + + #: Encoding of payload in :attr:`data`, one of the ``ENC_*`` constants. + #: :attr:`ENC_MGC` is an implicit, legacy value. New features & + #: :ref:`standard-handles` should explicitly declare an encoding. + enc = ENC_MGC + + #: 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 + + HEADER_FMT = '>hLLLLLL' + HEADER_LEN = struct.calcsize(HEADER_FMT) + HEADER_MAGIC = ENC_MGC + + 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) + assert isinstance(self.data, BytesType), 'Message data is not Bytes' + if self.enc not in self.ENCS: + raise ValueError('Invalid enc: %r' % (self.enc,)) + + def pack(self): + return ( + struct.pack(self.HEADER_FMT, self.enc, self.dst_id, + self.src_id, self.auth_id, self.handle, + self.reply_to or 0, len(self.data)) + + self.data + ) + + def _unpickle_context(self, 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) + + def _unpickle_bytes(self, s, encoding): + s, n = _codecs.latin_1_encode(s) + return s + + def _find_global(self, module, func): + """ + Return the class implementing `module_name.class_name` or raise + `StreamError` if the module is not whitelisted. + """ + if module == __name__: + if func == '_unpickle_call_error' or func == 'CallError': + return _unpickle_call_error + elif func == '_unpickle_sender': + return self._unpickle_sender + elif func == '_unpickle_context': + return self._unpickle_context + elif func == 'Blob': + return Blob + elif func == 'Secret': + return Secret + elif func == 'Kwargs': + return Kwargs + elif module == '_codecs' and func == 'encode': + return self._unpickle_bytes + elif module == '__builtin__' and func == 'bytes': + return BytesType + raise StreamError('cannot unpickle %r/%r', module, func) + + @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. Dead + messages can be raised in a variety of circumstances, see + :data:`IS_DEAD` for more information. + """ + return self.reply_to == IS_DEAD + + @classmethod + def dead(cls, reason=None, **kwargs): + """ + Syntax helper to construct a dead message. + """ + kwargs['data'], _ = _codecs.utf_8_encode(reason or u'') + return cls(reply_to=IS_DEAD, **kwargs) + + @classmethod + def encoded(cls, obj, enc, **kwargs): + if enc == cls.ENC_PKL: return cls.pickled(obj, **kwargs) + if enc == cls.ENC_BIN: return cls(data=obj, enc=enc, **kwargs) + raise ValueError('Invalid explicit enc: %r' % (enc,)) + + @classmethod + def pickled(cls, *args, **kwargs): + """ + Construct a pickled message, setting :attr:`data` to the serialization + of each object in `args`, and setting remaining fields using `kwargs`. + + :returns: + The new message. + """ + f = BytesIO() + p = Pickler(f, protocol=2) + for obj in args: + try: + p.dump(obj) + except PicklingError: + exc = sys.exc_info()[1] + p.dump(CallError(exc)) + return cls(enc=cls.ENC_PKL, data=f.getvalue(), **kwargs) + + def reply(self, msg, router=None, **kwargs): + """ + Compose a reply to this message and send it using :attr:`router`, or + `router` if :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 + msg.handle = self.reply_to + vars(msg).update(kwargs) + if msg.handle: + (self.router or router).route(msg) + else: + LOG.debug('dropping reply to message with no return address: %r', + msg) + + def _throw_dead(self): + if len(self.data): + raise ChannelError(self.data.decode('utf-8', 'replace')) + elif self.src_id == mitogen.context_id: + raise ChannelError(ChannelError.local_msg) + else: + raise ChannelError(ChannelError.remote_msg) + + def decode(self, throw=True, throw_dead=True): + if self.enc == self.ENC_PKL: return self.unpickle(throw, throw_dead) + if self.enc == self.ENC_BIN: return self.data + raise ValueError('Invalid explicit enc: %r' % (self.enc,)) + + def unpickle(self, throw=True, throw_dead=True, find_class=None): + """ + Return the first unpickled stream in :attr:`data`, optionally raise + :exc:`CallError` if the unpickled object is such. + + `throw` and `throw_dead` behave the same as with :meth:`unpickle_iter`. + + :param find_class: + Callable that takes ``(module, func)`` and returns a constructor. + Defaults to :meth:`_find_global`. + """ + if find_class is None: find_class = self._find_global + return next(self.unpickle_iter(throw, throw_dead, find_class)) + + def unpickle_iter(self, throw=True, throw_dead=True, find_class=find_deny): + """ + Return an iterator of objects unpickled from :attr:`data`, optionally + raising any :exc:`CallError` exceptions present. + + :param bool throw_dead: + If :data:`True`, raise exceptions, otherwise it is the caller's + responsibility. + :param find_class: + Callable that takes ``(module, func)`` and returns a constructor. + Default: :func:`find_deny`. + + :raises CallError: + The serialized data contained CallError exception. + :raises ChannelError: + The `is_dead` field was set. + """ + if self.enc not in (self.ENC_MGC, self.ENC_PKL): + raise ValueError( + 'Message %r is not pickled, invalid enc=%r', self, self.enc, + ) + if throw_dead and self.is_dead: + self._throw_dead() + + file = BytesIO(self.data) + unpickler = Unpickler(file, find_class) + while file.tell() < len(self.data): + try: + # Must occur off the broker thread. + try: + obj = unpickler.load() + except: + LOG.error('raw pickle was: %r', self.data) + raise + except (TypeError, ValueError): + e = sys.exc_info()[1] + raise StreamError('invalid message: %s', e) + + if throw and isinstance(obj, CallError): + raise obj + + yield obj + + def __repr__(self): + if len(self.data) > 60: + head, tail, size = self.data[:25], self.data[-25:], len(self.data) + data_summary = b('%s .. %s %d bytes') % (head, tail, size) + else: + data_summary = self.data + return 'Message(src=%r:%r dst=%r:%r auth_id=%r %r)' % ( + self.src_id, self.reply_to, self.dst_id, self.handle, self.auth_id, + data_summary, + ) + + +class Sender(object): + """ + Senders are used to send pickled messages to a handle in another context, + it is the inverse of :class:`mitogen.core.Receiver`. + + 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. + """ + def __init__(self, context, dst_handle): + self.context = context + self.dst_handle = dst_handle + + def send(self, data, enc=Message.ENC_PKL): + """ + Send `data` to the remote end. + """ + _vv and IOLOG.debug('%r.send(%*r.., enc=%s)', self, 100, data, enc) + self.context.send(Message.encoded(data, enc, 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( + reason=self.explicit_close_msg, + 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): + if not (isinstance(router, Router) and + isinstance(context_id, integer_types) and context_id >= 0 and + isinstance(dst_handle, integer_types) and dst_handle > 0): + raise TypeError('cannot unpickle Sender: bad input or missing router') + return Sender(Context(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, and ignores messages that + did not originate from the respondent context. + """ + #: If not :data:`None`, a function invoked as `notify(receiver)` after a + #: message has been received. The function is invoked on :class:`Broker` + #: thread, therefore it must not block. Used by + #: :class:`mitogen.select.Select` to efficiently implement waiting on + #: multiple event sources. + notify = None + + raise_channelerror = True + + def __init__(self, router, handle=None, persist=True, + respondent=None, policy=None, overwrite=False): + 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( + fn=self._on_receive, + handle=handle, + policy=policy, + persist=persist, + respondent=respondent, + overwrite=overwrite, + ) + + def __repr__(self): + return 'Receiver(%r, %r)' % (self.router, self.handle) + + def __enter__(self): + return self + + def __exit__(self, _1, _2, _3): + self.close() + + 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() + + @mitogen.main() + def main(router): + remote = router.ssh(hostname='mainframe') + recv = mitogen.core.Receiver(router) + remote.call(deliver_monthly_report, recv.to_sender()) + for msg in recv: + print(msg) + """ + return Sender(self.router.myself(), self.handle) + + def _on_receive(self, msg): + """ + Callback registered for the handle with :class:`Router`; 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) + + closed_msg = 'the Receiver has been closed' + + 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.close() + + def size(self): + """ + Return the number of items currently buffered. + + As with :class:`Queue.Queue`, `0` may be returned even though a + subsequent call to :meth:`get` will succeed, since a message may be + posted at any moment between :meth:`size` and :meth:`get`. + + As with :class:`Queue.Queue`, `>0` 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:`size` and :meth:`get`. + + :raises LatchError: + The underlying latch has already been marked closed. + """ + return self._latch.size() + + def empty(self): + """ + Return `size() == 0`. + + .. deprecated:: 0.2.8 + Use :meth:`size` instead. + + :raises LatchError: + The latch has already been marked closed. + """ + 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, + communication with it was lost, or :meth:`close` was called in the + local process. + + :raises mitogen.core.TimeoutError: + Timeout was reached. + + :returns: + :class:`Message` that was received. + """ + _vv and IOLOG.debug('%r.get(timeout=%r, block=%r)', self, timeout, block) + try: + msg = self._latch.get(timeout=timeout, block=block) + except LatchError: + raise ChannelError(self.closed_msg) + if msg.is_dead and throw_dead: + msg._throw_dead() + return msg + + def __iter__(self): + """ + Yield consecutive :class:`Message` instances delivered to this receiver + until :class:`ChannelError` is raised. + """ + while True: + try: + msg = self.get() + except ChannelError: + return + yield msg + + +class Channel(Sender, Receiver): + """ + A channel inherits from :class:`mitogen.core.Sender` and + `mitogen.core.Receiver` to provide bidirectional functionality. + + .. deprecated:: 0.2.0 + This class is incomplete and obsolete, it will be removed in Mitogen + 0.3. + + Channels were an early attempt at syntax sugar. It is always easier to pass + around unidirectional pairs of senders/receivers, even though the syntax is + baroque: + + .. literalinclude:: ../examples/ping_pong.py + + 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) + + def close(self): + Receiver.close(self) + Sender.close(self) + + def __repr__(self): + return 'Channel(%s, %s)' % ( + Sender.__repr__(self), + Receiver.__repr__(self) + ) + + +class ImportPolicy(object): + """ + Policy deciding which module prefixes :class:`Importer` will request from + :class:`mitogen.master.ModuleResponder` and which requests will be served + or denied. + + :param overrides: + Prefixes always requested, ignoring local versions. If ``overrides`` + has entries, then it's also used as an allow list by the responder - + any request for a prefix that's not overriden will be denied. + + :param blocks: + Prefixes always denied by the responder, only local versions can be + used. + """ + def __init__(self, overrides=(), blocks=()): + self.overrides = set(overrides) + self.blocks = set(blocks) + self._always = set(Importer.ALWAYS_BLACKLIST) + + def denied(self, fullname): + fullnames = frozenset(module_lineage(fullname)) + if self.overrides and not self.overrides.intersection(fullnames): + return ModuleDeniedByOverridesError + if self.blocks.intersection(fullnames): return ModuleDeniedByBlocksError + if self._always.intersection(fullnames): return ModuleUnsuitableError + return False + + def denied_raise(self, fullname): + denial = self.denied(fullname) + if denial: raise denial(denial.fmt % (fullname,)) + + def overriden(self, fullname): + return bool(self.overrides.intersection(module_lineage(fullname))) + + def __repr__(self): + args = (type(self).__name__, self.overrides, self.blocks) + return '%s(overrides=%r, blocks=%r)' % args + + +class Importer(object): + """ + Import protocol implementation that fetches modules from the parent + process. + + :param context: Context to communicate via. + """ + # The Mitogen package is handled specially, since the child context must + # construct it manually during startup. + MITOGEN_PKG_CONTENT = [ + 'buildah', + 'compat', + 'debug', + 'doas', + 'docker', + 'kubectl', + 'fakessh', + 'fork', + 'imports', + 'jail', + 'lxc', + 'lxd', + 'master', + 'minify', + 'os_fork', + 'parent', + 'podman', + 'select', + 'service', + 'setns', + 'ssh', + 'su', + 'sudo', + 'utils', + ] + + ALWAYS_BLACKLIST = [ + # 2.x generates needless imports for 'builtins', while 3.x does the + # same for '__builtin__'. The correct one is built-in, the other always + # a negative round-trip. + 'builtins', + '__builtin__', + + # On some Python releases (e.g. 3.8, 3.9) the subprocess module tries + # to import of this Windows-only builtin module. + 'msvcrt', + + # Python 2.x module that was renamed to _thread in 3.x. + # This entry avoids a roundtrip on 2.x -> 3.x. + 'thread', + + # org.python.core imported by copy, pickle, xml.sax; breaks Jython, but + # very unlikely to trigger a bug report. + 'org', + ] + + if sys.version_info >= (3, 0): + ALWAYS_BLACKLIST += ['cStringIO'] + + def __init__(self, router, context, core_src, policy): + self._log = logging.getLogger('mitogen.importer') + self._context = context + self._present = {'mitogen': self.MITOGEN_PKG_CONTENT} + self._lock = threading.Lock() + self.policy = policy + + # Presence of an entry in this map indicates in-flight GET_MODULE. + self._callbacks = {} + self._cache = {} + if core_src: + _update_linecache('x/mitogen/core.py', core_src) + self._cache['mitogen.core'] = ( + 'mitogen.core', + None, + 'x/mitogen/core.py', + zlib.compress(core_src, 9), + [], + ) + self._install_handler(router) + + def _install_handler(self, router): + router.add_handler( + fn=self._on_load_module, + handle=LOAD_MODULE, + policy=has_parent_authority, + ) + + def __repr__(self): + return 'Importer' + + @staticmethod + def _loader_from_module(module, default=None): + """Return the loader for a module object.""" + try: + return module.__spec__.loader + except AttributeError: + pass + try: + return module.__loader__ + except AttributeError: + pass + return default + + def builtin_find_module(self, fullname): + # imp.find_module() will always succeed for __main__, because it is a + # built-in module. That means it exists on a special linked list deep + # within the bowels of the interpreter. We must special case it. + if fullname == '__main__': + raise ModuleNotFoundError() + + # For a module inside a package (e.g. pkg_a.mod_b) use the search path + # of that package (e.g. ['/usr/lib/python3.11/site-packages/pkg_a']). + parent, _, modname = str_rpartition(fullname, '.') + if parent: + path = sys.modules[parent].__path__ + else: + path = None + + # For a top-level module search builtin modules, frozen modules, + # system specific locations (e.g. Windows registry, site-packages). + # Otherwise use search path of the parent package. + # Works for both stdlib modules & third-party modules. + # If the search is unsuccessful then raises ImportError. + fp, pathname, description = imp.find_module(modname, path) + if fp: + fp.close() + + def find_module(self, fullname, path=None): + """ + Return a loader (ourself) or None, for the module with fullname. + + Implements importlib.abc.MetaPathFinder.find_module(). + Deprecrated in Python 3.4+, replaced by find_spec(). + Raises ImportWarning in Python 3.10+. + Removed in Python 3.12. + + fullname Fully qualified module name, e.g. "os.path". + path __path__ of parent packge. None for a top level module. + """ + if hasattr(_tls, 'running'): + return None + + _tls.running = True + try: + #_v and self._log.debug('Python requested %r', fullname) + fullname = to_text(fullname) + pkgname, _, suffix = str_rpartition(fullname, '.') + pkg = sys.modules.get(pkgname) + if pkgname and getattr(pkg, '__loader__', None) is not self: + self._log.debug('%s is submodule of a locally loaded package', + fullname) + return None + + if pkgname and suffix not in self._present.get(pkgname, ()): + self._log.debug('%s has no submodule %s', pkgname, suffix) + return None + + if self.policy.overriden(fullname): + return self + + try: + self.builtin_find_module(fullname) + _vv and self._log.debug('%r is available locally', fullname) + except ImportError: + _vv and self._log.debug('we will try to load %r', fullname) + return self + finally: + del _tls.running + + def find_spec(self, fullname, path, target=None): + """ + Return a `ModuleSpec` for module with `fullname` if we will load it. + Otherwise return `None`, allowing other finders to try. + + fullname Fully qualified name of the module (e.g. foo.bar.baz) + path Path entries to search. None for a top-level module. + target Existing module to be reloaded (if any). + + Implements importlib.abc.MetaPathFinder.find_spec() + Python 3.4+. + """ + # Presence of _tls.running indicates we've re-invoked importlib. + # Abort early to prevent infinite recursion. See below. + if hasattr(_tls, 'running'): + return None + + log = self._log.getChild('find_spec') + + if fullname.endswith('.'): + return None + + pkgname, _, modname = fullname.rpartition('.') + if pkgname and modname not in self._present.get(pkgname, ()): + log.debug('Skipping %s. Parent %s has no submodule %s', + fullname, pkgname, modname) + return None + + pkg = sys.modules.get(pkgname) + pkg_loader = self._loader_from_module(pkg) + if pkgname and pkg_loader is not self: + log.debug('Skipping %s. Parent %s was loaded by %r', + fullname, pkgname, pkg_loader) + return None + + if self.policy.overriden(fullname): + log.debug('Handling %s. It is overriden', fullname) + return importlib.machinery.ModuleSpec(fullname, loader=self) + + if fullname == '__main__': + log.debug('Handling %s. A special case', fullname) + return importlib.machinery.ModuleSpec(fullname, loader=self) + + # Re-invoke the import machinery to allow other finders to try. + # Set a guard, so we don't infinitely recurse. See top of this method. + _tls.running = True + try: + spec = importlib.util._find_spec(fullname, path, target) + finally: + del _tls.running + + if spec: + log.debug('Skipping %s. Available as %r', fullname, spec) + return spec + + log.debug('Handling %s. Unavailable locally', fullname) + return importlib.machinery.ModuleSpec(fullname, loader=self) + + pkg_resources_msg = ( + 'pkg_resources is prohibited from importing __main__, as it causes ' + 'problems in applications whose main module is not designed to be ' + 're-imported by children.' + ) + absent_msg = ( + 'The Mitogen master process was unable to serve %r. It may be a ' + 'native Python extension, or it may be missing entirely. Check the ' + 'importer debug logs on the master for more information.' + ) + + def _refuse_imports(self, fullname): + self.policy.denied_raise(fullname) + + f = sys._getframe(2) + requestee = f.f_globals['__name__'] + + if fullname == '__main__' and requestee == 'pkg_resources': + # Anything that imports pkg_resources will eventually cause + # pkg_resources to try and scan __main__ for its __requires__ + # attribute (pkg_resources/__init__.py::_build_master()). This + # breaks any app that is not expecting its __main__ to suddenly be + # sucked over a network and injected into a remote process, like + # py.test. + raise ModuleNotFoundError(self.pkg_resources_msg) + + if fullname == 'pbr': + # It claims to use pkg_resources to read version information, which + # would result in PEP-302 being used, but it actually does direct + # filesystem access. So instead smodge the environment to override + # any version that was defined. This will probably break something + # later. + os.environ['PBR_VERSION'] = '0.0.0' + + def _on_load_module(self, msg): + if msg.is_dead: + return + + tup = msg.unpickle() + fullname = tup[0] + _v and self._log.debug('received %s', fullname) + + self._lock.acquire() + try: + self._cache[fullname] = tup + if sys.version_info < (2, 5) and tup[2] is not None: + _update_linecache( + path='master:' + tup[2], + data=zlib.decompress(tup[3]) + ) + callbacks = self._callbacks.pop(fullname, []) + finally: + self._lock.release() + + for callback in callbacks: + callback() + + def _request_module(self, fullname, callback): + self._lock.acquire() + try: + present = fullname in self._cache + if not present: + funcs = self._callbacks.get(fullname) + if funcs is not None: + _v and self._log.debug('existing request for %s in flight', + fullname) + funcs.append(callback) + else: + _v and self._log.debug('sending new %s request to parent', + fullname) + self._callbacks[fullname] = [callback] + self._context.send( + Message(data=b(fullname), handle=GET_MODULE) + ) + finally: + self._lock.release() + + if present: + callback() + + def create_module(self, spec): + """ + Return a module object for the given ModuleSpec. + + Implements PEP-451 importlib.abc.Loader API introduced in Python 3.4. + Unlike Loader.load_module() this shouldn't populate sys.modules or + set module attributes. Both are done by Python. + """ + self._log.debug('Creating module for %r', spec) + + # FIXME Should this be done in find_spec()? Can it? + self._refuse_imports(spec.name) + + # FIXME "create_module() should properly handle the case where it is + # called more than once for the same spec/module." -- PEP-451 + event = threading.Event() + self._request_module(spec.name, callback=event.set) + event.wait() + + # 0:fullname 1:pkg_present 2:path 3:compressed 4:related + _, pkg_present, path, _, _ = self._cache[spec.name] + + if path is None: + raise ImportError(self.absent_msg % (spec.name)) + + spec.origin = self.get_filename(spec.name) + if pkg_present is not None: + # TODO Namespace packages + spec.submodule_search_locations = [] + self._present[spec.name] = pkg_present + + module = types.ModuleType(spec.name) + # FIXME create_module() shouldn't initialise module attributes + module.__file__ = spec.origin + return module + + def exec_module(self, module): + """ + Execute the module to initialise it. Don't return anything. + + Implements PEP-451 importlib.abc.Loader API, introduced in Python 3.4. + """ + name = module.__spec__.name + origin = module.__spec__.origin + self._log.debug('Executing %s from %s', name, origin) + source = self.get_source(name) + try: + # Compile the source into a code object. Don't add any __future__ + # flags and don't inherit any from this module. + # FIXME Should probably be exposed as get_code() + code = compile(source, origin, 'exec', flags=0, dont_inherit=True) + except SyntaxError: + # FIXME Why is this LOG, rather than self._log? + LOG.exception('while importing %r', name) + raise + + exec(code, module.__dict__) + + def load_module(self, fullname): + """ + Return the loaded module specified by fullname. + + Implements importlib.abc.Loader.load_module(). + Deprecated in Python 3.4+, replaced by create_module() & exec_module(). + """ + fullname = to_text(fullname) + _v and self._log.debug('requesting %s', fullname) + self._refuse_imports(fullname) + + event = threading.Event() + self._request_module(fullname, event.set) + event.wait() + + # 0:fullname 1:pkg_present 2:path 3:compressed 4:related + _, pkg_present, path, _, _ = self._cache[fullname] + if path is None: + raise ModuleNotFoundError(self.absent_msg % (fullname,)) + + mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) + mod.__file__ = self.get_filename(fullname) + mod.__loader__ = self + if pkg_present is not None: # it's a package. + mod.__path__ = [] + mod.__package__ = fullname + self._present[fullname] = pkg_present + else: + mod.__package__ = str_rpartition(fullname, '.')[0] or None + + if sys.version_info < (3, 0) and mod.__package__: + # 2.x requires __package__ to be exactly a string. + mod.__package__, _ = _codecs.utf_8_encode(mod.__package__) + + source = self.get_source(fullname) + try: + code = compile(source, mod.__file__, 'exec', 0, 1) + except SyntaxError: + LOG.exception('while importing %r', fullname) + raise + + if sys.version_info >= (3, 0): + exec(code, vars(mod)) + else: + exec('exec code in vars(mod)') + + # #590: if a module replaces itself in sys.modules during import, below + # is necessary. This matches PyImport_ExecCodeModuleEx() + return sys.modules.get(fullname, mod) + + def get_filename(self, fullname): + if fullname in self._cache: + path = self._cache[fullname][2] + if path is None: + # If find_loader() returns self but a subsequent master RPC + # reveals the module can't be loaded, and so load_module() + # throws ImportError, on Python 3.x it is still possible for + # the loader to be called to fetch metadata. + raise ModuleNotFoundError(self.absent_msg % (fullname,)) + return u'master:' + self._cache[fullname][2] + + def get_source(self, fullname): + if fullname in self._cache: + compressed = self._cache[fullname][3] + if compressed is None: + raise ModuleNotFoundError(self.absent_msg % (fullname,)) + + source = zlib.decompress(self._cache[fullname][3]) + if sys.version_info >= (3, 0): + return to_text(source) + return source + + def get_resource_reader(self, fullname): + """ + Optional :class:`importlib.abc.Loader` method to provide data (files) + bundled with a package. + + Introduced in Python 3.7. + """ + return ResourceReader(self._resource_requester, fullname) + + +class ResourceRequester(object): + """ + Requests Python package resources from upstreams & caches responses. + """ + def __init__(self, router, context): + self._context = context + self._lock = threading.Lock() + self._callbacks = {} + self._cache = {} + router.add_handler( + fn=self._on_load_resource, + handle=LOAD_RESOURCE, + policy=has_parent_authority, + ) + + def _get_resource(self, fullname, resource): + event = threading.Event() + self._request_resource(fullname, resource, event.set) + event.wait() + content = self._cache[(fullname, resource)] + return content + + def _request_resource(self, fullname, resource, callback): + self._lock.acquire() + try: + present = (fullname, resource) in self._cache + if not present: + callbacks = self._callbacks.get((fullname, resource)) + if callbacks is not None: + callbacks.append(callback) + else: + self._callbacks[(fullname, resource)] = [callback] + msg = Message.pickled( + (fullname, resource), + handle=GET_RESOURCE, + ) + self._context.send(msg) + finally: + self._lock.release() + + if present: + callback() + + def _on_load_resource(self, msg): + if msg.is_dead: + return + (fullname, resource), content = msg.unpickle_iter() + + self._lock.acquire() + try: + self._cache[(fullname, resource)] = content + callbacks = self._callbacks.pop((fullname, resource), []) + finally: + self._lock.release() + + for callback in callbacks: + callback() + + +class ResourceReader(object): + """ + Implements :class:`importlib.resource.abc.ResourceReader` (Python >= 3.7). + """ + def __init__(self, requester, fullname): + self._requester = requester + self._fullname = fullname + + def open_resource(self, resource): + content = self._requester._get_resource(self._fullname, resource) + if content is None: + raise FileNotFoundError + return BytesIO(content) + + def resource_path(self, resource): + raise FileNotFoundError + + def is_resource(self, name): + content = self._requester._get_resource(self._fullname, name) + return bool(content is not None) + + def contents(self): + raise NotImplementedError + + +class LogHandler(logging.Handler): + """ + A :class:`logging.Handler` subclass that arranges for :data:`FORWARD_LOG` + messages to be sent to a parent context in response to logging messages + generated by the current context. This is installed by default in child + contexts during bootstrap, so that :mod:`logging` events can be viewed and + managed centrally in the master process. + + The handler is initially *corked* after construction, such that it buffers + messages until :meth:`uncork` is called. This allows logging to be + installed prior to communication with the target being available, and + avoids any possible race where early log messages might be dropped. + + :param mitogen.core.Context context: + The context to send log messages towards. At present this is always + the master process. + """ + def __init__(self, context): + logging.Handler.__init__(self) + self.context = context + self.local = threading.local() + self._buffer = [] + # Private synchronization is needed while corked, to ensure no + # concurrent call to _send() exists during uncork(). + self._buffer_lock = threading.Lock() + + def uncork(self): + """ + #305: during startup :class:`LogHandler` may be installed before it is + possible to route messages, therefore messages are buffered until + :meth:`uncork` is called by :class:`ExternalContext`. + """ + self._buffer_lock.acquire() + try: + self._send = self.context.send + for msg in self._buffer: + self._send(msg) + self._buffer = None + finally: + self._buffer_lock.release() + + def _send(self, msg): + self._buffer_lock.acquire() + try: + if self._buffer is None: + # uncork() may run concurrent to _send() + self._send(msg) + else: + self._buffer.append(msg) + finally: + self._buffer_lock.release() + + def emit(self, rec): + """ + Send a :data:`FORWARD_LOG` message towards the target context. + """ + if rec.name == 'mitogen.io' or \ + getattr(self.local, 'in_emit', False): + return + + self.local.in_emit = True + try: + msg = self.format(rec) + encoded = '%s\x00%s\x00%s' % (rec.name, rec.levelno, msg) + if isinstance(encoded, UnicodeType): + # Logging package emits both :( + encoded = encoded.encode('utf-8') + self._send(Message(data=encoded, handle=FORWARD_LOG)) + finally: + self.local.in_emit = False + + +class Stream(object): + """ + A :class:`Stream` is one readable and optionally one writeable file + descriptor (represented by :class:`Side`) aggregated alongside an + associated :class:`Protocol` that knows how to respond to IO readiness + events for those descriptors. + + Streams are registered with :class:`Broker`, and callbacks are invoked on + the broker thread in response to IO activity. When registered using + :meth:`Broker.start_receive` or :meth:`Broker._start_transmit`, the broker + may call any of :meth:`on_receive`, :meth:`on_transmit`, + :meth:`on_shutdown` or :meth:`on_disconnect`. + + It is expected that the :class:`Protocol` associated with a stream will + change over its life. For example during connection setup, the initial + protocol may be :class:`mitogen.parent.BootstrapProtocol` that knows how to + enter SSH and sudo passwords and transmit the :mod:`mitogen.core` source to + the target, before handing off to :class:`MitogenProtocol` when the target + process is initialized. + + Streams connecting to children are in turn aggregated by + :class:`mitogen.parent.Connection`, which contains additional logic for + managing any child process, and a reference to any separate ``stderr`` + :class:`Stream` connected to that process. + """ + #: 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 + + #: A :class:`Protocol` representing the protocol active on the stream. + protocol = None + + #: In parents, the :class:`mitogen.parent.Connection` instance. + conn = None + + #: The stream name. This is used in the :meth:`__repr__` output in any log + #: messages, it may be any descriptive string. + name = u'default' + + def set_protocol(self, protocol): + """ + Bind a :class:`Protocol` to this stream, by updating + :attr:`Protocol.stream` to refer to this stream, and updating this + stream's :attr:`Stream.protocol` to the refer to the protocol. Any + prior protocol's :attr:`Protocol.stream` is set to :data:`None`. + """ + if self.protocol: + self.protocol.stream = None + self.protocol = protocol + self.protocol.stream = self + + def accept(self, rfp, wfp): + """ + Attach a pair of file objects to :attr:`receive_side` and + :attr:`transmit_side`, after wrapping them in :class:`Side` instances. + :class:`Side` will call :func:`set_cloexec` on them. + + The same file object may be used for both sides. The default + :meth:`on_disconnect` is handles the possibility that only one + descriptor may need to be closed. + + :param file rfp: + The file object to receive from. + :param file wfp: + The file object to transmit to. + """ + self.receive_side = Side(self, rfp) + self.transmit_side = Side(self, wfp) + + def __repr__(self): + return "" % (self.name, id(self) & 0xffff,) + + def on_receive(self, broker): + """ + Invoked 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 they are registered using + :meth:`Broker.start_receive`, and the method must invoke + :meth:`on_disconnect` if reading produces an empty string. + + The default implementation reads :attr:`Protocol.read_size` bytes and + passes the resulting bytestring to :meth:`Protocol.on_receive`. If the + bytestring is 0 bytes, invokes :meth:`on_disconnect` instead. + """ + buf = self.receive_side.read(self.protocol.read_size) + if not buf: + LOG.debug('%r: empty read, disconnecting', self.receive_side) + return self.on_disconnect(broker) + + self.protocol.on_receive(broker, buf) + + def on_transmit(self, broker): + """ + Invoked 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 they are ever registerd with + :meth:`Broker._start_transmit`. + + The default implementation invokes :meth:`Protocol.on_transmit`. + """ + self.protocol.on_transmit(broker) + + def on_shutdown(self, broker): + """ + Invoked by :meth:`Broker.shutdown` to allow the stream time to + gracefully shutdown. + + The default implementation emits a ``shutdown`` signal before + invoking :meth:`on_disconnect`. + """ + fire(self, 'shutdown') + self.protocol.on_shutdown(broker) + + def on_disconnect(self, broker): + """ + Invoked by :class:`Broker` to force disconnect the stream during + shutdown, invoked by the default :meth:`on_shutdown` implementation, + and usually invoked by any subclass :meth:`on_receive` implementation + in response to a 0-byte read. + + The base implementation fires a ``disconnect`` event, then closes + :attr:`receive_side` and :attr:`transmit_side` after unregistering the + stream from the broker. + """ + fire(self, 'disconnect') + self.protocol.on_disconnect(broker) + + +class Protocol(object): + """ + Implement the program behaviour associated with activity on a + :class:`Stream`. The protocol in use may vary over a stream's life, for + example to allow :class:`mitogen.parent.BootstrapProtocol` to initialize + the connected child before handing it off to :class:`MitogenProtocol`. A + stream's active protocol is tracked in the :attr:`Stream.protocol` + attribute, and modified via :meth:`Stream.set_protocol`. + + Protocols do not handle IO, they are entirely reliant on the interface + provided by :class:`Stream` and :class:`Side`, allowing the underlying IO + implementation to be replaced without modifying behavioural logic. + """ + stream_class = Stream + + #: The :class:`Stream` this protocol is currently bound to, or + #: :data:`None`. + stream = None + + #: The size of the read buffer used by :class:`Stream` when this is the + #: active protocol for the stream. + read_size = CHUNK_SIZE + + @classmethod + def build_stream(cls, *args, **kwargs): + stream = cls.stream_class() + stream.set_protocol(cls(*args, **kwargs)) + return stream + + def __repr__(self): + return '%s(%s)' % ( + self.__class__.__name__, + self.stream and self.stream.name, + ) + + def on_shutdown(self, broker): + _v and LOG.debug('%r: shutting down', self) + self.stream.on_disconnect(broker) + + def on_disconnect(self, broker): + # Normally both sides an FD, so it is important that tranmit_side is + # deregistered from Poller before closing the receive side, as pollers + # like epoll and kqueue unregister all events on FD close, causing + # subsequent attempt to unregister the transmit side to fail. + LOG.debug('%r: disconnecting', self) + broker.stop_receive(self.stream) + if self.stream.transmit_side: + broker._stop_transmit(self.stream) + + self.stream.receive_side.close() + if self.stream.transmit_side: + self.stream.transmit_side.close() + + +class DelimitedProtocol(Protocol): + """ + Provide a :meth:`Protocol.on_receive` implementation for protocols that are + delimited by a fixed string, like text based protocols. Each message is + passed to :meth:`on_line_received` as it arrives, with incomplete messages + passed to :meth:`on_partial_line_received`. + + When emulating user input it is often necessary to respond to incomplete + lines, such as when a "Password: " prompt is sent. + :meth:`on_partial_line_received` may be called repeatedly with an + increasingly complete message. When a complete message is finally received, + :meth:`on_line_received` will be called once for it before the buffer is + discarded. + + If :func:`on_line_received` returns :data:`False`, remaining data is passed + unprocessed to the stream's current protocol's :meth:`on_receive`. This + allows switching from line-oriented to binary while the input buffer + contains both kinds of data. + """ + #: The delimiter. Defaults to newline. + delimiter = b('\n') + _trailer = b('') + + def on_receive(self, broker, buf): + _vv and IOLOG.debug('%r.on_receive()', self) + stream = self.stream + self._trailer, cont = mitogen.core.iter_split( + buf=self._trailer + buf, + delim=self.delimiter, + func=self.on_line_received, + ) + + if self._trailer: + if cont: + self.on_partial_line_received(self._trailer) + else: + assert stream.protocol is not self, \ + 'stream protocol is no longer %r' % (self,) + stream.protocol.on_receive(broker, self._trailer) + + def on_line_received(self, line): + """ + Receive a line from the stream. + + :param bytes line: + The encoded line, excluding the delimiter. + :returns: + :data:`False` to indicate this invocation modified the stream's + active protocol, and any remaining buffered data should be passed + to the new protocol's :meth:`on_receive` method. + + Any other return value is ignored. + """ + pass + + def on_partial_line_received(self, line): + """ + Receive a trailing unterminated partial line from the stream. + + :param bytes line: + The encoded partial line. + """ + pass + + +class BufferedWriter(object): + """ + Implement buffered output while avoiding quadratic string operations. This + is currently constructed by each protocol, in future it may become fixed + for each stream instead. + """ + def __init__(self, broker, protocol): + self._broker = broker + self._protocol = protocol + self._buf = collections.deque() + self._len = 0 + + def write(self, s): + """ + Transmit `s` immediately, falling back to enqueuing it and marking the + stream writeable if no OS buffer space is available. + """ + if not self._len: + # Modifying epoll/Kqueue state is expensive, as are needless broker + # loops. Rather than wait for writeability, just write immediately, + # and fall back to the broker loop on error or full buffer. + try: + n = self._protocol.stream.transmit_side.write(s) + if n: + if n == len(s): + return + s = s[n:] + except OSError: + pass + + self._broker._start_transmit(self._protocol.stream) + self._buf.append(s) + self._len += len(s) + + def on_transmit(self, broker): + """ + Respond to stream writeability by retrying previously buffered + :meth:`write` calls. + """ + if self._buf: + buf = self._buf.popleft() + written = self._protocol.stream.transmit_side.write(buf) + if not written: + _v and LOG.debug('disconnected during write to %r', self) + self._protocol.stream.on_disconnect(broker) + return + elif written != len(buf): + self._buf.appendleft(BufferType(buf, written)) + + _vv and IOLOG.debug('transmitted %d bytes to %r', written, self) + self._len -= written + + if not self._buf: + broker._stop_transmit(self._protocol.stream) + + +class Side(object): + """ + Represent one side of a :class:`Stream`. This allows unidirectional (e.g. + pipe) and bidirectional (e.g. socket) streams to operate identically. + + Sides are also responsible for tracking the open/closed state of the + underlying FD, preventing erroneous duplicate calls to :func:`os.close` due + to duplicate :meth:`Stream.on_disconnect` calls, which would otherwise risk + silently succeeding by closing an unrelated descriptor. For this reason, it + is crucial only one file object exists per unique descriptor. + + :param mitogen.core.Stream stream: + The stream this side is associated with. + :param object fp: + The file or socket object managing the underlying file descriptor. Any + object may be used that supports `fileno()` and `close()` methods. + :param bool cloexec: + If :data:`True`, the descriptor has its :data:`fcntl.FD_CLOEXEC` flag + enabled using :func:`fcntl.fcntl`. + :param bool keep_alive: + If :data:`True`, the continued existence of this side will extend the + shutdown grace period until it has been unregistered from the broker. + """ + _fork_refs = weakref.WeakValueDictionary() + closed = False + + def __init__(self, stream, fp, cloexec=True, keep_alive=True): + #: The :class:`Stream` for which this is a read or write side. + self.stream = stream + # File or socket object responsible for the lifetime of its underlying + # file descriptor. + self.fp = fp + #: Integer file descriptor to perform IO on, or :data:`None` if + #: :meth:`close` has been called. This is saved separately from the + #: file object, since :meth:`file.fileno` cannot be called on it after + #: it has been closed. + self.fd = fp.fileno() + #: 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: + set_cloexec(self.fd) + + def __repr__(self): + return '' % ( + self.stream.name or repr(self.stream), + self.fd + ) + + @classmethod + def _on_fork(cls): + while cls._fork_refs: + _, side = cls._fork_refs.popitem() + _vv and IOLOG.debug('Side._on_fork() closing %r', side) + side.close() + + def close(self): + """ + Call :meth:`file.close` on :attr:`fp` if it is not :data:`None`, + then set it to :data:`None`. + """ + _vv and IOLOG.debug('%r.close()', self) + if not self.closed: + self.closed = True + self.fp.close() + + 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 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(). + return b('') + s, disconnected = io_op(os.read, self.fd, n) + if disconnected: + LOG.debug('%r: disconnected during read: %s', self, disconnected) + return b('') + 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 conditions. + + :returns: + Number of bytes written, or :data:`None` if disconnection was + detected. + """ + if self.closed: + # Don't touch the handle after close, it may be reused elsewhere. + return None + + written, disconnected = io_op(os.write, self.fd, s) + if disconnected: + LOG.debug('%r: disconnected during write: %s', self, disconnected) + return None + return written + + +class MitogenProtocol(Protocol): + """ + :class:`Protocol` implementing mitogen's :ref:`stream protocol + `. + """ + #: If not :data:`False`, indicates the stream has :attr:`auth_id` set and + #: its value is the same as :data:`mitogen.context_id` or appears in + #: :data:`mitogen.parent_ids`. + is_privileged = False + + #: Invoked as `on_message(stream, msg)` each message received from the + #: peer. + on_message = None + + def __init__(self, router, remote_id, auth_id=None, + local_id=None, parent_ids=None): + self._router = router + self.remote_id = remote_id + #: If not :data:`None`, :class:`Router` stamps this into + #: :attr:`Message.auth_id` of every message received on this stream. + self.auth_id = auth_id + + if parent_ids is None: + parent_ids = mitogen.parent_ids + if local_id is None: + local_id = mitogen.context_id + + self.is_privileged = ( + (remote_id in parent_ids) or + auth_id in ([local_id] + parent_ids) + ) + self.sent_modules = set(['mitogen', 'mitogen.core']) + self._input_buf = collections.deque() + self._input_buf_len = 0 + self._writer = BufferedWriter(router.broker, self) + + #: 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 on_receive(self, broker, buf): + """ + Handle the next complete message on the stream. Raise + :class:`StreamError` on failure. + """ + _vv and IOLOG.debug('%r.on_receive()', self) + if self._input_buf and self._input_buf_len < 128: + self._input_buf[0] += buf + else: + self._input_buf.append(buf) + + self._input_buf_len += len(buf) + while self._receive_one(broker): + pass + + corrupt_msg = ( + '%s: Corruption detected: frame signature incorrect. This likely means' + ' some external process is interfering with the connection. Received:' + '\n\n' + '%r' + ) + + def _receive_one(self, broker): + if self._input_buf_len < Message.HEADER_LEN: + return False + + msg = Message() + msg.router = self._router + (msg.enc, msg.dst_id, msg.src_id, msg.auth_id, + msg.handle, msg.reply_to, msg_len) = struct.unpack( + Message.HEADER_FMT, + self._input_buf[0][:Message.HEADER_LEN], + ) + + if msg.enc not in Message.ENCS: + LOG.error(self.corrupt_msg, self.stream.name, self._input_buf[0][:2048]) + self.stream.on_disconnect(broker) + return False + + if msg_len > self._router.max_message_size: + LOG.error('%r: Maximum message size exceeded (got %d, max %d)', + self, msg_len, self._router.max_message_size) + self.stream.on_disconnect(broker) + return False + + total_len = msg_len + Message.HEADER_LEN + if self._input_buf_len < total_len: + _vv and IOLOG.debug( + '%r: Input too short (want %d, got %d)', + self, msg_len, self._input_buf_len - Message.HEADER_LEN + ) + return False + + start = Message.HEADER_LEN + prev_start = start + remain = total_len + bits = [] + while remain: + buf = self._input_buf.popleft() + bit = buf[start:remain] + bits.append(bit) + remain -= len(bit) + start + prev_start = start + start = 0 + + msg.data = b('').join(bits) + self._input_buf.appendleft(buf[prev_start+len(bit):]) + self._input_buf_len -= total_len + self._router._async_route(msg, self.stream) + 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._writer._len + + def on_transmit(self, broker): + """ + Transmit buffered messages. + """ + _vv and IOLOG.debug('%r.on_transmit()', self) + self._writer.on_transmit(broker) + + def _send(self, msg): + _vv and IOLOG.debug('%r._send(%r)', self, msg) + self._writer.write(msg.pack()) + + def send(self, msg): + """ + Send `data` to `handle`, and tell the broker we have output. May be + called from any thread. + """ + self._router.broker.defer(self._send, msg) + + def on_shutdown(self, broker): + """ + Disable :class:`Protocol` immediate disconnect behaviour. + """ + _v and LOG.debug('%r: shutting down', self) + + +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 mitogen.core.Router router: + Router to emit messages through. + :param int context_id: + Context ID. + :param str name: + Context name. + """ + name = None + remote_name = None + + def __init__(self, router, context_id, name=None): + self.router = router + self.context_id = context_id + if name: + self.name = to_text(name) + + def __reduce__(self): + return _unpickle_context, (self.context_id, self.name) + + def on_disconnect(self): + _v and LOG.debug('%r: disconnecting', self) + 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. + """ + receiver = Receiver(self.router, persist=persist, respondent=self) + msg.dst_id = self.context_id + msg.reply_to = receiver.handle + + _v and LOG.debug('sending message to %r: %r', self, msg) + self.send(msg) + return receiver + + def call_service_async(self, service_name, method_name, **kwargs): + if isinstance(service_name, BytesType): + service_name = service_name.encode('utf-8') + elif not isinstance(service_name, UnicodeType): + service_name = service_name.name() # Service.name() + _v and LOG.debug('calling service %s.%s of %r, args: %r', + service_name, method_name, self, kwargs) + tup = (service_name, to_text(method_name), Kwargs(kwargs)) + msg = Message.pickled(tup, handle=CALL_SERVICE) + return self.send_async(msg) + + def send(self, msg): + """ + 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) + + def call_service(self, service_name, method_name, **kwargs): + recv = self.call_service_async(service_name, method_name, **kwargs) + return recv.get().unpickle() + + def send_await(self, 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: + Deserialized reply. + :raises TimeoutError: + No message was received and `deadline` passed. + """ + receiver = self.send_async(msg) + response = receiver.get(deadline) + data = response.unpickle() + _vv and IOLOG.debug('%r._send_await() -> %r', self, data) + return data + + def __repr__(self): + return 'Context(%s, %r)' % (self.context_id, self.name) + + +def _unpickle_context(context_id, name, router=None): + if not (isinstance(context_id, integer_types) and context_id >= 0 and ( + (name is None) or + (isinstance(name, UnicodeType) and len(name) < 100)) + ): + raise TypeError('cannot unpickle Context: bad input') + + 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): + """ + A poller manages OS file descriptors the user is waiting to become + available for IO. The :meth:`poll` method blocks the calling thread + until one or more become ready. + + Each descriptor has an associated `data` element, which is unique for each + readiness type, and defaults to being the same as the file descriptor. The + :meth:`poll` method yields the data associated with a descriptor, rather + than the descriptor itself, allowing concise loops like:: + + p = Poller() + p.start_receive(conn.fd, data=conn.on_read) + p.start_transmit(conn.fd, data=conn.on_write) + + for callback in p.poll(): + callback() # invoke appropriate bound instance method + + Pollers may be modified while :meth:`poll` is yielding results. Removals + are processed immediately, causing pending events for the descriptor to be + discarded. + + The :meth:`close` method must be called when a poller is discarded to avoid + a resource leak. + + Pollers may only be used by one thread at a time. + + This implementation uses :func:`select.select` for wider platform support. + That is considered an implementation detail. Previous versions have used + :func:`select.poll`. Future versions may decide at runtime. + """ + SUPPORTED = True + + #: Increments on every poll(). Used to version _rfds and _wfds. + _generation = 1 + + def __init__(self): + self._rfds = {} + self._wfds = {} + + def __repr__(self): + return '%s' % (type(self).__name__,) + + def _update(self, fd): + """ + Required by PollPoller subclass. + """ + pass + + @property + def readers(self): + """ + Return a list of `(fd, data)` tuples for every FD registered for + receive readiness. + """ + return list((fd, data) for fd, (data, gen) in self._rfds.items()) + + @property + def writers(self): + """ + Return a list of `(fd, data)` tuples for every FD registered for + transmit readiness. + """ + return list((fd, data) for fd, (data, gen) in self._wfds.items()) + + def close(self): + """ + Close any underlying OS resource used by the poller. + """ + pass + + def start_receive(self, fd, data=None): + """ + Cause :meth:`poll` to yield `data` when `fd` is readable. + """ + self._rfds[fd] = (data or fd, self._generation) + self._update(fd) + + def stop_receive(self, fd): + """ + Stop yielding readability events for `fd`. + + Redundant calls to :meth:`stop_receive` are silently ignored, this may + change in future. + """ + self._rfds.pop(fd, None) + self._update(fd) + + def start_transmit(self, fd, data=None): + """ + Cause :meth:`poll` to yield `data` when `fd` is writeable. + """ + self._wfds[fd] = (data or fd, self._generation) + self._update(fd) + + def stop_transmit(self, fd): + """ + Stop yielding writeability events for `fd`. + + Redundant calls to :meth:`stop_transmit` are silently ignored, this may + change in future. + """ + self._wfds.pop(fd, None) + self._update(fd) + + def _poll(self, timeout): + (rfds, wfds, _), _ = io_op(select.select, + self._rfds, + self._wfds, + (), timeout + ) + + for fd in rfds: + _vv and IOLOG.debug('%r: POLLIN for %r', self, 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) + data, gen = self._wfds.get(fd, (None, None)) + if gen and gen < self._generation: + yield data + + def poll(self, timeout=None): + """ + Block the calling thread until one or more FDs are ready for IO. + + :param float timeout: + If not :data:`None`, seconds to wait without an event before + returning an empty iterable. + :returns: + Iterable of `data` elements associated with ready FDs. + """ + _vv and IOLOG.debug('%r.poll(%r)', self, timeout) + self._generation += 1 + return self._poll(timeout) + + +class Latch(object): + """ + 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 + :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. + + See :ref:`waking-sleeping-threads` for further discussion. + """ + #: The :class:`Poller` implementation to use. Instances are short lived so + #: prefer :class:`mitogen.parent.PollPoller` if it's available, otherwise + #: :class:`mitogen.core.Poller`. They don't need syscalls to create, + #: configure, or destroy. Replaced during import of :mod:`mitogen.parent`. + poller_class = Poller + + #: If not :data:`None`, a function invoked as `notify(latch)` after a + #: successful call to :meth:`put`. The function is invoked on the + #: :meth:`put` caller's thread, which may be the :class:`Broker` thread, + #: therefore it must not block. Used by :class:`mitogen.select.Select` to + #: efficiently implement waiting on multiple event sources. + notify = None + + # The _cls_ prefixes here are to make it crystal clear in the code which + # state mutation isn't covered by :attr:`_lock`. + + #: List of reusable :func:`socket.socketpair` tuples. The list is mutated + #: from multiple threads, the only safe operations are `append()` and + #: `pop()`. + _cls_idle_socketpairs = [] + + #: List of every socket object that must be closed by :meth:`_on_fork`. + #: Inherited descriptors cannot be reused, as the duplicated handles + #: reference the same underlying kernel object in use by the parent. + _cls_all_sockets = [] + + def __init__(self): + self.closed = False + self._lock = threading.Lock() + #: List of unconsumed enqueued items. + self._queue = [] + #: List of `(wsock, cookie)` awaiting an element, where `wsock` is the + #: socketpair's write side, and `cookie` is the string to write. + self._sleeping = [] + #: Number of elements of :attr:`_sleeping` that have already been + #: woken, and have a corresponding element index from :attr:`_queue` + #: assigned to them. + self._waking = 0 + + @classmethod + def _on_fork(cls): + """ + Clean up any files belonging to the parent process after a fork. + """ + cls._cls_idle_socketpairs = [] + while cls._cls_all_sockets: + cls._cls_all_sockets.pop().close() + + def close(self): + """ + Mark the latch as closed, and cause every sleeping thread to be woken, + with :class:`mitogen.core.LatchError` raised in each thread. + """ + self._lock.acquire() + try: + self.closed = True + while self._waking < len(self._sleeping): + wsock, cookie = self._sleeping[self._waking] + self._wake(wsock, cookie) + self._waking += 1 + finally: + self._lock.release() + + def size(self): + """ + Return the number of items currently buffered. + + As with :class:`Queue.Queue`, `0` may be returned even though a + subsequent call to :meth:`get` will succeed, since a message may be + posted at any moment between :meth:`size` and :meth:`get`. + + As with :class:`Queue.Queue`, `>0` 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:`size` and :meth:`get`. + + :raises LatchError: + The latch has already been marked closed. + """ + self._lock.acquire() + try: + if self.closed: + raise LatchError() + return len(self._queue) + finally: + self._lock.release() + + def empty(self): + """ + Return `size() == 0`. + + .. deprecated:: 0.2.8 + Use :meth:`size` instead. + + :raises LatchError: + The latch has already been marked closed. + """ + return self.size() == 0 + + def _get_socketpair(self): + """ + Return an unused socketpair, creating one if none exist. + """ + try: + return self._cls_idle_socketpairs.pop() # pop() must be atomic + except IndexError: + rsock, wsock = socketpair() + rsock.setblocking(False) + set_cloexec(rsock.fileno()) + set_cloexec(wsock.fileno()) + self._cls_all_sockets.extend((rsock, wsock)) + return rsock, wsock + + COOKIE_MAGIC, = struct.unpack('L', b('LTCH') * (struct.calcsize('L')//4)) + COOKIE_FMT = '>Qqqq' # #545: id() and get_ident() may exceed long on armhfp. + COOKIE_SIZE = struct.calcsize(COOKIE_FMT) + + def _make_cookie(self): + """ + Return a string encoding the ID of the process, instance and thread. + This disambiguates legitimate wake-ups, accidental writes to the FD, + and buggy internal FD sharing. + """ + return struct.pack(self.COOKIE_FMT, self.COOKIE_MAGIC, + os.getpid(), id(self), thread.get_ident()) + + def get(self, timeout=None, block=True): + """ + Return the next enqueued object, or sleep waiting for one. + + :param float timeout: + If not :data:`None`, specifies a timeout in seconds. + + :param bool block: + If :data:`False`, immediately raise + :class:`mitogen.core.TimeoutError` if the latch is empty. + + :raises mitogen.core.LatchError: + :meth:`close` has been called, and the object is no longer valid. + + :raises mitogen.core.TimeoutError: + Timeout was reached. + + :returns: + The de-queued object. + """ + _vv and IOLOG.debug('%r.get(timeout=%r, block=%r)', + self, timeout, block) + self._lock.acquire() + try: + if self.closed: + raise LatchError() + i = len(self._sleeping) + if len(self._queue) > i: + _vv and IOLOG.debug('%r.get() -> %r', self, self._queue[i]) + return self._queue.pop(i) + if not block: + raise TimeoutError() + rsock, wsock = self._get_socketpair() + cookie = self._make_cookie() + self._sleeping.append((wsock, cookie)) + finally: + self._lock.release() + + poller = self.poller_class() + poller.start_receive(rsock.fileno()) + try: + return self._get_sleep(poller, timeout, block, rsock, wsock, cookie) + finally: + poller.close() + + def _get_sleep(self, poller, timeout, block, rsock, wsock, cookie): + """ + When a result is not immediately available, sleep waiting for + :meth:`put` to write a byte to our socket pair. + """ + _vv and IOLOG.debug( + '%r._get_sleep(timeout=%r, block=%r, fd=%d/%d)', + self, timeout, block, rsock.fileno(), wsock.fileno() + ) + + e = None + try: + list(poller.poll(timeout)) + except Exception: + e = sys.exc_info()[1] + + self._lock.acquire() + try: + i = self._sleeping.index((wsock, cookie)) + del self._sleeping[i] + + try: + got_cookie = rsock.recv(self.COOKIE_SIZE) + except socket.error: + e2 = sys.exc_info()[1] + if e2.args[0] == errno.EAGAIN: + e = TimeoutError() + else: + e = e2 + + self._cls_idle_socketpairs.append((rsock, wsock)) + if e: + raise e + + assert cookie == got_cookie, ( + "Cookie incorrect; got %r, expected %r" + % (binascii.hexlify(got_cookie), + binascii.hexlify(cookie)) + ) + assert i < self._waking, ( + "Cookie correct, but no queue element assigned." + ) + self._waking -= 1 + if self.closed: + raise LatchError() + _vv and IOLOG.debug('%r.get() wake -> %r', self, self._queue[i]) + return self._queue.pop(i) + finally: + self._lock.release() + + def put(self, obj=None): + """ + Enqueue an object, waking the first thread waiting for a result, if one + exists. + + :param obj: + Object to enqueue. Defaults to :data:`None` as a convenience when + using :class:`Latch` only for synchronization. + :raises mitogen.core.LatchError: + :meth:`close` has been called, and the object is no longer valid. + """ + _vv and IOLOG.debug('%r.put(%r)', self, obj) + self._lock.acquire() + try: + if self.closed: + raise LatchError() + self._queue.append(obj) + + wsock = None + if self._waking < len(self._sleeping): + wsock, cookie = self._sleeping[self._waking] + self._waking += 1 + _vv and IOLOG.debug('%r.put() -> waking wfd=%r', + self, wsock.fileno()) + elif self.notify: + self.notify(self) + finally: + self._lock.release() + + if wsock: + self._wake(wsock, cookie) + + def _wake(self, wsock, cookie): + written, disconnected = io_op(os.write, wsock.fileno(), cookie) + assert written == len(cookie) and not disconnected + + def __repr__(self): + return 'Latch(%#x, size=%d, t=%r)' % ( + id(self), + len(self._queue), + threading__thread_name(threading__current_thread()), + ) + + +class Waker(Protocol): + """ + :class:`Protocol` implementing the `UNIX self-pipe trick`_. Used to wake + :class:`Broker` when another thread needs to modify its state, by enqueing + a function call to run on the :class:`Broker` thread. + + .. _UNIX self-pipe trick: https://cr.yp.to/docs/selfpipe.html + """ + read_size = 1 + broker_ident = None + + @classmethod + def build_stream(cls, broker): + stream = super(Waker, cls).build_stream(broker) + rfp, wfp = pipe(blocking=False) + stream.accept(rfp, wfp) + return stream + + def __init__(self, broker): + self._broker = broker + self._deferred = collections.deque() + + def __repr__(self): + return 'Waker(fd=%r/%r)' % ( + self.stream.receive_side and self.stream.receive_side.fd, + self.stream.transmit_side and self.stream.transmit_side.fd, + ) + + @property + def keep_alive(self): + """ + Prevent immediate Broker shutdown while deferred functions remain. + """ + return len(self._deferred) + + def on_receive(self, broker, buf): + """ + 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) + while True: + try: + func, args, kwargs = self._deferred.popleft() + except IndexError: + return + + try: + func(*args, **kwargs) + except Exception: + LOG.exception('defer() crashed: %r(*%r, **%r)', + func, args, kwargs) + 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.stream.transmit_side.write(b(' ')) + except OSError: + e = sys.exc_info()[1] + if e.args[0] not in (errno.EBADF, errno.EWOULDBLOCK): + raise + + broker_shutdown_msg = ( + "An attempt was made to enqueue a message with a Broker that has " + "already exitted. It is likely your program called Broker.shutdown() " + "too early." + ) + + def defer(self, func, *args, **kwargs): + """ + Arrange for `func()` to execute on the broker thread. This function + returns immediately without waiting the result of `func()`. Use + :meth:`defer_sync` to block until a result is available. + + :raises mitogen.core.Error: + :meth:`defer` was called after :class:`Broker` has begun shutdown. + """ + if thread.get_ident() == self.broker_ident: + _vv and IOLOG.debug('%r.defer() [immediate]', self) + return func(*args, **kwargs) + if self._broker._exitted: + raise Error(self.broker_shutdown_msg) + + _vv and IOLOG.debug('%r.defer() [fd=%r]', self, + self.stream.transmit_side.fd) + self._deferred.append((func, args, kwargs)) + self._wake() + + +class IoLoggerProtocol(DelimitedProtocol): + """ + Attached to one end of a socket pair whose other end overwrites one of the + standard ``stdout`` or ``stderr`` file descriptors in a child context. + Received data is split up into lines, decoded as UTF-8 and logged to the + :mod:`logging` package as either the ``stdout`` or ``stderr`` logger. + + Logging in child contexts is in turn forwarded to the master process using + :class:`LogHandler`. + """ + @classmethod + def build_stream(cls, name, dest_fd): + """ + Even though the file descriptor `dest_fd` will hold the opposite end of + the socket open, we must keep a separate dup() of it (i.e. wsock) in + case some code decides to overwrite `dest_fd` later, which would + prevent break :meth:`on_shutdown` from calling :meth:`shutdown() + ` on it. + """ + # Leave wsock & dest_fd blocking, so the subprocess will have sane stdio + rsock, wsock = socketpair() + os.dup2(wsock.fileno(), dest_fd) + stream = super(IoLoggerProtocol, cls).build_stream(name) + stream.name = name + stream.accept(rsock, wsock) + return stream + + def __init__(self, name): + self._log = logging.getLogger(name) + # #453: prevent accidental log initialization in a child creating a + # feedback loop. + self._log.propagate = False + self._log.handlers = logging.getLogger().handlers[:] + + def on_shutdown(self, broker): + """ + Shut down the write end of the socket, preventing any further writes to + it by this process, or subprocess that inherited it. This allows any + remaining kernel-buffered data to be drained during graceful shutdown + without the buffer continuously refilling due to some out of control + child process. + """ + _v and LOG.debug('%r: shutting down', self) + if not IS_WSL: + # #333: WSL generates invalid readiness indication on shutdown(). + # This modifies the *kernel object* inherited by children, causing + # EPIPE on subsequent writes to any dupped FD in any process. The + # read side can then drain completely of prior buffered data. + self.stream.transmit_side.fp.shutdown(socket.SHUT_WR) + self.stream.transmit_side.close() + + def on_line_received(self, line): + """ + Decode the received line as UTF-8 and pass it to the logging framework. + """ + self._log.info('%s', line.decode('utf-8', 'replace')) + + +class Router(object): + """ + Route messages between contexts, and invoke local handlers for messages + addressed to this context. :meth:`Router.route() ` straddles the + :class:`Broker` thread 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. + """ + #: The :class:`mitogen.core.Context` subclass to use when constructing new + #: :class:`Context` objects in :meth:`myself` and :meth:`context_by_id`. + #: Permits :class:`Router` subclasses to extend the :class:`Context` + #: interface, as done in :class:`mitogen.parent.Router`. + 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 + + duplicate_handle_msg = 'cannot register a handle that already exists' + 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_exit_msg = 'Broker has exitted' + no_route_msg = 'no route to %r, my ID is %r' + unidirectional_msg = ( + 'routing mode prevents forward of message from context %d to ' + 'context %d via context %d' + ) + + def __init__(self, broker): + self.broker = broker + listen(broker, 'exit', self._on_broker_exit) + self._setup_logging() + + self._write_lock = threading.Lock() + #: context ID -> Stream; must hold _write_lock to edit or iterate + self._stream_by_id = {} + #: List of contexts to notify of shutdown; must hold _write_lock + self._context_by_id = {} + 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): + return 'Router(%r)' % (self.broker,) + + def _setup_logging(self): + """ + This is done in the :class:`Router` constructor for historical reasons. + It must be called before ExternalContext logs its first messages, but + after logging has been setup. It must also be called when any router is + constructed for a consumer app. + """ + # Here seems as good a place as any. + global _v, _vv + _v = logging.getLogger().level <= logging.DEBUG + _vv = IOLOG.level <= logging.DEBUG + + def _on_del_route(self, msg): + """ + 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. + """ + if msg.is_dead: + return + + target_id_s, _, name = bytes_partition(msg.data, b(':')) + target_id = int(target_id_s, 10) + LOG.error('%r: deleting route to %s (%d)', + self, to_text(name), target_id) + context = self._context_by_id.get(target_id) + if context: + fire(context, 'disconnect') + else: + LOG.debug('DEL_ROUTE for unknown ID %r: %r', target_id, msg) + + def _on_stream_disconnect(self, stream): + notify = [] + self._write_lock.acquire() + try: + for context in list(self._context_by_id.values()): + stream_ = self._stream_by_id.get(context.context_id) + if stream_ is stream: + del self._stream_by_id[context.context_id] + notify.append(context) + finally: + self._write_lock.release() + + # Happens outside lock as e.g. RouteMonitor wants the same lock. + for context in notify: + context.on_disconnect() + + def _on_broker_exit(self): + """ + Called prior to broker exit, informs callbacks registered with + :meth:`add_handler` the connection is dead. + """ + _v and LOG.debug('%r: broker has exitted', self) + while self._handle_map: + _, (_, func, _, _) = self._handle_map.popitem() + func(Message.dead(self.broker_exit_msg)) + + def myself(self): + """ + Return a :class:`Context` referring to the current process. Since + :class:`Context` is serializable, this is convenient to use in remote + function call parameter lists. + """ + return self.context_class( + router=self, + context_id=mitogen.context_id, + name='self', + ) + + def context_by_id(self, context_id, via_id=None, create=True, name=None): + """ + Return or construct a :class:`Context` given its ID. An internal + mapping of ID to the canonical :class:`Context` representing that ID, + so that :ref:`signals` can be raised. + + This may be called from any thread, lookup and construction are atomic. + + :param int context_id: + The context ID to look up. + :param int via_id: + If the :class:`Context` does not already exist, set its + :attr:`Context.via` to the :class:`Context` matching this ID. + :param bool create: + If the :class:`Context` does not already exist, create it. + :param str name: + If the :class:`Context` does not already exist, set its name. + + :returns: + :class:`Context`, or return :data:`None` if `create` is + :data:`False` and no :class:`Context` previously existed. + """ + context = self._context_by_id.get(context_id) + if context: + return context + + if create and via_id is not None: + via = self.context_by_id(via_id) + else: + via = None + + self._write_lock.acquire() + try: + context = self._context_by_id.get(context_id) + if create and not context: + context = self.context_class(self, context_id, name=name) + context.via = via + self._context_by_id[context_id] = context + finally: + self._write_lock.release() + + 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('%s: registering %r to stream %r', + self, context, stream) + self._write_lock.acquire() + try: + self._stream_by_id[context.context_id] = stream + self._context_by_id[context.context_id] = context + finally: + self._write_lock.release() + + self.broker.start_receive(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. If the parent is disconnected, + or when running in the master context, return :data:`None` instead. + + This can be used from any thread, but its output is only meaningful + from the context of the :class:`Broker` thread, as disconnection or + replacement could happen in parallel on the broker thread at any + moment. + """ + return ( + self._stream_by_id.get(dst_id) or + 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. + """ + _, _, _, 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, + overwrite=False): + """ + Invoke `fn(msg)` on the :class:`Broker` thread 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:`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. + + :param bool overwrite: + If :data:`True`, allow existing handles to be silently overwritten. + + :return: + `handle`, or if `handle` was :data:`None`, the newly allocated + handle. + :raises Error: + Attemp to register handle that was already registered. + """ + handle = handle or next(self._last_handle) + _vv and IOLOG.debug('%r.add_handler(%r, %r, %r)', self, fn, handle, persist) + if handle in self._handle_map and not overwrite: + raise Error(self.duplicate_handle_msg) + + self._handle_map[handle] = persist, fn, policy, respondent + if respondent: + 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(self.respondent_disconnect_msg)) + del self._handle_map[handle] + + def _maybe_send_dead(self, unreachable, msg, reason, *args): + """ + Send a dead message to either the original sender or the intended + recipient of `msg`, if the original sender was expecting a reply + (because its `reply_to` was set), otherwise assume the message is a + reply of some sort, and send the dead message to the original + destination. + + :param bool unreachable: + If :data:`True`, the recipient is known to be dead or routing + failed due to a security precaution, so don't attempt to fallback + to sending the dead message to the recipient if the original sender + did not include a reply address. + :param mitogen.core.Message msg: + Message that triggered the dead message. + :param str reason: + Human-readable error reason. + :param tuple args: + Elements to interpolate with `reason`. + """ + 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(reason=reason), router=self) + elif not unreachable: + self._async_route( + Message.dead( + dst_id=msg.dst_id, + handle=msg.handle, + reason=reason, + ) + ) + + def _invoke(self, msg, stream): + # IOLOG.debug('%r._invoke(%r)', self, msg) + try: + persist, fn, policy, respondent = self._handle_map[msg.handle] + except KeyError: + self._maybe_send_dead(True, msg, reason=self.invalid_handle_msg) + return + + if respondent and not (msg.is_dead or + msg.src_id == respondent.context_id): + self._maybe_send_dead(True, msg, 'reply from unexpected context') + return + + if policy and not policy(msg, stream): + self._maybe_send_dead(True, msg, self.refused_msg) + return + + if not persist: + self.del_handler(msg.handle) + + try: + fn(msg) + except Exception: + LOG.exception('%r._invoke(%r): %r crashed', self, msg, fn) + + 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 :class:`Broker` 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: + self._maybe_send_dead(False, msg, self.too_large_msg % ( + self.max_message_size, + )) + return + + parent_stream = self._stream_by_id.get(mitogen.parent_id) + src_stream = self._stream_by_id.get(msg.src_id, parent_stream) + + # When the ingress stream is known, verify the message was received on + # the same as the stream we would expect to receive messages from the + # src_id and auth_id. This is like Reverse Path Filtering in IP, and + # ensures messages from a privileged context cannot be spoofed by a + # child. + if in_stream: + auth_stream = self._stream_by_id.get(msg.auth_id, parent_stream) + if in_stream != auth_stream: + LOG.error('%r: bad auth_id: got %r via %r, not %r: %r', + self, msg.auth_id, in_stream, auth_stream, msg) + return + + if msg.src_id != msg.auth_id and in_stream != src_stream: + LOG.error('%r: bad src_id: got %r via %r, not %r: %r', + self, msg.src_id, in_stream, src_stream, msg) + return + + # If the stream's MitogenProtocol has auth_id set, copy it to the + # message. This allows subtrees to become privileged by stamping a + # parent's context ID. It is used by mitogen.unix to mark client + # streams (like Ansible WorkerProcess) as having the same rights as + # the parent. + if in_stream.protocol.auth_id is not None: + msg.auth_id = in_stream.protocol.auth_id + if in_stream.protocol.on_message is not None: + in_stream.protocol.on_message(in_stream, msg) + + # Record the IDs the source ever communicated with. + in_stream.protocol.egress_ids.add(msg.dst_id) + + if msg.dst_id == mitogen.context_id: + return self._invoke(msg, in_stream) + + out_stream = self._stream_by_id.get(msg.dst_id) + if (not out_stream) and (parent_stream != src_stream or not in_stream): + # No downstream route exists. The message could be from a child or + # ourselves for a parent, in which case we must forward it + # upstream, or it could be from a parent for a dead child, in which + # case its src_id/auth_id would fail verification if returned to + # the parent, so in that case reply with a dead message instead. + out_stream = parent_stream + + if out_stream is None: + self._maybe_send_dead(True, msg, self.no_route_msg, + msg.dst_id, mitogen.context_id) + return + + if in_stream and self.unidirectional and not \ + (in_stream.protocol.is_privileged or + out_stream.protocol.is_privileged): + self._maybe_send_dead(True, msg, self.unidirectional_msg, + in_stream.protocol.remote_id, + out_stream.protocol.remote_id, + mitogen.context_id) + return + + out_stream.protocol._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) + + +class NullTimerList(object): + def get_timeout(self): + return None + + +class Broker(object): + """ + Responsible for handling I/O multiplexing in a private thread. + + **Note:** This somewhat limited core version is used by children. The + master subclass is documented below. + """ + poller_class = Poller + _waker = None + _thread = None + + # :func:`mitogen.parent._upgrade_broker` replaces this with + # :class:`mitogen.parent.TimerList` during upgrade. + timers = NullTimerList() + + #: 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, activate_compat=True): + self._alive = True + self._exitted = False + self._waker = Waker.build_stream(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.protocol.defer + self.poller = self.poller_class() + self.poller.start_receive( + self._waker.receive_side.fd, + (self._waker.receive_side, self._waker.on_receive) + ) + self._thread = threading.Thread( + target=self._broker_main, + name='mitogen.broker' + ) + self._thread.start() + if activate_compat: + self._py24_25_compat() + + def _py24_25_compat(self): + """ + Python 2.4/2.5 have grave difficulties with threads/fork. We + mandatorily quiesce all running threads during fork using a + monkey-patch there. + """ + if sys.version_info < (2, 6): + # import_module() is used to avoid dep scanner. + os_fork = import_module('mitogen.os_fork') + os_fork._notice_broker_or_pool(self) + + 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 not side.closed + self.defer(self.poller.start_receive, + 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 not side.closed + 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) > 0 or self.timers.get_timeout() is not None + + def defer_sync(self, func): + """ + Arrange for `func()` to execute on :class:`Broker` thread, blocking the + current thread until a result or exception is available. + + :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): + """ + Call `func(self)`, catching any exception that might occur, logging it, + and force-disconnecting the related `stream`. + """ + try: + func(self) + except Exception: + LOG.exception('%r crashed', stream) + stream.on_disconnect(self) + + def _loop_once(self, timeout=None): + """ + Execute a single :class:`Poller` wait, dispatching any IO events that + caused the wait to complete. + + :param float timeout: + If not :data:`None`, maximum time in seconds to wait for events. + """ + _vv and IOLOG.debug('%r._loop_once(%r, %r)', + self, timeout, self.poller) + + timer_to = self.timers.get_timeout() + if timeout is None: + timeout = timer_to + elif timer_to is not None and timer_to < timeout: + timeout = timer_to + + #IOLOG.debug('readers =\n%s', pformat(self.poller.readers)) + #IOLOG.debug('writers =\n%s', pformat(self.poller.writers)) + for side, func in self.poller.poll(timeout): + self._call(side.stream, func) + if timer_to is not None: + self.timers.expire() + + def _broker_exit(self): + """ + Forcefully call :meth:`Stream.on_disconnect` on any streams that failed + to shut down gracefully, then discard the :class:`Poller`. + """ + for _, (side, _) in self.poller.readers + self.poller.writers: + LOG.debug('%r: force disconnecting %r', self, side) + side.stream.on_disconnect(self) + + self.poller.close() + + def _broker_shutdown(self): + """ + Invoke :meth:`Stream.on_shutdown` for every active stream, then allow + up to :attr:`shutdown_timeout` seconds for the streams to unregister + themselves, logging an error if any did not unregister during the grace + period. + """ + for _, (side, _) in self.poller.readers + self.poller.writers: + self._call(side.stream, side.stream.on_shutdown) + + deadline = now() + self.shutdown_timeout + while self.keep_alive() and now() < deadline: + self._loop_once(max(0, deadline - now())) + + if self.keep_alive(): + LOG.error('%r: pending work still existed %d seconds after ' + 'shutdown began. This may be due to a timer that is yet ' + 'to expire, or a child connection that did not fully ' + 'shut down.', self, self.shutdown_timeout) + + def _do_broker_main(self): + """ + Broker thread main function. Dispatches IO events until + :meth:`shutdown` is called. + """ + # For Python 2.4, no way to retrieve ident except on thread. + self._waker.protocol.broker_ident = thread.get_ident() + try: + while self._alive: + self._loop_once() + + fire(self, 'before_shutdown') + fire(self, 'shutdown') + self._broker_shutdown() + except Exception: + e = sys.exc_info()[1] + LOG.exception('broker crashed') + syslog.syslog(syslog.LOG_ERR, 'broker crashed: %s' % (e,)) + syslog.closelog() # prevent test 'fd leak'. + + self._alive = False # Ensure _alive is consistent on crash. + self._exitted = True + self._broker_exit() + + def _broker_main(self): + try: + _profile_hook('mitogen.broker', self._do_broker_main) + finally: + # 'finally' to ensure _on_broker_exit() can always SIGTERM. + fire(self, 'exit') + + def shutdown(self): + """ + Request broker gracefully disconnect streams and stop. Safe to call + from any thread. + """ + _v and LOG.debug('%r: shutting down', self) + def _shutdown(): + self._alive = False + if self._alive and not self._exitted: + 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): + return 'Broker(%04x)' % (id(self) & 0xffff,) + + +class Dispatcher(object): + """ + Implementation of the :data:`CALL_FUNCTION` handle for a child context. + Listens on the child's main thread for messages sent by + :class:`mitogen.parent.CallChain` and dispatches the function calls they + describe. + + If a :class:`mitogen.parent.CallChain` sending a message is in pipelined + mode, any exception that occurs is recorded, and causes all subsequent + calls with the same `chain_id` to fail with the same exception. + """ + _service_recv = None + + def __repr__(self): + return 'Dispatcher' + + def __init__(self, econtext): + self.econtext = econtext + #: Chain ID -> CallError if prior call failed. + self._error_by_chain_id = {} + self.recv = Receiver( + router=econtext.router, + handle=CALL_FUNCTION, + policy=has_parent_authority, + ) + #: The :data:`CALL_SERVICE` :class:`Receiver` that will eventually be + #: reused by :class:`mitogen.service.Pool`, should it ever be loaded. + #: This is necessary for race-free reception of all service requests + #: delivered regardless of whether the stub or real service pool are + #: loaded. See #547 for related sorrows. + Dispatcher._service_recv = Receiver( + router=econtext.router, + handle=CALL_SERVICE, + policy=has_parent_authority, + ) + self._service_recv.notify = self._on_call_service + listen(econtext.broker, 'shutdown', self._on_broker_shutdown) + + def _on_broker_shutdown(self): + if self._service_recv.notify == self._on_call_service: + self._service_recv.notify = None + self.recv.close() + + @classmethod + @takes_econtext + def forget_chain(cls, chain_id, econtext): + econtext.dispatcher._error_by_chain_id.pop(chain_id, None) + + def _parse_request(self, msg): + data = msg.unpickle(throw=False) + _v and LOG.debug('%r: dispatching %r', self, data) + + chain_id, modname, klass, func, args, kwargs = data + obj = import_module(modname) + if klass: + obj = getattr(obj, klass) + fn = getattr(obj, func) + if getattr(fn, 'mitogen_takes_econtext', None): + kwargs.setdefault('econtext', self.econtext) + if getattr(fn, 'mitogen_takes_router', None): + kwargs.setdefault('router', self.econtext.router) + + return chain_id, fn, args, kwargs + + def _dispatch_one(self, msg): + try: + chain_id, fn, args, kwargs = self._parse_request(msg) + except Exception: + return None, CallError(sys.exc_info()[1]) + + if chain_id in self._error_by_chain_id: + return chain_id, self._error_by_chain_id[chain_id] + + try: + return chain_id, fn(*args, **kwargs) + except Exception: + e = CallError(sys.exc_info()[1]) + if chain_id is not None: + self._error_by_chain_id[chain_id] = e + return chain_id, e + + def _on_call_service(self, recv): + """ + Notifier for the :data:`CALL_SERVICE` receiver. This is called on the + :class:`Broker` thread for any service messages arriving at this + context, for as long as no real service pool implementation is loaded. + + In order to safely bootstrap the service pool implementation a sentinel + message is enqueued on the :data:`CALL_FUNCTION` receiver in order to + wake the main thread, where the importer can run without any + possibility of suffering deadlock due to concurrent uses of the + importer. + + Should the main thread be blocked indefinitely, preventing the import + from ever running, if it is blocked waiting on a service call, then it + means :mod:`mitogen.service` has already been imported and + :func:`mitogen.service.get_or_create_pool` has already run, meaning the + service pool is already active and the duplicate initialization was not + needed anyway. + + #547: This trickery is needed to avoid the alternate option of spinning + a temporary thread to import the service pool, which could deadlock if + a custom import hook executing on the main thread (under the importer + lock) would block waiting for some data that was in turn received by a + service. Main thread import lock can't be released until service is + running, service cannot satisfy request until import lock is released. + """ + self.recv._on_receive(Message(handle=STUB_CALL_SERVICE)) + + def _init_service_pool(self): + import mitogen.service + mitogen.service.get_or_create_pool(router=self.econtext.router) + + def _dispatch_calls(self): + for msg in self.recv: + if msg.handle == STUB_CALL_SERVICE: + if msg.src_id == mitogen.context_id: + self._init_service_pool() + continue + + chain_id, ret = self._dispatch_one(msg) + _v and LOG.debug('%r: %r -> %r', self, msg, ret) + if msg.reply_to: + msg.reply(ret) + elif isinstance(ret, CallError) and chain_id is None: + LOG.error('No-reply function call failed: %s', ret) + + def run(self): + if self.econtext.config.get('on_start'): + self.econtext.config['on_start'](self.econtext) + + _profile_hook('mitogen.child_main', self._dispatch_calls) + + +class ExternalContext(object): + """ + External context implementation. + + This class contains the main program implementation for new children. It is + responsible for setting up everything about the process environment, import + hooks, standard IO redirection, logging, configuring a :class:`Router` and + :class:`Broker`, and finally arranging for :class:`Dispatcher` to take over + the main thread after initialization is complete. + + .. 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:: importer + + The :class:`mitogen.core.Importer` instance. + + .. attribute:: stdout_log + + The :class:`IoLogger` connected to :data:`sys.stdout`. + + .. attribute:: stderr_log + + The :class:`IoLogger` connected to :data:`sys.stderr`. + """ + detached = False + + def __init__(self, config): + self.config = config + + def _on_broker_exit(self): + if not self.config['profiling']: + os.kill(os.getpid(), signal.SIGTERM) + + def _on_shutdown_msg(self, msg): + if not msg.is_dead: + _v and LOG.debug('shutdown request from context %d', msg.src_id) + self.broker.shutdown() + + def _on_parent_disconnect(self): + if self.detached: + mitogen.parent_ids = [] + mitogen.parent_id = None + LOG.info('Detachment complete') + else: + _v and LOG.debug('parent stream is gone, dying.') + self.broker.shutdown() + + def detach(self): + self.detached = True + stream = self.router.stream_by_id(mitogen.parent_id) + if stream: # not double-detach()'d + os.setsid() + 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.broker.defer_sync(stream.protocol.pending_bytes) + if not pending: + break + time.sleep(0.05) + if pending: + LOG.error('Stream had %d bytes after 2000ms', pending) + self.broker.defer(stream.on_disconnect, self.broker) + + def _setup_master(self): + Router.max_message_size = self.config['max_message_size'] + if self.config['profiling']: + enable_profiling() + self.broker = Broker(activate_compat=False) + self.router = Router(self.broker) + self.router.debug = self.config.get('debug', False) + self.router.unidirectional = self.config['unidirectional'] + self.router.add_handler( + fn=self._on_shutdown_msg, + handle=SHUTDOWN, + policy=has_parent_authority, + ) + self.master = Context(self.router, 0, 'master') + parent_id = self.config['parent_ids'][0] + if parent_id == 0: + self.parent = self.master + else: + self.parent = Context(self.router, parent_id, 'parent') + + in_fd = self.config.get('in_fd', 100) + in_fp = os.fdopen(os.dup(in_fd), 'rb', 0) + os.close(in_fd) + + out_fd = self.config.get('out_fd', pty.STDOUT_FILENO) + out_fd2 = os.dup(out_fd) + out_fp = os.fdopen(out_fd2, 'wb', 0) + self.stream = MitogenProtocol.build_stream( + self.router, + parent_id, + local_id=self.config['context_id'], + parent_ids=self.config['parent_ids'] + ) + for f in in_fp, out_fp: + fd = f.fileno() + set_blocking(fd, False) + self.stream.accept(in_fp, out_fp) + self.stream.name = 'parent' + self.stream.receive_side.keep_alive = False + + listen(self.stream, 'disconnect', self._on_parent_disconnect) + listen(self.broker, 'exit', self._on_broker_exit) + + def _reap_first_stage(self): + try: + os.wait() # Reap first stage. + except OSError: + pass # No first stage exists (e.g. fakessh) + + def _setup_logging(self): + self.log_handler = LogHandler(self.master) + root = logging.getLogger() + root.setLevel(self.config['log_level']) + root.handlers = [self.log_handler] + if self.config['debug']: + enable_debug_logging() + + def _setup_importer(self): + importer = self.config.get('importer') + if importer: + importer._install_handler(self.router) + importer._context = self.parent + else: + core_src_fd = self.config.get('core_src_fd', 101) + if core_src_fd: + fp = os.fdopen(core_src_fd, 'rb', 0) + try: + core_src = fp.read() + # Strip "ExternalContext.main()" call from last line. + core_src = b('\n').join(core_src.splitlines()[:-1]) + finally: + fp.close() + else: + core_src = None + + policy = ImportPolicy( + self.config['import_overrides'], + self.config['import_blocks'], + ) + importer = Importer( + self.router, + self.parent, + core_src, + policy, + ) + + self.importer = importer + self.router.importer = importer + sys.meta_path.insert(0, self.importer) + + def _setup_resource_requester(self): + resource_getter = ResourceRequester(self.router, self.parent) + self.importer._resource_requester = resource_getter + + def _setup_package(self): + global mitogen + mitogen = types.ModuleType('mitogen') + mitogen.__package__ = 'mitogen' + mitogen.__path__ = [] + mitogen.__loader__ = self.importer + mitogen.main = lambda *args, **kwargs: (lambda func: None) + mitogen.core = sys.modules['__main__'] + mitogen.core.__file__ = 'x/mitogen/core.py' # For inspect.getsource() + mitogen.core.__loader__ = self.importer + sys.modules['mitogen'] = mitogen + sys.modules['mitogen.core'] = mitogen.core + del sys.modules['__main__'] + + def _setup_globals(self): + mitogen.is_master = False + mitogen.__version__ = self.config['version'] + mitogen.context_id = self.config['context_id'] + mitogen.parent_ids = self.config['parent_ids'][:] + mitogen.parent_id = mitogen.parent_ids[0] + + def _nullify_stdio(self): + """ + Open /dev/null to replace stdio temporarily. In case of odd startup, + assume we may be allocated a standard handle. + """ + for stdio, stdfd, mode in [ + (sys.stdin, pty.STDIN_FILENO, os.O_RDONLY), + (sys.stdout, pty.STDOUT_FILENO, os.O_RDWR), + (sys.stderr, pty.STDERR_FILENO, os.O_RDWR), + ]: + if stdio is None: + continue + fd = os.open('/dev/null', mode) + if fd != stdfd: + os.dup2(fd, stdfd) + os.close(fd) + + def _preserve_tty_fp(self): + """ + #481: when stderr is a TTY due to being started via tty_create_child() + or hybrid_tty_create_child(), and some privilege escalation tool like + prehistoric versions of sudo exec this process over the top of itself, + there is nothing left to keep the slave PTY open after we replace our + stdio. Therefore if stderr is a TTY, keep around a permanent dup() to + avoid receiving SIGHUP. + """ + try: + if os.isatty(pty.STDERR_FILENO): + reserve_tty_fd = os.dup(pty.STDERR_FILENO) + self.reserve_tty_fp = os.fdopen(reserve_tty_fd, 'r+b', 0) + set_cloexec(self.reserve_tty_fp.fileno()) + except OSError: + pass + + def _setup_stdio(self): + self._preserve_tty_fp() + # When sys.stdout was opened by the runtime, overwriting it will not + # 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() + self._nullify_stdio() + + self.loggers = [] + for stdio, stdfd, name in [ + (sys.stdout, pty.STDOUT_FILENO, 'stdout'), + (sys.stderr, pty.STDERR_FILENO, 'stderr'), + ]: + if stdio is None: + continue + log = IoLoggerProtocol.build_stream(name, stdfd) + self.broker.start_receive(log) + self.loggers.append(log) + + # Reopen with line buffering. + sys.stdout = os.fdopen(pty.STDOUT_FILENO, 'w', 1) + + def main(self): + self._setup_master() + try: + try: + self._setup_logging() + self._setup_importer() + self._setup_resource_requester() + self._reap_first_stage() + if self.config.get('setup_package', True): + self._setup_package() + self._setup_globals() + if self.config.get('setup_stdio', True): + self._setup_stdio() + + self.dispatcher = Dispatcher(self) + self.router.register(self.parent, self.stream) + self.router._setup_logging() + + _v and LOG.debug('Python version is %s', sys.version) + _v and LOG.debug('Parent is context %r (%s); my ID is %r', + self.parent.context_id, self.parent.name, + mitogen.context_id) + _v and LOG.debug('pid:%r ppid:%r uid:%r/%r, gid:%r/%r host:%r', + os.getpid(), os.getppid(), os.geteuid(), + os.getuid(), os.getegid(), os.getgid(), + socket.gethostname()) + + sys.executable = os.environ.pop('ARGV0', sys.executable) + _v and LOG.debug('Recovered sys.executable: %r', sys.executable) + + if self.config.get('send_ec2', True): + self.stream.transmit_side.write(b('MITO002\n')) + self.broker._py24_25_compat() + self.log_handler.uncork() + self.dispatcher.run() + _v and LOG.debug('ExternalContext.main() normal exit') + except KeyboardInterrupt: + LOG.debug('KeyboardInterrupt received, exiting gracefully.') + except BaseException: + LOG.exception('ExternalContext.main() crashed') + raise + finally: + self.broker.shutdown() diff --git a/plugins/mitogen-0.3.44/mitogen/debug.py b/plugins/mitogen-0.3.44/mitogen/debug.py new file mode 100644 index 0000000..dbab550 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/debug.py @@ -0,0 +1,236 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +Basic signal handler for dumping thread stacks. +""" + +import difflib +import logging +import os +import gc +import signal +import sys +import threading +import time +import traceback + +import mitogen.core +import mitogen.parent + + +LOG = logging.getLogger(__name__) +_last = None + + +def enable_evil_interrupts(): + signal.signal(signal.SIGALRM, (lambda a, b: None)) + signal.setitimer(signal.ITIMER_REAL, 0.01, 0.01) + + +def disable_evil_interrupts(): + signal.setitimer(signal.ITIMER_REAL, 0, 0) + + +def _hex(n): + return '%08x' % n + + +def get_subclasses(klass): + """ + Rather than statically import every interesting subclass, forcing it all to + be transferred and potentially disrupting the debugged environment, + enumerate only those loaded in memory. Also returns the original class. + """ + stack = [klass] + seen = set() + while stack: + klass = stack.pop() + seen.add(klass) + stack.extend(klass.__subclasses__()) + return seen + + +def get_routers(): + return dict( + (_hex(id(router)), router) + for klass in get_subclasses(mitogen.core.Router) + for router in gc.get_referrers(klass) + if isinstance(router, mitogen.core.Router) + ) + + +def get_router_info(): + return { + 'routers': dict( + (id_, { + 'id': id_, + 'streams': len(set(router._stream_by_id.values())), + 'contexts': len(set(router._context_by_id.values())), + 'handles': len(router._handle_map), + }) + for id_, router in get_routers().items() + ) + } + + +def get_stream_info(router_id): + router = get_routers().get(router_id) + return { + 'streams': dict( + (_hex(id(stream)), ({ + 'name': stream.name, + 'remote_id': stream.remote_id, + 'sent_module_count': len(getattr(stream, 'sent_modules', [])), + 'routes': sorted(getattr(stream, 'routes', [])), + 'type': type(stream).__module__, + })) + for via_id, stream in router._stream_by_id.items() + ) + } + + +def format_stacks(): + name_by_id = dict( + (t.ident, t.name) + for t in threading.enumerate() + ) + + l = ['', ''] + for threadId, stack in sys._current_frames().items(): + l += ["# PID %d ThreadID: (%s) %s; %r" % ( + os.getpid(), + name_by_id.get(threadId, ''), + threadId, + stack, + )] + #stack = stack.f_back.f_back + + for filename, lineno, name, line in traceback.extract_stack(stack): + l += [ + 'File: "%s", line %d, in %s' % ( + filename, + lineno, + name + ) + ] + if line: + l += [' ' + line.strip()] + l += [''] + + l += ['', ''] + return '\n'.join(l) + + +def get_snapshot(): + global _last + + s = format_stacks() + snap = s + if _last: + snap += '\n' + diff = list(difflib.unified_diff( + a=_last.splitlines(), + b=s.splitlines(), + fromfile='then', + tofile='now' + )) + + if diff: + snap += '\n'.join(diff) + '\n' + else: + snap += '(no change since last time)\n' + _last = s + return snap + + +def _handler(*_): + fp = open('/dev/tty', 'w', 1) + fp.write(get_snapshot()) + fp.close() + + +def install_handler(): + signal.signal(signal.SIGUSR2, _handler) + + +def _logging_main(secs): + while True: + time.sleep(secs) + LOG.info('PERIODIC THREAD DUMP\n\n%s', get_snapshot()) + + +def dump_to_logger(secs=5): + th = threading.Thread( + target=_logging_main, + kwargs={'secs': secs}, + name='mitogen.debug.dump_to_logger', + ) + th.setDaemon(True) + th.start() + + +class ContextDebugger(object): + @classmethod + @mitogen.core.takes_econtext + def _configure_context(cls, econtext): + mitogen.parent.upgrade_router(econtext) + econtext.debugger = cls(econtext.router) + + def __init__(self, router): + self.router = router + self.router.add_handler( + func=self._on_debug_msg, + handle=mitogen.core.DEBUG, + persist=True, + policy=mitogen.core.has_parent_authority, + ) + mitogen.core.listen(router, 'register', self._on_stream_register) + LOG.debug('Context debugging configured.') + + def _on_stream_register(self, context, stream): + LOG.debug('_on_stream_register: sending configure() to %r', stream) + context.call_async(ContextDebugger._configure_context) + + def _on_debug_msg(self, msg): + if msg != mitogen.core._DEAD: + threading.Thread( + target=self._handle_debug_msg, + name='ContextDebuggerHandler', + args=(msg,) + ).start() + + def _handle_debug_msg(self, msg): + try: + method, args, kwargs = msg.unpickle() + msg.reply(getattr(self, method)(*args, **kwargs)) + except Exception: + e = sys.exc_info()[1] + msg.reply(mitogen.core.CallError(e)) diff --git a/plugins/mitogen-0.3.44/mitogen/doas.py b/plugins/mitogen-0.3.44/mitogen/doas.py new file mode 100644 index 0000000..5b212b9 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/doas.py @@ -0,0 +1,142 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import logging +import re + +import mitogen.core +import mitogen.parent + + +LOG = logging.getLogger(__name__) + +password_incorrect_msg = 'doas password is incorrect' +password_required_msg = 'doas password is required' + + +class PasswordError(mitogen.core.StreamError): + pass + + +class Options(mitogen.parent.Options): + username = u'root' + password = None + doas_path = 'doas' + password_prompt = u'Password:' + incorrect_prompts = ( + u'doas: authentication failed', # slicer69/doas + u'doas: Authorization failed', # openbsd/src + ) + + def __init__(self, username=None, password=None, doas_path=None, + password_prompt=None, incorrect_prompts=None, **kwargs): + super(Options, self).__init__(**kwargs) + if username is not None: + self.username = mitogen.core.to_text(username) + if password is not None: + self.password = mitogen.core.to_text(password) + if doas_path is not None: + self.doas_path = doas_path + if password_prompt is not None: + self.password_prompt = mitogen.core.to_text(password_prompt) + if incorrect_prompts is not None: + self.incorrect_prompts = [ + mitogen.core.to_text(p) + for p in incorrect_prompts + ] + + +class BootstrapProtocol(mitogen.parent.RegexProtocol): + password_sent = False + + def setup_patterns(self, conn): + prompt_pattern = re.compile( + re.escape(conn.options.password_prompt).encode('utf-8'), + re.I + ) + incorrect_prompt_pattern = re.compile( + u'|'.join( + re.escape(s) + for s in conn.options.incorrect_prompts + ).encode('utf-8'), + re.I + ) + + self.PATTERNS = [ + (incorrect_prompt_pattern, type(self)._on_incorrect_password), + ] + self.PARTIAL_PATTERNS = [ + (prompt_pattern, type(self)._on_password_prompt), + ] + + def _on_incorrect_password(self, line, match): + if self.password_sent: + self.stream.conn._fail_connection( + PasswordError(password_incorrect_msg) + ) + + def _on_password_prompt(self, line, match): + if self.stream.conn.options.password is None: + self.stream.conn._fail_connection( + PasswordError(password_required_msg) + ) + return + + if self.password_sent: + self.stream.conn._fail_connection( + PasswordError(password_incorrect_msg) + ) + return + + LOG.debug('sending password') + self.stream.transmit_side.write( + (self.stream.conn.options.password + '\n').encode('utf-8') + ) + self.password_sent = True + + +class Connection(mitogen.parent.Connection): + options_class = Options + diag_protocol_class = BootstrapProtocol + + create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) + child_is_immediate_subprocess = False + + def _get_name(self): + return u'doas.' + self.options.username + + def stderr_stream_factory(self): + stream = super(Connection, self).stderr_stream_factory() + stream.protocol.setup_patterns(self) + return stream + + def get_boot_command(self): + bits = [self.options.doas_path, '-u', self.options.username, '--'] + return bits + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/docker.py b/plugins/mitogen-0.3.44/mitogen/docker.py new file mode 100644 index 0000000..48848c8 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/docker.py @@ -0,0 +1,83 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import logging + +import mitogen.core +import mitogen.parent + + +LOG = logging.getLogger(__name__) + + +class Options(mitogen.parent.Options): + container = None + image = None + username = None + docker_path = u'docker' + + def __init__(self, container=None, image=None, docker_path=None, + username=None, **kwargs): + super(Options, self).__init__(**kwargs) + assert container or image + if container: + self.container = mitogen.core.to_text(container) + if image: + self.image = mitogen.core.to_text(image) + if docker_path: + self.docker_path = mitogen.core.to_text(docker_path) + if username: + self.username = mitogen.core.to_text(username) + + +class Connection(mitogen.parent.Connection): + options_class = Options + child_is_immediate_subprocess = False + + # TODO: better way of capturing errors such as "No such container." + create_child_args = { + 'merge_stdio': True + } + + def _get_name(self): + return u'docker.' + (self.options.container or self.options.image) + + def get_boot_command(self): + args = ['--interactive'] + if self.options.username: + args += ['--user=' + self.options.username] + + bits = [self.options.docker_path] + if self.options.container: + bits += ['exec'] + args + [self.options.container] + elif self.options.image: + bits += ['run'] + args + ['--rm', self.options.image] + + return bits + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/fakessh.py b/plugins/mitogen-0.3.44/mitogen/fakessh.py new file mode 100644 index 0000000..70eacac --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/fakessh.py @@ -0,0 +1,460 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +:mod:`mitogen.fakessh` is a stream implementation that starts a subprocess with +its environment modified such that ``PATH`` searches for `ssh` return a Mitogen +implementation of SSH. When invoked, this implementation arranges for the +command line supplied by the caller to be executed in a remote context, reusing +the parent context's (possibly proxied) connection to that remote context. + +This allows tools like `rsync` and `scp` to transparently reuse the connections +and tunnels already established by the host program to connect to a target +machine, without wasteful redundant SSH connection setup, 3-way handshakes, or +firewall hopping configurations, and enables these tools to be used in +impossible scenarios, such as over `sudo` with ``requiretty`` enabled. + +The fake `ssh` command source is written to a temporary file on disk, and +consists of a copy of the :py:mod:`mitogen.core` source code (just like any +other child context), with a line appended to cause it to connect back to the +host process over an FD it inherits. As there is no reliance on an existing +filesystem file, it is possible for child contexts to use fakessh. + +As a consequence of connecting back through an inherited FD, only one SSH +invocation is possible, which is fine for tools like `rsync`, however in future +this restriction will be lifted. + +Sequence: + + 1. ``fakessh`` Context and Stream created by parent context. The stream's + buffer has a :py:func:`_fakessh_main` :py:data:`CALL_FUNCTION + ` enqueued. + 2. Target program (`rsync/scp/sftp`) invoked, which internally executes + `ssh` from ``PATH``. + 3. :py:mod:`mitogen.core` bootstrap begins, recovers the stream FD + inherited via the target program, established itself as the fakessh + context. + 4. :py:func:`_fakessh_main` :py:data:`CALL_FUNCTION + ` is read by fakessh context, + + a. sets up :py:class:`IoPump` for stdio, registers + stdin_handle for local context. + b. Enqueues :py:data:`CALL_FUNCTION ` for + :py:func:`_start_slave` invoked in target context, + + i. the program from the `ssh` command line is started + ii. sets up :py:class:`IoPump` for `ssh` command line process's + stdio pipes + iii. returns `(control_handle, stdin_handle)` to + :py:func:`_fakessh_main` + + 5. :py:func:`_fakessh_main` receives control/stdin handles from from + :py:func:`_start_slave`, + + a. registers remote's stdin_handle with local :py:class:`IoPump`. + b. sends `("start", local_stdin_handle)` to remote's control_handle + c. registers local :py:class:`IoPump` with + :py:class:`mitogen.core.Broker`. + d. loops waiting for `local stdout closed && remote stdout closed` + + 6. :py:func:`_start_slave` control channel receives `("start", stdin_handle)`, + + a. registers remote's stdin_handle with local :py:class:`IoPump` + b. registers local :py:class:`IoPump` with + :py:class:`mitogen.core.Broker`. + c. loops waiting for `local stdout closed && remote stdout closed` +""" + +import getopt +import inspect +import os +import pty +import shutil +import subprocess +import sys +import tempfile +import threading + +import mitogen.core +import mitogen.parent + +from mitogen.core import LOG, IOLOG + + +SSH_GETOPTS = ( + "1246ab:c:e:fgi:kl:m:no:p:qstvx" + "ACD:E:F:I:KL:MNO:PQ:R:S:TVw:W:XYy" +) + +_mitogen = None + + +class IoPump(mitogen.core.Protocol): + _output_buf = '' + _closed = False + + def __init__(self, broker): + self._broker = broker + + def write(self, s): + self._output_buf += s + self._broker._start_transmit(self) + + def close(self): + self._closed = True + # If local process hasn't exitted yet, ensure its write buffer is + # drained before lazily triggering disconnect in on_transmit. + if self.transmit_side.fp.fileno() is not None: + self._broker._start_transmit(self) + + def on_shutdown(self, stream, broker): + self.close() + + def on_transmit(self, stream, broker): + written = self.transmit_side.write(self._output_buf) + IOLOG.debug('%r.on_transmit() -> len %r', self, written) + if written is None: + self.on_disconnect(broker) + else: + self._output_buf = self._output_buf[written:] + + if not self._output_buf: + broker._stop_transmit(self) + if self._closed: + self.on_disconnect(broker) + + def on_receive(self, stream, broker): + s = stream.receive_side.read() + IOLOG.debug('%r.on_receive() -> len %r', self, len(s)) + if s: + mitogen.core.fire(self, 'receive', s) + else: + self.on_disconnect(broker) + + def __repr__(self): + return 'IoPump(%r, %r)' % ( + self.receive_side.fp.fileno(), + self.transmit_side.fp.fileno(), + ) + + +class Process(object): + """ + Manages the lifetime and pipe connections of the SSH command running in the + slave. + """ + def __init__(self, router, stdin, stdout, proc=None): + self.router = router + self.stdin = stdin + self.stdout = stdout + self.proc = proc + self.control_handle = router.add_handler(self._on_control) + self.stdin_handle = router.add_handler(self._on_stdin) + self.pump = IoPump.build_stream(router.broker) + for fp in stdin, stdout: + fd = fp.fileno() + mitogen.core.set_blocking(fd, False) + self.pump.accept(stdin, stdout) + self.stdin = None + self.control = None + self.wake_event = threading.Event() + + mitogen.core.listen(self.pump, 'disconnect', self._on_pump_disconnect) + mitogen.core.listen(self.pump, 'receive', self._on_pump_receive) + + if proc: + pmon = mitogen.parent.ProcessMonitor.instance() + pmon.add(proc.pid, self._on_proc_exit) + + def __repr__(self): + return 'Process(%r, %r)' % (self.stdin, self.stdout) + + def _on_proc_exit(self, status): + LOG.debug('%r._on_proc_exit(%r)', self, status) + self.control.put(('exit', status)) + + def _on_stdin(self, msg): + if msg.is_dead: + IOLOG.debug('%r._on_stdin() -> %r', self, msg) + self.pump.protocol.close() + return + + data = msg.unpickle() + IOLOG.debug('%r._on_stdin() -> len %d', self, len(data)) + self.pump.protocol.write(data) + + def _on_control(self, msg): + if not msg.is_dead: + command, arg = msg.unpickle(throw=False) + LOG.debug('%r._on_control(%r, %s)', self, command, arg) + + func = getattr(self, '_on_%s' % (command,), None) + if func: + return func(msg, arg) + + LOG.warning('%r: unknown command %r', self, command) + + def _on_start(self, msg, arg): + dest = mitogen.core.Context(self.router, msg.src_id) + self.control = mitogen.core.Sender(dest, arg[0]) + self.stdin = mitogen.core.Sender(dest, arg[1]) + self.router.broker.start_receive(self.pump) + + def _on_exit(self, msg, arg): + LOG.debug('on_exit: proc = %r', self.proc) + if self.proc: + self.proc.terminate() + else: + self.router.broker.shutdown() + + def _on_pump_receive(self, s): + IOLOG.info('%r._on_pump_receive(len %d)', self, len(s)) + self.stdin.put(s) + + def _on_pump_disconnect(self): + LOG.debug('%r._on_pump_disconnect()', self) + mitogen.core.fire(self, 'disconnect') + self.stdin.close() + self.wake_event.set() + + def start_master(self, stdin, control): + self.stdin = stdin + self.control = control + control.put(('start', (self.control_handle, self.stdin_handle))) + self.router.broker.start_receive(self.pump) + + def wait(self): + while not self.wake_event.isSet(): + # Timeout is used so that sleep is interruptible, as blocking + # variants of libc thread operations cannot be interrupted e.g. via + # KeyboardInterrupt. isSet() test and wait() are separate since in + # <2.7 wait() always returns None. + self.wake_event.wait(0.1) + + +@mitogen.core.takes_router +def _start_slave(src_id, cmdline, router): + """ + This runs in the target context, it is invoked by _fakessh_main running in + the fakessh context immediately after startup. It starts the slave process + (the the point where it has a stdin_handle to target but not stdout_chan to + write to), and waits for main to. + """ + LOG.debug('_start_slave(%r, %r)', router, cmdline) + + proc = subprocess.Popen( + cmdline, + # SSH server always uses user's shell. + shell=True, + # SSH server always executes new commands in the user's HOME. + cwd=os.path.expanduser('~'), + + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + process = Process(router, proc.stdin, proc.stdout, proc) + return process.control_handle, process.stdin_handle + + +# +# SSH client interface. +# + + +def exit(): + _mitogen.broker.shutdown() + + +def die(msg, *args): + if args: + msg %= args + sys.stderr.write('%s\n' % (msg,)) + exit() + + +def parse_args(): + hostname = None + remain = sys.argv[1:] + allopts = [] + restarted = 0 + + while remain and restarted < 2: + opts, args = getopt.getopt(remain, SSH_GETOPTS) + remain = remain[:] # getopt bug! + allopts += opts + if not args: + break + + if not hostname: + hostname = args.pop(0) + remain = remain[remain.index(hostname) + 1:] + + restarted += 1 + + return hostname, allopts, args + + +@mitogen.core.takes_econtext +def _fakessh_main(dest_context_id, econtext): + hostname, opts, args = parse_args() + if not hostname: + die('Missing hostname') + + subsystem = False + for opt, optarg in opts: + if opt == '-s': + subsystem = True + else: + LOG.debug('Warning option %s %s is ignored.', opt, optarg) + + LOG.debug('hostname: %r', hostname) + LOG.debug('opts: %r', opts) + LOG.debug('args: %r', args) + + if subsystem: + die('-s is not yet supported') + + if not args: + die('fakessh: login mode not supported and no command specified') + + dest = mitogen.parent.Context(econtext.router, dest_context_id) + + # Even though SSH receives an argument vector, it still cats the vector + # together before sending to the server, the server just uses /bin/sh -c to + # run the command. We must remain puke-for-puke compatible. + control_handle, stdin_handle = dest.call(_start_slave, + mitogen.context_id, ' '.join(args)) + + LOG.debug('_fakessh_main: received control_handle=%r, stdin_handle=%r', + control_handle, stdin_handle) + + process = Process(econtext.router, + stdin=os.fdopen(pty.STDOUT_FILENO, 'w+b', 0), + stdout=os.fdopen(pty.STDIN_FILENO, 'r+b', 0), + ) + process.start_master( + stdin=mitogen.core.Sender(dest, stdin_handle), + control=mitogen.core.Sender(dest, control_handle), + ) + process.wait() + process.control.put(('exit', None)) + + +def _get_econtext_config(context, sock2): + parent_ids = mitogen.parent_ids[:] + parent_ids.insert(0, mitogen.context_id) + return { + 'context_id': context.context_id, + 'core_src_fd': None, + 'debug': getattr(context.router, 'debug', False), + 'in_fd': sock2.fileno(), + 'log_level': mitogen.parent.get_log_level(), + 'max_message_size': context.router.max_message_size, + 'out_fd': sock2.fileno(), + 'parent_ids': parent_ids, + 'profiling': getattr(context.router, 'profiling', False), + 'unidirectional': getattr(context.router, 'unidirectional', False), + 'setup_stdio': False, + 'version': mitogen.__version__, + } + + +# +# Public API. +# + +@mitogen.core.takes_econtext +@mitogen.core.takes_router +def run(dest, router, args, deadline=None, econtext=None): + """ + Run the command specified by `args` such that ``PATH`` searches for SSH by + the command will cause its attempt to use SSH to execute a remote program + to be redirected to use mitogen to execute that program using the context + `dest` instead. + + :param list args: + Argument vector. + :param mitogen.core.Context dest: + The destination context to execute the SSH command line in. + + :param mitogen.core.Router router: + + :param list[str] args: + Command line arguments for local program, e.g. + ``['rsync', '/tmp', 'remote:/tmp']`` + + :returns: + Exit status of the child process. + """ + if econtext is not None: + mitogen.parent.upgrade_router(econtext) + + context_id = router.allocate_id() + fakessh = mitogen.parent.Context(router, context_id) + fakessh.name = u'fakessh.%d' % (context_id,) + + sock1, sock2 = mitogen.core.socketpair() + + stream = mitogen.core.Stream(router, context_id) + stream.name = u'fakessh' + mitogen.core.set_blocking(sock1.fileno(), False) + stream.accept(sock1, sock1) + router.register(fakessh, stream) + + # Held in socket buffer until process is booted. + fakessh.call_async(_fakessh_main, dest.context_id) + + tmp_path = tempfile.mkdtemp(prefix='mitogen_fakessh') + try: + ssh_path = os.path.join(tmp_path, 'ssh') + fp = open(ssh_path, 'w') + try: + fp.write('#!%s\n' % (mitogen.parent.get_sys_executable(),)) + fp.write(inspect.getsource(mitogen.core)) + fp.write('\n') + fp.write('ExternalContext(%r).main()\n' % ( + _get_econtext_config(econtext, sock2), + )) + finally: + fp.close() + + os.chmod(ssh_path, int('0755', 8)) + env = os.environ.copy() + env.update({ + 'PATH': '%s:%s' % (tmp_path, env.get('PATH', '')), + 'ARGV0': mitogen.parent.get_sys_executable(), + 'SSH_PATH': ssh_path, + }) + + proc = subprocess.Popen(args, env=env) + return proc.wait() + finally: + shutil.rmtree(tmp_path) diff --git a/plugins/mitogen-0.3.44/mitogen/fork.py b/plugins/mitogen-0.3.44/mitogen/fork.py new file mode 100644 index 0000000..d77ed6f --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/fork.py @@ -0,0 +1,250 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import errno +import logging +import os +import random +import sys +import threading +import traceback + +import mitogen.core +import mitogen.parent +from mitogen.core import b + + +LOG = logging.getLogger(__name__) + +# Python 2.4/2.5 cannot support fork+threads whatsoever, it doesn't even fix up +# interpreter state. So 2.4/2.5 interpreters start .local() contexts for +# isolation instead. Since we don't have any crazy memory sharing problems to +# avoid, there is no virginal fork parent either. The child is started directly +# from the login/become process. In future this will be default everywhere, +# fork is brainwrong from the stone age. +FORK_SUPPORTED = sys.version_info >= (2, 6) + + +class Error(mitogen.core.StreamError): + pass + + +def fixup_prngs(): + """ + Add 256 bits of /dev/urandom to OpenSSL's PRNG in the child, and re-seed + the random package with the same data. + """ + s = os.urandom(256 // 8) + random.seed(s) + if 'ssl' in sys.modules: + sys.modules['ssl'].RAND_add(s, 75.0) + + +def reset_logging_framework(): + """ + After fork, ensure any logging.Handler locks are recreated, as a variety of + threads in the parent may have been using the logging package at the moment + of fork. + + It is not possible to solve this problem in general; see :gh:issue:`150` + for a full discussion. + """ + logging._lock = threading.RLock() + + # The root logger does not appear in the loggerDict. + logging.Logger.manager.loggerDict = {} + logging.getLogger().handlers = [] + + +def on_fork(): + """ + Should be called by any program integrating Mitogen each time the process + is forked, in the context of the new child. + """ + reset_logging_framework() # Must be first! + fixup_prngs() + mitogen.core.Latch._on_fork() + mitogen.core.Side._on_fork() + mitogen.core.ExternalContext.service_stub_lock = threading.Lock() + + mitogen__service = sys.modules.get('mitogen.service') + if mitogen__service: + mitogen__service._pool_lock = threading.Lock() + + +def handle_child_crash(): + """ + Respond to _child_main() crashing by ensuring the relevant exception is + logged to /dev/tty. + """ + tty = open('/dev/tty', 'wb') + tty.write('\n\nFORKED CHILD PID %d CRASHED\n%s\n\n' % ( + os.getpid(), + traceback.format_exc(), + )) + tty.close() + os._exit(1) + + +def _convert_exit_status(status): + """ + Convert a :func:`os.waitpid`-style exit status to a :mod:`subprocess` style + exit status. + """ + if os.WIFEXITED(status): + return os.WEXITSTATUS(status) + elif os.WIFSIGNALED(status): + return -os.WTERMSIG(status) + elif os.WIFSTOPPED(status): + return -os.WSTOPSIG(status) + + +class Process(mitogen.parent.Process): + def poll(self): + try: + pid, status = os.waitpid(self.pid, os.WNOHANG) + except OSError: + e = sys.exc_info()[1] + if e.args[0] == errno.ECHILD: + LOG.warn('%r: waitpid(%r) produced ECHILD', self, self.pid) + return + raise + + if not pid: + return + return _convert_exit_status(status) + + +class Options(mitogen.parent.Options): + #: Reference to the importer, if any, recovered from the parent. + importer = None + + #: User-supplied function for cleaning up child process state. + on_fork = None + + def __init__(self, old_router, max_message_size, on_fork=None, debug=False, + profiling=False, unidirectional=False, on_start=None, + name=None): + if not FORK_SUPPORTED: + raise Error(self.python_version_msg) + + # fork method only supports a tiny subset of options. + super(Options, self).__init__( + max_message_size=max_message_size, debug=debug, + profiling=profiling, unidirectional=unidirectional, name=name, + ) + self.on_fork = on_fork + self.on_start = on_start + + responder = getattr(old_router, 'responder', None) + if isinstance(responder, mitogen.parent.ModuleForwarder): + self.importer = responder.importer + + +class Connection(mitogen.parent.Connection): + options_class = Options + child_is_immediate_subprocess = True + + python_version_msg = ( + "The mitogen.fork method is not supported on Python versions " + "prior to 2.6, since those versions made no attempt to repair " + "critical interpreter state following a fork. Please use the " + "local() method instead." + ) + + name_prefix = u'fork' + + def start_child(self): + parentfp, childfp = mitogen.parent.create_socketpair() + pid = os.fork() + if pid: + childfp.close() + return Process(pid, stdin=parentfp, stdout=parentfp) + else: + parentfp.close() + self._wrap_child_main(childfp) + + def _wrap_child_main(self, childfp): + try: + self._child_main(childfp) + except BaseException: + handle_child_crash() + + def get_econtext_config(self): + config = super(Connection, self).get_econtext_config() + config['core_src_fd'] = None + config['importer'] = self.options.importer + config['send_ec2'] = False + config['setup_package'] = False + if self.options.on_start: + config['on_start'] = self.options.on_start + return config + + def _child_main(self, childfp): + on_fork() + if self.options.on_fork: + self.options.on_fork() + mitogen.core.set_blocking(childfp.fileno(), True) + + childfp.send(b('MITO002\n')) + + # Expected by the ExternalContext.main(). + os.dup2(childfp.fileno(), 1) + os.dup2(childfp.fileno(), 100) + + # Overwritten by ExternalContext.main(); we must replace the + # parent-inherited descriptors that were closed by Side._on_fork() to + # avoid ExternalContext.main() accidentally allocating new files over + # the standard handles. + os.dup2(childfp.fileno(), 0) + + # Avoid corrupting the stream on fork crash by dupping /dev/null over + # stderr. Instead, handle_child_crash() uses /dev/tty to log errors. + devnull = os.open('/dev/null', os.O_WRONLY) + if devnull != 2: + os.dup2(devnull, 2) + os.close(devnull) + + # If we're unlucky, childfp.fileno() may coincidentally be one of our + # desired FDs. In that case closing it breaks ExternalContext.main(). + if childfp.fileno() not in (0, 1, 100): + childfp.close() + + mitogen.core.IOLOG.setLevel(logging.INFO) + + try: + try: + mitogen.core.ExternalContext(self.get_econtext_config()).main() + except Exception: + # TODO: report exception somehow. + os._exit(72) + finally: + # Don't trigger atexit handlers, they were copied from the parent. + os._exit(0) diff --git a/plugins/mitogen-0.3.44/mitogen/imports/__init__.py b/plugins/mitogen-0.3.44/mitogen/imports/__init__.py new file mode 100644 index 0000000..bf246e2 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/imports/__init__.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2025 Mitogen authors +# SPDX-License-Identifier: BSD-3-Clause +# !mitogen: minify_safe + +import sys + +if sys.version_info >= (3, 14): + from mitogen.imports._py314 import _code_imports +elif sys.version_info >= (3, 6): + from mitogen.imports._py36 import _code_imports +elif sys.version_info >= (2, 5): + from mitogen.imports._py2 import _code_imports_py25 as _code_imports +else: + from mitogen.imports._py2 import _code_imports_py24 as _code_imports + + +def codeobj_imports(co): + """ + Yield (level, modname, names) tuples by scanning the code object `co`. + + Top level `import mod` & `from mod import foo` statements are matched. + Those inside a `class ...` or `def ...` block are currently skipped. + + >>> co = compile('import a, b; from c import d, e as f', '', 'exec') + >>> list(codeobj_imports(co)) # doctest: +ELLIPSIS + [(..., 'a', ()), (..., 'b', ()), (..., 'c', ('d', 'e'))] + + :return: + Generator producing `(level, modname, names)` tuples, where: + + * `level`: + -1 implicit relative (Python 2.x default) + 0 absolute (Python 3.x, `from __future__ import absolute_import`) + >0 explicit relative (`from . import a`, `from ..b, import c`) + * `modname`: Name of module to import, or to import `names` from. + * `names`: tuple of names in `from mod import ..`. + """ + return _code_imports(co.co_code, co.co_consts, co.co_names) diff --git a/plugins/mitogen-0.3.44/mitogen/imports/_py2.py b/plugins/mitogen-0.3.44/mitogen/imports/_py2.py new file mode 100644 index 0000000..c640e27 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/imports/_py2.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2025 Mitogen authors +# SPDX-License-Identifier: BSD-3-Clause +# !mitogen: minify_safe + +import array +import itertools +import opcode + + +IMPORT_NAME = opcode.opmap['IMPORT_NAME'] +LOAD_CONST = opcode.opmap['LOAD_CONST'] + + +def _opargs(code, _have_arg=opcode.HAVE_ARGUMENT): + it = iter(array.array('B', code)) + nexti = it.next + for i in it: + if i >= _have_arg: + yield (i, nexti() | (nexti() << 8)) + else: + yield (i, None) + + +def _code_imports_py25(code, consts, names): + it1, it2, it3 = itertools.tee(_opargs(code), 3) + try: + next(it2) + next(it3) + next(it3) + except StopIteration: + return + for oparg1, oparg2, (op3, arg3) in itertools.izip(it1, it2, it3): + if op3 != IMPORT_NAME: + continue + op1, arg1 = oparg1 + op2, arg2 = oparg2 + if op1 != LOAD_CONST or op2 != LOAD_CONST: + continue + yield (consts[arg1], names[arg3], consts[arg2] or ()) + + +def _code_imports_py24(code, consts, names): + it1, it2 = itertools.tee(_opargs(code), 2) + try: + next(it2) + except StopIteration: + return + for oparg1, (op2, arg2) in itertools.izip(it1, it2): + if op2 != IMPORT_NAME: + continue + op1, arg1 = oparg1 + if op1 != LOAD_CONST: + continue + yield (-1, names[arg2], consts[arg1] or ()) diff --git a/plugins/mitogen-0.3.44/mitogen/imports/_py314.py b/plugins/mitogen-0.3.44/mitogen/imports/_py314.py new file mode 100644 index 0000000..acf369c --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/imports/_py314.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2025 Mitogen authors +# SPDX-License-Identifier: BSD-3-Clause +# !mitogen: minify_safe + +import opcode + +IMPORT_NAME = opcode.opmap['IMPORT_NAME'] +LOAD_CONST = opcode.opmap['LOAD_CONST'] +LOAD_SMALL_INT = opcode.opmap['LOAD_SMALL_INT'] + + +def _code_imports(code, consts, names): + start = 4 + while True: + op3_idx = code.find(IMPORT_NAME, start, -1) + if op3_idx < 0: + return + if op3_idx % 2: + start = op3_idx + 1 + continue + if code[op3_idx-4] != LOAD_SMALL_INT or code[op3_idx-2] != LOAD_CONST: + start = op3_idx + 2 + continue + start = op3_idx + 6 + arg1, arg2, arg3 = code[op3_idx-3], code[op3_idx-1], code[op3_idx+1] + yield (arg1, names[arg3], consts[arg2] or ()) diff --git a/plugins/mitogen-0.3.44/mitogen/imports/_py36.py b/plugins/mitogen-0.3.44/mitogen/imports/_py36.py new file mode 100644 index 0000000..a6d6f1a --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/imports/_py36.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 Mitogen authors +# SPDX-License-Identifier: BSD-3-Clause +# !mitogen: minify_safe + +import opcode + +IMPORT_NAME = opcode.opmap['IMPORT_NAME'] +LOAD_CONST = opcode.opmap['LOAD_CONST'] + + +def _code_imports(code, consts, names): + start = 4 + while True: + op3_idx = code.find(IMPORT_NAME, start, -1) + if op3_idx < 0: + return + if op3_idx % 2: + start = op3_idx + 1 + continue + if code[op3_idx-4] != LOAD_CONST or code[op3_idx-2] != LOAD_CONST: + start = op3_idx + 2 + continue + start = op3_idx + 6 + arg1, arg2, arg3 = code[op3_idx-3], code[op3_idx-1], code[op3_idx+1] + yield (consts[arg1], names[arg3], consts[arg2] or ()) diff --git a/plugins/mitogen-0.3.44/mitogen/incus.py b/plugins/mitogen-0.3.44/mitogen/incus.py new file mode 100644 index 0000000..ada32b0 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/incus.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2019 David Wilson +# SPDX-FileCopyrightText: 2026 Mitogen authors +# SPDX-License-Identifier: BSD-3-Clause +# !mitogen: minify_safe + +import mitogen.parent + + +class Options(mitogen.parent.Options): + container = None + incus_path = 'incus' + python_path = 'python' + + def __init__(self, container, incus_path=None, **kwargs): + super(Options, self).__init__(**kwargs) + self.container = container + if incus_path: + self.incus_path = incus_path + + +class Connection(mitogen.parent.Connection): + options_class = Options + + child_is_immediate_subprocess = False + create_child_args = { + # If incus finds any of stdin, stdout, stderr connected to a TTY, to + # prevent input injection it creates a proxy pty, forcing all IO to be + # buffered in <4KiB chunks. So ensure stderr is also routed to the + # socketpair. + 'merge_stdio': True + } + + eof_error_hint = ( + 'Note: many versions of Incus do not report program execution failure ' + 'meaningfully. Please check the host logs (/var/log) for more ' + 'information.' + ) + + def _get_name(self): + return u'incus.' + self.options.container + + def get_boot_command(self): + bits = [ + self.options.incus_path, + 'exec', + '--mode=non-interactive', + self.options.container, + '--', + ] + return bits + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/jail.py b/plugins/mitogen-0.3.44/mitogen/jail.py new file mode 100644 index 0000000..4da7eb0 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/jail.py @@ -0,0 +1,65 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import mitogen.core +import mitogen.parent + + +class Options(mitogen.parent.Options): + container = None + username = None + jexec_path = u'/usr/sbin/jexec' + + def __init__(self, container, jexec_path=None, username=None, **kwargs): + super(Options, self).__init__(**kwargs) + self.container = mitogen.core.to_text(container) + if username: + self.username = mitogen.core.to_text(username) + if jexec_path: + self.jexec_path = jexec_path + + +class Connection(mitogen.parent.Connection): + options_class = Options + + child_is_immediate_subprocess = False + create_child_args = { + 'merge_stdio': True + } + + def _get_name(self): + return u'jail.' + self.options.container + + def get_boot_command(self): + bits = [self.options.jexec_path] + if self.options.username: + bits += ['-U', self.options.username] + bits += [self.options.container] + return bits + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/kubectl.py b/plugins/mitogen-0.3.44/mitogen/kubectl.py new file mode 100644 index 0000000..5d3994a --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/kubectl.py @@ -0,0 +1,66 @@ +# 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: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import mitogen.parent + + +class Options(mitogen.parent.Options): + pod = None + kubectl_path = 'kubectl' + kubectl_args = None + + def __init__(self, pod, kubectl_path=None, kubectl_args=None, **kwargs): + super(Options, self).__init__(**kwargs) + assert pod + self.pod = pod + if kubectl_path: + self.kubectl_path = kubectl_path + self.kubectl_args = kubectl_args or [] + + +class Connection(mitogen.parent.Connection): + options_class = Options + child_is_immediate_subprocess = True + + # TODO: better way of capturing errors such as "No such container." + create_child_args = { + 'merge_stdio': True + } + + def _get_name(self): + return u'kubectl.%s%s' % (self.options.pod, self.options.kubectl_args) + + def get_boot_command(self): + bits = [ + self.options.kubectl_path + ] + self.options.kubectl_args + [ + 'exec', '-it', self.options.pod + ] + return bits + ["--"] + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/lxc.py b/plugins/mitogen-0.3.44/mitogen/lxc.py new file mode 100644 index 0000000..21dfef5 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/lxc.py @@ -0,0 +1,73 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import mitogen.parent + + +class Options(mitogen.parent.Options): + container = None + lxc_attach_path = 'lxc-attach' + + def __init__(self, container, lxc_attach_path=None, **kwargs): + super(Options, self).__init__(**kwargs) + self.container = container + if lxc_attach_path: + self.lxc_attach_path = lxc_attach_path + + +class Connection(mitogen.parent.Connection): + options_class = Options + + child_is_immediate_subprocess = False + create_child_args = { + # If lxc-attach finds any of stdin, stdout, stderr connected to a TTY, + # to prevent input injection it creates a proxy pty, forcing all IO to + # be buffered in <4KiB chunks. So ensure stderr is also routed to the + # socketpair. + 'merge_stdio': True + } + + 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 _get_name(self): + return u'lxc.' + self.options.container + + def get_boot_command(self): + bits = [ + self.options.lxc_attach_path, + '--clear-env', + '--name', self.options.container, + '--', + ] + return bits + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/lxd.py b/plugins/mitogen-0.3.44/mitogen/lxd.py new file mode 100644 index 0000000..09034ab --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/lxd.py @@ -0,0 +1,75 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import mitogen.parent + + +class Options(mitogen.parent.Options): + container = None + lxc_path = 'lxc' + python_path = 'python' + + def __init__(self, container, lxc_path=None, **kwargs): + super(Options, self).__init__(**kwargs) + self.container = container + if lxc_path: + self.lxc_path = lxc_path + + +class Connection(mitogen.parent.Connection): + options_class = Options + + child_is_immediate_subprocess = False + create_child_args = { + # If lxc finds any of stdin, stdout, stderr connected to a TTY, to + # prevent input injection it creates a proxy pty, forcing all IO to be + # buffered in <4KiB chunks. So ensure stderr is also routed to the + # socketpair. + 'merge_stdio': True + } + + 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 _get_name(self): + return u'lxd.' + self.options.container + + def get_boot_command(self): + bits = [ + self.options.lxc_path, + 'exec', + '--mode=noninteractive', + self.options.container, + '--', + ] + return bits + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/master.py b/plugins/mitogen-0.3.44/mitogen/master.py new file mode 100644 index 0000000..cf5c860 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/master.py @@ -0,0 +1,1566 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +This module implements functionality required by master processes, such as +starting new contexts via SSH. Its size is also restricted, since it must +be sent to any context that will be used to establish additional child +contexts. +""" + +import errno +import inspect +import logging +import os +import pkgutil +import re +import string +import sys +import threading +import types +import zlib + +if sys.version_info >= (3, 7): + import importlib.resources + +if sys.version_info >= (3, 4): + import importlib.util + from _imp import is_builtin as _is_builtin + + def _find_loader(fullname): + try: + maybe_spec = importlib.util.find_spec(fullname) + except (ImportError, AttributeError, TypeError, ValueError): + exc = sys.exc_info()[1] + raise ImportError(*exc.args) + try: + return maybe_spec.loader + except AttributeError: + return None +else: + import imp + from imp import is_builtin as _is_builtin + + if sys.version_info >= (2, 5): + from pkgutil import find_loader as _find_loader + else: + from mitogen.compat.pkgutil import find_loader as _find_loader + +if sys.version_info >= (2, 7): + import sysconfig +else: + sysconfig = None + +import mitogen +import mitogen.core +import mitogen.imports +import mitogen.minify +import mitogen.parent + +from mitogen.core import any +from mitogen.core import b +from mitogen.core import IOLOG +from mitogen.core import LOG +from mitogen.core import str_partition +from mitogen.core import str_rpartition +from mitogen.core import to_text + +RLOG = logging.getLogger('mitogen.ctx') + + +# there are some cases where modules are loaded in memory only, such as +# ansible collections, and the module "filename" doesn't actually exist +SPECIAL_FILE_PATHS = { + "__synthetic__", + "" +} + + +def _stdlib_paths(): + """ + Return a set of paths from which Python imports the standard library. + """ + attr_candidates = [ + 'prefix', + 'real_prefix', # virtualenv: only set inside a virtual environment. + 'base_prefix', # venv: always set, equal to prefix if outside. + ] + prefixes = (getattr(sys, a, None) for a in attr_candidates) + version = 'python%s.%s' % sys.version_info[0:2] + s = set(os.path.realpath(os.path.join(p, 'lib', version)) + for p in prefixes if p is not None) + + # When running 'unit2 tests/module_finder_test.py' in a Py2 venv on Ubuntu + # 18.10, above is insufficient to catch the real directory. + if sysconfig is not None: + s.add(sysconfig.get_config_var('DESTLIB')) + return s + + +def is_stdlib_name(modname): + """ + Return :data:`True` if `modname` appears to come from the standard library. + """ + # `(_imp|imp).is_builtin()` isn't a documented part of Python's stdlib. + # Returns 1 if modname names a module that is "builtin" to the the Python + # interpreter (e.g. '_sre'). Otherwise 0 (e.g. 're', 'netifaces'). + # + # """ + # Main is a little special - imp.is_builtin("__main__") will return False, + # but BuiltinImporter is still the most appropriate initial setting for + # its __loader__ attribute. + # """ -- comment in CPython pylifecycle.c:add_main_module() + if _is_builtin(modname) != 0: + return True + + module = sys.modules.get(modname) + if module is None: + return False + + origin = getattr(module, '__file__', None) + if origin is None: + return False + + return is_stdlib_path(os.path.abspath(origin)) + + +_STDLIB_PATHS = _stdlib_paths() + + +def is_stdlib_path(path): + return any( + os.path.commonprefix((libpath, path)) == libpath + and 'site-packages' not in path + and 'dist-packages' not in path + for libpath in _STDLIB_PATHS + ) + + +def get_child_modules(path, fullname): + """ + Return the suffixes of submodules directly neated beneath of the package + directory at `path`. + + :param str path: + Path to the module's source code on disk, or some PEP-302-recognized + equivalent. Usually this is the module's ``__file__`` attribute, but + is specified explicitly to avoid loading the module. + :param str fullname: + Name of the package we're trying to get child modules for + + :return: + List of submodule name suffixes. + """ + mod_path = os.path.dirname(path) + if mod_path != '': + return [to_text(name) for _, name, _ in pkgutil.iter_modules([mod_path])] + else: + # we loaded some weird package in memory, so we'll see if it has a custom loader we can use + loader = _find_loader(fullname) + return [to_text(name) for name, _ in loader.iter_modules(None)] if loader else [] + + +def _looks_like_script(path): + """ + Return :data:`True` if the (possibly extensionless) file at `path` + resembles a Python script. For now we simply verify the file contains + ASCII text. + """ + try: + fp = open(path, 'rb') + except IOError: + e = sys.exc_info()[1] + if e.args[0] == errno.EISDIR: + return False + raise + + try: + sample = fp.read(512).decode('latin-1') + return not set(sample).difference(string.printable) + finally: + fp.close() + + +def _py_filename(path): + """ + Returns a tuple of a Python path (if the file looks Pythonic) and whether or not + the Python path is special. Special file paths/modules might only exist in memory + """ + if not path: + return None, False + + if path[-4:] in ('.pyc', '.pyo'): + path = path.rstrip('co') + + if path.endswith('.py'): + return path, False + + if os.path.exists(path) and _looks_like_script(path): + return path, False + + basepath = os.path.basename(path) + if basepath in SPECIAL_FILE_PATHS: + return path, True + + # return None, False means that the filename passed to _py_filename does not appear + # to be python, and code later will handle when this function returns None + # see https://github.com/dw/mitogen/pull/715#discussion_r532380528 for how this + # decision was made to handle non-python files in this manner + return None, False + + +def _get_core_source(): + """ + Master version of parent.get_core_source(). + """ + source = inspect.getsource(mitogen.core) + return mitogen.minify.minimize_source(source) + + +if mitogen.is_master: + # TODO: find a less surprising way of installing this. + mitogen.parent._get_core_source = _get_core_source + + +class ThreadWatcher(object): + """ + Manage threads that wait for another thread to shut down, before invoking + `on_join()` for each associated ThreadWatcher. + + In CPython it seems possible to use this method to ensure a non-main thread + is signalled when the main thread has exited, using a third thread as a + proxy. + """ + #: Protects remaining _cls_* members. + _cls_lock = threading.Lock() + + #: PID of the process that last modified the class data. If the PID + #: changes, it means the thread watch dict refers to threads that no longer + #: exist in the current process (since it forked), and so must be reset. + _cls_pid = None + + #: Map watched Thread -> list of ThreadWatcher instances. + _cls_instances_by_target = {} + + #: Map watched Thread -> watcher Thread for each watched thread. + _cls_thread_by_target = {} + + @classmethod + def _reset(cls): + """ + If we have forked since the watch dictionaries were initialized, all + that has is garbage, so clear it. + """ + if os.getpid() != cls._cls_pid: + cls._cls_pid = os.getpid() + cls._cls_instances_by_target.clear() + cls._cls_thread_by_target.clear() + + def __init__(self, target, on_join): + self.target = target + self.on_join = on_join + + @classmethod + def _watch(cls, target): + target.join() + for watcher in cls._cls_instances_by_target[target]: + watcher.on_join() + + def install(self): + self._cls_lock.acquire() + try: + self._reset() + lst = self._cls_instances_by_target.setdefault(self.target, []) + lst.append(self) + if self.target not in self._cls_thread_by_target: + self._cls_thread_by_target[self.target] = threading.Thread( + name='mitogen.master.join_thread_async', + target=self._watch, + args=(self.target,) + ) + self._cls_thread_by_target[self.target].start() + finally: + self._cls_lock.release() + + def remove(self): + self._cls_lock.acquire() + try: + self._reset() + lst = self._cls_instances_by_target.get(self.target, []) + if self in lst: + lst.remove(self) + finally: + self._cls_lock.release() + + @classmethod + def watch(cls, target, on_join): + watcher = cls(target, on_join) + watcher.install() + return watcher + + +class LogForwarder(object): + """ + Install a :data:`mitogen.core.FORWARD_LOG` handler that delivers forwarded + log events into the local logging framework. This is used by the master's + :class:`Router`. + + The forwarded :class:`logging.LogRecord` objects are delivered to loggers + under ``mitogen.ctx.*`` corresponding to their + :attr:`mitogen.core.Context.name`, with the message prefixed with the + logger name used in the child. The records include some extra attributes: + + * ``mitogen_message``: Unicode original message without the logger name + prepended. + * ``mitogen_context``: :class:`mitogen.parent.Context` reference to the + source context. + * ``mitogen_name``: Original logger name. + + :param mitogen.master.Router router: + Router to install the handler on. + """ + def __init__(self, router): + self._router = router + self._cache = {} + router.add_handler( + fn=self._on_forward_log, + handle=mitogen.core.FORWARD_LOG, + ) + + def _on_forward_log(self, msg): + if msg.is_dead: + return + + context = self._router.context_by_id(msg.src_id) + if context is None: + LOG.error('%s: dropping log from unknown context %d', + self, msg.src_id) + return + + name, level_s, s = msg.data.decode('utf-8', 'replace').split('\x00', 2) + + logger_name = '%s.[%s]' % (name, context.name) + logger = self._cache.get(logger_name) + if logger is None: + self._cache[logger_name] = logger = logging.getLogger(logger_name) + + levelno = int(level_s) + # See logging.Handler.makeRecord() + record = logging.makeLogRecord( + { + "name": logger.name, + "levelname": logging.getLevelName(levelno), + "levelno": levelno, + "pathname": "(unknown file)", + "lineno": 0, + "msg": s, + "args": (), + "exc_info": None, + } + ) + record.mitogen_message = s + record.mitogen_context = self._router.context_by_id(msg.src_id) + record.mitogen_name = name + logger.handle(record) + + def __repr__(self): + return 'LogForwarder(%r)' % (self._router,) + + +class FinderMethod(object): + """ + Interface to a method for locating a Python module or package given its + name according to the running Python interpreter. You'd think this was a + simple task, right? Naive young fellow, welcome to the real world. + """ + def __init__(self): + self.log = LOG.getChild(self.__class__.__name__) + + def __repr__(self): + return '%s()' % (type(self).__name__,) + + def find(self, fullname): + """ + Accept a canonical module name as would be found in :data:`sys.modules` + and return a `(path, source, is_pkg)` tuple, where: + + * `path`: Unicode string containing path to source file. + * `source`: Bytestring containing source file's content. + * `is_pkg`: :data:`True` if `fullname` is a package. + + :returns: + :data:`None` if not found, or tuple as described above. + """ + raise NotImplementedError() + + +class DefectivePython3xMainMethod(FinderMethod): + """ + Recent versions of Python 3.x introduced an incomplete notion of + importer specs, and in doing so created permanent asymmetry in the + :mod:`pkgutil` interface handling for the :mod:`__main__` module. Therefore + we must handle :mod:`__main__` specially. + """ + def find(self, fullname): + """ + Find :mod:`__main__` using its :data:`__file__` attribute. + """ + if fullname != '__main__': + return None + + mod = sys.modules.get(fullname) + if not mod: + return None + + path = getattr(mod, '__file__', None) + if not (path is not None and os.path.exists(path) and _looks_like_script(path)): + return None + + fp = open(path, 'rb') + try: + source = fp.read() + finally: + fp.close() + + return path, source, False + + +class PkgutilMethod(FinderMethod): + """ + Attempt to fetch source code via pkgutil. In an ideal world, this would + be the only required implementation of get_module(). + """ + def find(self, fullname): + """ + Find `fullname` using :func:`pkgutil.find_loader`. + """ + try: + # If fullname refers to a submodule that's not already imported + # then the containing package is imported. + # Pre-'import spec' this returned None, in Python3.6 it raises + # ImportError. + loader = _find_loader(fullname) + except ImportError: + e = sys.exc_info()[1] + LOG.debug('%r: find_loader(%r) failed: %s', self, fullname, e) + return None + + if not loader: + LOG.debug('%r: find_loader(%r) returned %r, aborting', + self, fullname, loader) + return + + try: + path = loader.get_filename(fullname) + except (AttributeError, ImportError, ValueError): + # - get_filename() may throw ImportError if pkgutil.find_loader() + # picks a "parent" package's loader for some crap that's been + # stuffed in sys.modules, for example in the case of urllib3: + # "loader for urllib3.contrib.pyopenssl cannot handle + # requests.packages.urllib3.contrib.pyopenssl" + e = sys.exc_info()[1] + LOG.debug('%r: %r.get_file_name(%r) failed: %r', self, loader, fullname, e) + return + + path, is_special = _py_filename(path) + + try: + source = loader.get_source(fullname) + except AttributeError: + # Per PEP-302, get_source() is optional, + e = sys.exc_info()[1] + LOG.debug('%r: %r.get_source() failed: %r', self, loader, fullname, e) + return + + try: + is_pkg = loader.is_package(fullname) + except AttributeError: + # Per PEP-302, is_package() is optional, + e = sys.exc_info()[1] + LOG.debug('%r: %r.is_package(%r) failed: %r', self, loader, fullname, e) + return + + # workaround for special python modules that might only exist in memory + if is_special and is_pkg and not source: + source = '\n' + + if path is None or source is None: + LOG.debug('%r: path=%r, source=%r, aborting', self, path, source) + return + + if isinstance(source, mitogen.core.UnicodeType): + # get_source() returns "string" according to PEP-302, which was + # reinterpreted for Python 3 to mean a Unicode string. + source = source.encode('utf-8') + + return path, source, is_pkg + + +class SysModulesMethod(FinderMethod): + """ + Attempt to fetch source code via :data:`sys.modules`. This was originally + specifically to support :mod:`__main__`, but it may catch a few more cases. + """ + def find(self, fullname): + """ + Find `fullname` using its :data:`__file__` attribute. + """ + try: + module = sys.modules[fullname] + except KeyError: + LOG.debug('%r: sys.modules[%r] absent, aborting', self, fullname) + return + + if not isinstance(module, types.ModuleType): + LOG.debug('%r: sys.modules[%r] is %r, aborting', + self, fullname, module) + return + + try: + resolved_name = module.__name__ + except AttributeError: + LOG.debug('%r: %r has no __name__, aborting', self, module) + return + + if resolved_name != fullname: + LOG.debug('%r: %r.__name__ is %r, aborting', + self, module, resolved_name) + return + + try: + path = module.__file__ + except AttributeError: + LOG.debug('%r: %r has no __file__, aborting', self, module) + return + + path, _ = _py_filename(path) + if not path: + LOG.debug('%r: %r.__file__ is %r, aborting', self, module, path) + return + + LOG.debug('%r: sys.modules[%r]: found %s', self, fullname, path) + is_pkg = hasattr(module, '__path__') + try: + source = inspect.getsource(module) + except IOError: + # Work around inspect.getsourcelines() bug for 0-byte __init__.py + # files. + if not is_pkg: + raise + source = '\n' + + if isinstance(source, mitogen.core.UnicodeType): + # get_source() returns "string" according to PEP-302, which was + # reinterpreted for Python 3 to mean a Unicode string. + source = source.encode('utf-8') + + return path, source, is_pkg + + +class ParentImpEnumerationMethod(FinderMethod): + """ + Attempt to fetch source code by examining the module's (hopefully less + insane) parent package, and if no insane parents exist, simply use + :mod:`sys.path` to search for it from scratch on the filesystem using the + normal Python lookup mechanism. + + This is required for older versions of :mod:`ansible.compat.six`, + :mod:`plumbum.colors`, Ansible 2.8 :mod:`ansible.module_utils.distro` and + its submodule :mod:`ansible.module_utils.distro._distro`. + + When some package dynamically replaces itself in :data:`sys.modules`, but + only conditionally according to some program logic, it is possible that + children may attempt to load modules and subpackages from it that can no + longer be resolved by examining a (corrupted) parent. + + For cases like :mod:`ansible.module_utils.distro`, this must handle cases + where a package transmuted itself into a totally unrelated module during + import and vice versa, where :data:`sys.modules` is replaced with junk that + makes it impossible to discover the loaded module using the in-memory + module object or any parent package's :data:`__path__`, since they have all + been overwritten. Some men just want to watch the world burn. + """ + + @staticmethod + def _iter_parents(fullname): + """ + >>> list(ParentEnumerationMethod._iter_parents('a')) + [('', 'a')] + >>> list(ParentEnumerationMethod._iter_parents('a.b.c')) + [('a.b', 'c'), ('a', 'b'), ('', 'a')] + """ + while fullname: + fullname, _, modname = str_rpartition(fullname, u'.') + yield fullname, modname + + def _find_sane_parent(self, fullname): + """ + Iteratively search :data:`sys.modules` for the least indirect parent of + `fullname` that's from the same package and has a :data:`__path__` + attribute. + + :return: + `(parent_name, path, modpath)` tuple, where: + + * `modname`: canonical name of the found package, or the empty + string if none is found. + * `search_path`: :data:`__path__` attribute of the least + indirect parent found, or :data:`None` if no indirect parent + was found. + * `modpath`: list of module name components leading from `path` + to the target module. + """ + modpath = [] + for pkgname, modname in self._iter_parents(fullname): + modpath.insert(0, modname) + if not pkgname: + return [], None, modpath + + try: + pkg = sys.modules[pkgname] + except KeyError: + LOG.debug('%r: sys.modules[%r] absent, skipping', self, pkgname) + continue + + try: + resolved_pkgname = pkg.__name__ + except AttributeError: + LOG.debug('%r: %r has no __name__, skipping', self, pkg) + continue + + if resolved_pkgname != pkgname: + LOG.debug('%r: %r.__name__ is %r, skipping', + self, pkg, resolved_pkgname) + continue + + try: + path = pkg.__path__ + except AttributeError: + LOG.debug('%r: %r has no __path__, skipping', self, pkg) + continue + + if not path: + LOG.debug('%r: %r.__path__ is %r, skipping', self, pkg, path) + continue + + return pkgname.split('.'), path, modpath + + def _found_package(self, fullname, path): + path = os.path.join(path, '__init__.py') + LOG.debug('%r: %r is PKG_DIRECTORY: %r', self, fullname, path) + return self._found_module( + fullname=fullname, + path=path, + fp=open(path, 'rb'), + is_pkg=True, + ) + + def _found_module(self, fullname, path, fp, is_pkg=False): + try: + path, _ = _py_filename(path) + if not path: + return + + source = fp.read() + finally: + if fp: + fp.close() + + if isinstance(source, mitogen.core.UnicodeType): + # get_source() returns "string" according to PEP-302, which was + # reinterpreted for Python 3 to mean a Unicode string. + source = source.encode('utf-8') + return path, source, is_pkg + + def _find_one_component(self, modname, search_path): + try: + #fp, path, (suffix, _, kind) = imp.find_module(modname, search_path) + # FIXME The imp module was removed in Python 3.12. + return imp.find_module(modname, search_path) + except ImportError: + e = sys.exc_info()[1] + LOG.debug('%r: imp.find_module(%r, %r) -> %s', + self, modname, [search_path], e) + return None + + def find(self, fullname): + """ + See implementation for a description of how this works. + """ + if sys.version_info >= (3, 4): + return None + + #if fullname not in sys.modules: + # Don't attempt this unless a module really exists in sys.modules, + # else we could return junk. + #return + + fullname = to_text(fullname) + modname, search_path, modpath = self._find_sane_parent(fullname) + while True: + tup = self._find_one_component(modpath.pop(0), search_path) + if tup is None: + return None + + fp, path, (suffix, _, kind) = tup + if modpath: + # Still more components to descent. Result must be a package + if fp: + fp.close() + if kind != imp.PKG_DIRECTORY: + LOG.debug('%r: %r appears to be child of non-package %r', + self, fullname, path) + return None + search_path = [path] + elif kind == imp.PKG_DIRECTORY: + return self._found_package(fullname, path) + else: + return self._found_module(fullname, path, fp) + + +class ParentSpecEnumerationMethod(ParentImpEnumerationMethod): + def _find_parent_spec(self, fullname): + #history = [] + debug = self.log.debug + children = [] + for parent_name, child_name in self._iter_parents(fullname): + children.insert(0, child_name) + if not parent_name: + debug('abandoning %r, reached top-level', fullname) + return None, children + + try: + parent = sys.modules[parent_name] + except KeyError: + debug('skipping %r, not in sys.modules', parent_name) + continue + + try: + spec = parent.__spec__ + except AttributeError: + debug('skipping %r: %r.__spec__ is absent', + parent_name, parent) + continue + + if not spec: + debug('skipping %r: %r.__spec__=%r', + parent_name, parent, spec) + continue + + if spec.name != parent_name: + debug('skipping %r: %r.__spec__.name=%r does not match', + parent_name, parent, spec.name) + continue + + if not spec.submodule_search_locations: + debug('skipping %r: %r.__spec__.submodule_search_locations=%r', + parent_name, parent, spec.submodule_search_locations) + continue + + return spec, children + + raise ValueError('%s._find_parent_spec(%r) unexpectedly reached bottom' + % (self.__class__.__name__, fullname)) + + def find(self, fullname): + # Returns absolute path, ParentImpEnumerationMethod returns relative + # >>> spec_pem.find('six_brokenpkg._six')[::2] + # ('/Users/alex/src/mitogen/tests/data/importer/six_brokenpkg/_six.py', False) + + if sys.version_info < (3, 4): + return None + + fullname = to_text(fullname) + spec, children = self._find_parent_spec(fullname) + for child_name in children: + if spec: + name = '%s.%s' % (spec.name, child_name) + submodule_search_locations = spec.submodule_search_locations + else: + name = child_name + submodule_search_locations = None + spec = importlib.util._find_spec(name, submodule_search_locations) + if spec is None: + self.log.debug('%r spec unavailable from %s', fullname, spec) + return None + + is_package = spec.submodule_search_locations is not None + if name != fullname: + if not is_package: + self.log.debug('%r appears to be child of non-package %r', + fullname, spec) + return None + continue + + if not spec.has_location: + self.log.debug('%r.origin cannot be read as a file', spec) + return None + + if os.path.splitext(spec.origin)[1] != '.py': + self.log.debug('%r.origin does not contain Python source code', + spec) + return None + + # FIXME This should use loader.get_source() + with open(spec.origin, 'rb') as f: + source = f.read() + + return spec.origin, source, is_package + + raise ValueError('%s.find(%r) unexpectedly reached bottom' + % (self.__class__.__name__, fullname)) + + +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. + """ + + # Fullnames of modules that should not be sent as a related module + _related_modules_denylist = frozenset({ + '__main__', + }) + + def __init__(self): + #: Import machinery is expensive, keep :py:meth`:get_module_source` + #: results around. + self._found_cache = {} + + #: Avoid repeated dependency scanning, which is expensive. + self._related_cache = {} + + def __repr__(self): + return 'ModuleFinder()' + + def add_source_override(self, fullname, path, source, is_pkg): + """ + Explicitly install a source cache entry, preventing usual lookup + methods from being used. + + Beware the value of `path` is critical when `is_pkg` is specified, + since it directs where submodules are searched for. + + :param str fullname: + Name of the module to override. + :param str path: + Module's path as it will appear in the cache. + :param bytes source: + Module source code as a bytestring. + :param bool is_pkg: + :data:`True` if the module is a package. + """ + self._found_cache[fullname] = (path, source, is_pkg) + + get_module_methods = [ + DefectivePython3xMainMethod(), + PkgutilMethod(), + SysModulesMethod(), + ParentSpecEnumerationMethod(), + ParentImpEnumerationMethod(), + ] + + def get_module_source(self, fullname): + """ + Given the name of a loaded module `fullname`, attempt to find its + source code. + + :returns: + Tuple of `(module path, source text, is package?)`, or :data:`None` + if the source cannot be found. + """ + tup = self._found_cache.get(fullname) + if tup: + return tup + + for method in self.get_module_methods: + tup = method.find(fullname) + if tup: + #LOG.debug('%r returned %r', method, tup) + break + else: + tup = None, None, None + LOG.debug('get_module_source(%r): cannot find source', fullname) + + self._found_cache[fullname] = tup + return tup + + 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. + """ + mod = sys.modules.get(fullname, None) + if hasattr(mod, '__path__'): + fullname += '.__init__' + + if level == 0 or not fullname: + return '' + + bits = fullname.split('.') + if len(bits) <= level: + # This would be an ImportError in real code. + return '' + + return '.'.join(bits[:-level]) + '.' + + def generate_parent_names(self, fullname): + while '.' in fullname: + fullname, _, _ = str_rpartition(to_text(fullname), u'.') + yield fullname + + def _reject_related_module(self, requested_fullname, related_fullname): + def _log_reject(reason): + LOG.debug( + '%r: Rejected related module %s of requested module %s: %s', + self, related_fullname, requested_fullname, reason, + ) + return reason + + try: + related_module = sys.modules[related_fullname] + except KeyError: + return _log_reject('sys.modules entry absent') + + # Python 2.x "indirection entry" + if related_module is None: + return _log_reject('sys.modules entry is None') + + if is_stdlib_name(related_fullname): + return _log_reject('stdlib module') + + if 'six.moves' in related_fullname: + return _log_reject('six.moves avoidence') + + if related_fullname in self._related_modules_denylist: + return _log_reject('on denylist') + + return False + + def find_related_imports(self, fullname): + """ + 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 + `fullname`, compiling it, and examining all IMPORT_NAME ops. + + :param fullname: Fully qualified name of an *already imported* module + for which source code can be retrieved + :type fullname: str + """ + related = self._related_cache.get(fullname) + if related is not None: + return related + + modpath, src, _ = self.get_module_source(fullname) + if src is None: + return [] + + maybe_names = list(self.generate_parent_names(fullname)) + + co = compile(src, modpath, 'exec') + for level, modname, namelist in mitogen.imports.codeobj_imports(co): + if level == -1: + modnames = [modname, '%s.%s' % (fullname, modname)] + else: + modnames = [ + '%s%s' % (self.resolve_relpath(fullname, level), modname) + ] + + maybe_names.extend(modnames) + maybe_names.extend( + '%s.%s' % (mname, name) + for mname in modnames + for name in namelist + ) + + return self._related_cache.setdefault(fullname, sorted( + set( + mitogen.core.to_text(name) + for name in maybe_names + if not self._reject_related_module(fullname, name) + ) + )) + + def find_related(self, fullname): + """ + Return a list of non-stdlib modules that are imported directly or + indirectly by `fullname`, plus their parents. + + 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 + :type fullname: str + """ + stack = [fullname] + found = set() + + while stack: + name = stack.pop(0) + names = self.find_related_imports(name) + stack.extend(set(names).difference(set(found).union(stack))) + found.update(names) + + found.discard(fullname) + return sorted(found) + + +class ModuleResponder(object): + def __init__(self, router, policy): + self._log = logging.getLogger('mitogen.responder') + self._router = router + self._finder = ModuleFinder() + self._cache = {} # fullname -> pickled + self.policy = policy + + #: Context -> set([fullname, ..]) + self._forwarded_by_context = {} + + #: Number of GET_MODULE messages received. + self.get_module_count = 0 + #: Total time spent in uncached GET_MODULE. + self.get_module_secs = 0.0 + #: Total time spent minifying modules. + self.minify_secs = 0.0 + #: Number of successful LOAD_MODULE messages sent. + self.good_load_module_count = 0 + #: Total bytes in successful LOAD_MODULE payloads. + self.good_load_module_size = 0 + #: Number of negative LOAD_MODULE messages sent. + self.bad_load_module_count = 0 + + router.add_handler( + fn=self._on_get_module, + handle=mitogen.core.GET_MODULE, + ) + + def __repr__(self): + return 'ModuleResponder' + + def add_source_override(self, fullname, path, source, is_pkg): + """ + See :meth:`ModuleFinder.add_source_override`. + """ + self._finder.add_source_override(fullname, path, source, is_pkg) + + MAIN_RE = re.compile(b(r'^if\s+__name__\s*==\s*.__main__.\s*:'), re.M) + main_guard_msg = ( + "A child context attempted to import __main__, however the main " + "module present in the master process lacks an execution guard. " + "Update %r to prevent unintended execution, using a guard like:\n" + "\n" + " if __name__ == '__main__':\n" + " # your code here.\n" + ) + + def whitelist_prefix(self, fullname): + if not self.policy.overrides: + self.policy.overrides.add('mitogen') + self.policy.overrides.add(fullname) + + def blacklist_prefix(self, fullname): + self.policy.blocks.add(fullname) + + def neutralize_main(self, path, src): + """ + Given the source for the __main__ module, try to find where it begins + conditional execution based on a "if __name__ == '__main__'" guard, and + remove any code after that point. + """ + match = self.MAIN_RE.search(src) + if match: + return src[:match.start()] + + if b('mitogen.main(') in src: + return src + + self._log.error(self.main_guard_msg, path) + raise ImportError('refused') + + def _make_negative_response(self, fullname): + return (fullname, None, None, None, ()) + + minify_safe_re = re.compile(b(r'\s+#\s*!mitogen:\s*minify_safe')) + + def _build_tuple(self, fullname): + if fullname in self._cache: + return self._cache[fullname] + + self.policy.denied_raise(fullname) + + path, source, is_pkg = self._finder.get_module_source(fullname) + if path and is_stdlib_path(path): + # Prevent loading of 2.x<->3.x stdlib modules! This costs one + # RTT per hit, so a client-side solution is also required. + self._log.debug('refusing to serve stdlib module %r', fullname) + tup = self._make_negative_response(fullname) + self._cache[fullname] = tup + return tup + + if source is None: + # TODO: make this .warning() or similar again once importer has its + # own logging category. + self._log.debug('could not find source for %r', fullname) + tup = self._make_negative_response(fullname) + self._cache[fullname] = tup + return tup + + if self.minify_safe_re.search(source): + # If the module contains a magic marker, it's safe to minify. + t0 = mitogen.core.now() + source = mitogen.minify.minimize_source(source).encode('utf-8') + self.minify_secs += mitogen.core.now() - t0 + + if is_pkg: + pkg_present = get_child_modules(path, fullname) + self._log.debug('%s is a package at %s with submodules %r', + fullname, path, pkg_present) + else: + pkg_present = None + + if fullname == '__main__': + source = self.neutralize_main(path, source) + compressed = mitogen.core.Blob(zlib.compress(source, 9)) + related = [ + to_text(name) + for name in self._finder.find_related(fullname) + if not self.policy.denied(name) + ] + # 0:fullname 1:pkg_present 2:path 3:compressed 4:related + tup = ( + to_text(fullname), + pkg_present, + to_text(path), + compressed, + related + ) + self._cache[fullname] = tup + return tup + + def _send_load_module(self, stream, fullname, reason): + if fullname not in stream.protocol.sent_modules: + tup = self._build_tuple(fullname) + msg = mitogen.core.Message.pickled( + tup, + dst_id=stream.protocol.remote_id, + handle=mitogen.core.LOAD_MODULE, + ) + self._log.debug( + 'sending %s %s (%.2f KiB) to %s', + reason, fullname, len(msg.data) / 1024.0, stream.name, + ) + self._router._async_route(msg) + stream.protocol.sent_modules.add(fullname) + if tup[2] is not None: + self.good_load_module_count += 1 + self.good_load_module_size += len(msg.data) + else: + self.bad_load_module_count += 1 + + def _send_module_load_failed(self, stream, fullname): + self.bad_load_module_count += 1 + stream.protocol.send( + mitogen.core.Message.pickled( + self._make_negative_response(fullname), + dst_id=stream.protocol.remote_id, + handle=mitogen.core.LOAD_MODULE, + ) + ) + + def _send_module_and_related(self, stream, fullname): + if fullname in stream.protocol.sent_modules: + return + + try: + tup = self._build_tuple(fullname) + for name in tup[4]: # related + parent, _, _ = str_partition(name, '.') + if parent != fullname and parent not in stream.protocol.sent_modules: + # Parent hasn't been sent, so don't load submodule yet. + continue + + self._send_load_module(stream, name, 'related') + self._send_load_module(stream, fullname, 'requested') + except Exception: + LOG.debug('While importing %r', fullname, exc_info=True) + self._send_module_load_failed(stream, fullname) + + def _on_get_module(self, msg): + if msg.is_dead: + return + + stream = self._router.stream_by_id(msg.src_id) + if stream is None: + return + + fullname = msg.data.decode() + self._log.debug('%s requested module %s', stream.name, fullname) + self.get_module_count += 1 + if fullname in stream.protocol.sent_modules: + LOG.warning('_on_get_module(): dup request for %r from %r', + fullname, stream) + + t0 = mitogen.core.now() + try: + self._send_module_and_related(stream, fullname) + finally: + self.get_module_secs += mitogen.core.now() - t0 + + def _send_forward_module(self, stream, context, fullname): + if stream.protocol.remote_id != context.context_id: + stream.protocol._send( + mitogen.core.Message( + data=b('%s\x00%s' % (context.context_id, fullname)), + handle=mitogen.core.FORWARD_MODULE, + dst_id=stream.protocol.remote_id, + ) + ) + + def _forward_one_module(self, context, fullname): + forwarded = self._forwarded_by_context.get(context) + if forwarded is None: + forwarded = set() + self._forwarded_by_context[context] = forwarded + + if fullname in forwarded: + return + + path = [] + while fullname: + path.append(fullname) + fullname, _, _ = str_rpartition(fullname, u'.') + + stream = self._router.stream_by_id(context.context_id) + if stream is None: + LOG.debug('%r: dropping forward of %s to no longer existent ' + '%r', self, path[0], context) + return + + for fullname in reversed(path): + self._send_module_and_related(stream, fullname) + self._send_forward_module(stream, context, fullname) + + def _forward_modules(self, context, fullnames): + IOLOG.debug('%r._forward_modules(%r, %r)', self, context, fullnames) + for fullname in fullnames: + self._forward_one_module(context, mitogen.core.to_text(fullname)) + + def forward_modules(self, context, fullnames): + self._router.broker.defer(self._forward_modules, context, fullnames) + + +class ResourceResponder(object): + def __init__(self, router): + self._router = router + self._router.add_handler( + self._on_get_resource, + mitogen.core.GET_RESOURCE, + ) + + def _on_get_resource(self, msg): + if msg.is_dead: + return + stream = self._router.stream_by_id(msg.src_id) + if stream is None: + return + fullname, resource = msg.unpickle() + try: + content = importlib.resources.read_binary(fullname, resource) + except (FileNotFoundError, IsADirectoryError): + content = None + + msg = mitogen.core.Message.pickled( + (fullname, resource), content, + dst_id=stream.protocol.remote_id, + handle=mitogen.core.LOAD_RESOURCE, + ) + + if content is not None: + self._router._async_route(msg) + else: + stream.protocol.send(msg) + + +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 + + def __init__(self, install_watcher=True): + if install_watcher: + self._watcher = ThreadWatcher.watch( + target=mitogen.core.threading__current_thread(), + on_join=self.shutdown, + ) + super(Broker, self).__init__() + self.timers = mitogen.parent.TimerList() + + def shutdown(self): + super(Broker, self).shutdown() + if self._watcher: + self._watcher.remove() + + +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. + + :param int max_message_size: + Override the maximum message size this router is willing to receive or + transmit. Any value set here is automatically inherited by any children + created by the router. + + This has a liberal default of 128 MiB, but may be set much lower. + Beware that setting it below 64KiB may encourage unexpected failures as + parents and children can no longer route large Python modules that may + be required by your application. + """ + + 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:: + #: + #: mitogen.master.Router.profiling = True + profiling = os.environ.get('MITOGEN_PROFILING') is not None + + def __init__(self, broker=None, max_message_size=None): + if broker is None: + broker = self.broker_class() + if max_message_size: + self.max_message_size = max_message_size + super(Router, self).__init__(broker) + self.upgrade() + + def upgrade(self): + self.id_allocator = IdAllocator(self) + self.responder = ModuleResponder(self, mitogen.core.ImportPolicy()) + self.resource_responder = ResourceResponder(self) + self.log_forwarder = LogForwarder(self) + self.route_monitor = mitogen.parent.RouteMonitor(router=self) + self.add_handler( # TODO: cutpaste. + fn=self._on_detaching, + handle=mitogen.core.DETACHING, + persist=True, + ) + + def _on_broker_exit(self): + super(Router, self)._on_broker_exit() + dct = self.get_stats() + dct['self'] = self + dct['minify_ms'] = 1000 * dct['minify_secs'] + dct['get_module_ms'] = 1000 * dct['get_module_secs'] + dct['good_load_module_size_kb'] = dct['good_load_module_size'] / 1024.0 + dct['good_load_module_size_avg'] = ( + ( + dct['good_load_module_size'] / + (float(dct['good_load_module_count']) or 1.0) + ) / 1024.0 + ) + + LOG.debug( + '%(self)r: stats: ' + '%(get_module_count)d module requests in ' + '%(get_module_ms)d ms, ' + '%(good_load_module_count)d sent ' + '(%(minify_ms)d ms minify time), ' + '%(bad_load_module_count)d negative responses. ' + 'Sent %(good_load_module_size_kb).01f kb total, ' + '%(good_load_module_size_avg).01f kb avg.' + % dct + ) + + def get_stats(self): + """ + Return performance data for the module responder. + + :returns: + + Dict containing keys: + + * `get_module_count`: Integer count of + :data:`mitogen.core.GET_MODULE` messages received. + * `get_module_secs`: Floating point total seconds spent servicing + :data:`mitogen.core.GET_MODULE` requests. + * `good_load_module_count`: Integer count of successful + :data:`mitogen.core.LOAD_MODULE` messages sent. + * `good_load_module_size`: Integer total bytes sent in + :data:`mitogen.core.LOAD_MODULE` message payloads. + * `bad_load_module_count`: Integer count of negative + :data:`mitogen.core.LOAD_MODULE` messages sent. + * `minify_secs`: CPU seconds spent minifying modules marked + minify-safe. + """ + return { + 'get_module_count': self.responder.get_module_count, + 'get_module_secs': self.responder.get_module_secs, + 'good_load_module_count': self.responder.good_load_module_count, + 'good_load_module_size': self.responder.good_load_module_size, + 'bad_load_module_count': self.responder.bad_load_module_count, + 'minify_secs': self.responder.minify_secs, + } + + 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 + + def __enter__(self): + return self + + def __exit__(self, e_type, e_val, tb): + self.broker.shutdown() + self.broker.join() + + def disconnect_stream(self, stream): + self.broker.defer(stream.on_disconnect, self.broker) + + def disconnect_all(self): + # making stream_by_id python3-safe by converting stream_by_id values iter to list + for stream in list(self._stream_by_id.values()): + self.disconnect_stream(stream) + + +class IdAllocator(object): + """ + Allocate IDs for new contexts constructed locally, and blocks of IDs for + children to allocate their own IDs using + :class:`mitogen.parent.ChildIdAllocator` without risk of conflict, and + without necessitating network round-trips for each new context. + + This class responds to :data:`mitogen.core.ALLOCATE_ID` messages received + from children by replying with fresh block ID allocations. + + The master's :class:`IdAllocator` instance can be accessed via + :attr:`mitogen.master.Router.id_allocator`. + """ + #: Block allocations are made in groups of 1000 by default. + BLOCK_SIZE = 1000 + + def __init__(self, router): + self.router = router + self.next_id = 1 + self.lock = threading.Lock() + router.add_handler( + fn=self.on_allocate_id, + handle=mitogen.core.ALLOCATE_ID, + ) + + def __repr__(self): + return 'IdAllocator(%r)' % (self.router,) + + def allocate(self): + """ + Allocate a context ID by directly incrementing an internal counter. + + :returns: + The new context ID. + """ + self.lock.acquire() + try: + id_ = self.next_id + self.next_id += 1 + return id_ + finally: + self.lock.release() + + def allocate_block(self): + """ + Allocate a block of IDs for use in a child context. + + This function is safe to call from any thread. + + :returns: + Tuple of the form `(id, end_id)` where `id` is the first usable ID + and `end_id` is the last usable ID. + """ + self.lock.acquire() + try: + id_ = self.next_id + self.next_id += self.BLOCK_SIZE + end_id = id_ + self.BLOCK_SIZE + LOG.debug('%r: allocating [%d..%d)', self, id_, end_id) + return id_, end_id + finally: + self.lock.release() + + def on_allocate_id(self, msg): + if msg.is_dead: + return + + id_, last_id = self.allocate_block() + requestee = self.router.context_by_id(msg.src_id) + LOG.debug('%r: allocating [%r..%r) to %r', + self, id_, last_id, requestee) + msg.reply((id_, last_id)) diff --git a/plugins/mitogen-0.3.44/mitogen/minify.py b/plugins/mitogen-0.3.44/mitogen/minify.py new file mode 100644 index 0000000..8f78b2a --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/minify.py @@ -0,0 +1,143 @@ +# Copyright 2017, Alex Willmer +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import sys + +try: + from io import StringIO +except ImportError: + from StringIO import StringIO + +import mitogen.core + +if sys.version_info < (2, 7, 11): + from mitogen.compat import tokenize +else: + import tokenize + + +def minimize_source(source): + """ + Remove comments and docstrings from Python `source`, preserving line + numbers and syntax of empty blocks. + + :param str source: + The source to minimize. + + :returns str: + The minimized source. + """ + source = mitogen.core.to_text(source) + tokens = tokenize.generate_tokens(StringIO(source).readline) + tokens = strip_comments(tokens) + tokens = strip_docstrings(tokens) + tokens = reindent(tokens) + return tokenize.untokenize(tokens) + + +def strip_comments(tokens): + """ + Drop comment tokens from a `tokenize` stream. + + Comments on lines 1-2 are kept, to preserve hashbang and encoding. + Trailing whitespace is remove from all lines. + """ + prev_typ = None + prev_end_col = 0 + for typ, tok, (start_row, start_col), (end_row, end_col), line in tokens: + if typ in (tokenize.NL, tokenize.NEWLINE): + if prev_typ in (tokenize.NL, tokenize.NEWLINE): + start_col = 0 + else: + start_col = prev_end_col + end_col = start_col + 1 + elif typ == tokenize.COMMENT and start_row > 2: + continue + prev_typ = typ + prev_end_col = end_col + yield typ, tok, (start_row, start_col), (end_row, end_col), line + + +def strip_docstrings(tokens): + """ + Replace docstring tokens with NL tokens in a `tokenize` stream. + + Any STRING token not part of an expression is deemed a docstring. + Indented docstrings are not yet recognised. + """ + stack = [] + state = 'wait_string' + for t in tokens: + typ = t[0] + if state == 'wait_string': + if typ in (tokenize.NL, tokenize.COMMENT): + yield t + elif typ in (tokenize.DEDENT, tokenize.INDENT, tokenize.STRING): + stack.append(t) + elif typ == tokenize.NEWLINE: + stack.append(t) + start_line, end_line = stack[0][2][0], stack[-1][3][0]+1 + for i in mitogen.core.range(start_line, end_line): + yield tokenize.NL, '\n', (i, 0), (i,1), '\n' + for t in stack: + if t[0] in (tokenize.DEDENT, tokenize.INDENT): + yield t[0], t[1], (i+1, t[2][1]), (i+1, t[3][1]), t[4] + del stack[:] + else: + stack.append(t) + for t in stack: yield t + del stack[:] + state = 'wait_newline' + elif state == 'wait_newline': + if typ == tokenize.NEWLINE: + state = 'wait_string' + yield t + + +def reindent(tokens, indent=' '): + """ + Replace existing indentation in a token steam, with `indent`. + """ + old_levels = [] + old_level = 0 + new_level = 0 + for typ, tok, (start_row, start_col), (end_row, end_col), line in tokens: + if typ == tokenize.INDENT: + old_levels.append(old_level) + old_level = len(tok) + new_level += 1 + tok = indent * new_level + elif typ == tokenize.DEDENT: + old_level = old_levels.pop() + new_level -= 1 + start_col = max(0, start_col - old_level + new_level) + if start_row == end_row: + end_col = start_col + len(tok) + yield typ, tok, (start_row, start_col), (end_row, end_col), line diff --git a/plugins/mitogen-0.3.44/mitogen/os_fork.py b/plugins/mitogen-0.3.44/mitogen/os_fork.py new file mode 100644 index 0000000..f85534b --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/os_fork.py @@ -0,0 +1,187 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +Support for operating in a mixed threading/forking environment. +""" + +import os +import socket +import sys +import weakref + +import mitogen.core +import mitogen.parent + + +# List of weakrefs. On Python 2.4, mitogen.core registers its Broker on this +# list and mitogen.service registers its Pool too. +_brokers = weakref.WeakKeyDictionary() +_pools = weakref.WeakKeyDictionary() + + +def _notice_broker_or_pool(obj): + """ + Used by :mod:`mitogen.core` and :mod:`mitogen.service` to automatically + register every broker and pool on Python 2.4/2.5. + """ + if isinstance(obj, mitogen.core.Broker): + _brokers[obj] = True + else: + _pools[obj] = True + + +def wrap_os__fork(): + corker = Corker( + brokers=list(_brokers), + pools=list(_pools), + ) + try: + corker.cork() + return os__fork() + finally: + corker.uncork() + + +# If Python 2.4/2.5 where threading state is not fixed up, subprocess.Popen() +# may still deadlock due to the broker thread. In this case, pause os.fork() so +# that all active threads are paused during fork. +if sys.version_info < (2, 6): + os__fork = os.fork + os.fork = wrap_os__fork + + +class Corker(object): + """ + Arrange for :class:`mitogen.core.Broker` and optionally + :class:`mitogen.service.Pool` to be temporarily "corked" while fork + operations may occur. + + In a mixed threading/forking environment, it is critical no threads are + active at the moment of fork, as they could hold mutexes whose state is + unrecoverably snapshotted in the locked state in the fork child, causing + deadlocks at random future moments. + + To ensure a target thread has all locks dropped, it is made to write a + large string to a socket with a small buffer that has :data:`os.O_NONBLOCK` + disabled. CPython will drop the GIL and enter the ``write()`` system call, + where it will block until the socket buffer is drained, or the write side + is closed. + + :class:`mitogen.core.Poller` is used to ensure the thread really has + blocked outside any Python locks, by checking if the socket buffer has + started to fill. + + Since this necessarily involves posting a message to every existent thread + and verifying acknowledgement, it will never be a fast operation. + + This does not yet handle the case of corking being initiated from within a + thread that is also a cork target. + + :param brokers: + Sequence of :class:`mitogen.core.Broker` instances to cork. + :param pools: + Sequence of :class:`mitogen.core.Pool` instances to cork. + """ + def __init__(self, brokers=(), pools=()): + self.brokers = brokers + self.pools = pools + + def _do_cork(self, s, wsock): + try: + try: + while True: + # at least EINTR is possible. Do our best to keep handling + # outside the GIL in this case using sendall(). + wsock.sendall(s) + except socket.error: + pass + finally: + wsock.close() + + def _cork_one(self, s, obj): + """ + Construct a socketpair, saving one side of it, and passing the other to + `obj` to be written to by one of its threads. + """ + rsock, wsock = mitogen.parent.create_socketpair(size=4096) + mitogen.core.set_blocking(wsock.fileno(), True) # gevent + mitogen.core.set_cloexec(rsock.fileno()) + mitogen.core.set_cloexec(wsock.fileno()) + self._rsocks.append(rsock) + obj.defer(self._do_cork, s, wsock) + + def _verify_one(self, rsock): + """ + Pause until the socket `rsock` indicates readability, due to + :meth:`_do_cork` triggering a blocking write on another thread. + """ + poller = mitogen.core.Poller() + poller.start_receive(rsock.fileno()) + try: + while True: + for fd in poller.poll(): + return + finally: + poller.close() + + def cork(self): + """ + Arrange for any associated brokers and pools to be paused with no locks + held. This will not return until each thread acknowledges it has ceased + execution. + """ + current = mitogen.core.threading__current_thread() + s = mitogen.core.b('CORK') * ((128 // 4) * 1024) + self._rsocks = [] + + # Pools must be paused first, as existing work may require the + # participation of a broker in order to complete. + for pool in self.pools: + if not pool.closed: + for th in pool._threads: + if th != current: + self._cork_one(s, pool) + + for broker in self.brokers: + if broker._alive: + if broker._thread != current: + self._cork_one(s, broker) + + # Pause until we can detect every thread has entered write(). + for rsock in self._rsocks: + self._verify_one(rsock) + + def uncork(self): + """ + Arrange for paused threads to resume operation. + """ + for rsock in self._rsocks: + rsock.close() diff --git a/plugins/mitogen-0.3.44/mitogen/parent.py b/plugins/mitogen-0.3.44/mitogen/parent.py new file mode 100644 index 0000000..3f67805 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/parent.py @@ -0,0 +1,2840 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +This module defines functionality common to master and parent processes. It is +sent to any child context that is due to become a parent, due to recursive +connection. +""" + +import binascii +import errno +import fcntl +import getpass +import heapq +import inspect +import logging +import os +import re +import pty +import signal +import socket +import struct +import subprocess +import sys +import termios +import textwrap +import threading +import zlib + +# Absolute imports for <2.5. +select = __import__('select') + +import mitogen.core +from mitogen.core import b +from mitogen.core import bytes_partition +from mitogen.core import IOLOG +from mitogen.core import itervalues +from mitogen.core import next +from mitogen.core import thread + + +LOG = logging.getLogger(__name__) + +# #410: we must avoid the use of socketpairs if SELinux is enabled. +try: + fp = open('/sys/fs/selinux/enforce', 'rb') + try: + SELINUX_ENABLED = bool(int(fp.read())) + finally: + fp.close() +except IOError: + SELINUX_ENABLED = False + + +if sys.version_info >= (3, 0): + closure_attr = '__closure__' + IM_SELF_ATTR = '__self__' +else: + closure_attr = 'func_closure' + IM_SELF_ATTR = 'im_self' + + +try: + SC_OPEN_MAX = os.sysconf('SC_OPEN_MAX') +except ValueError: + SC_OPEN_MAX = 1024 + +BROKER_SHUTDOWN_MSG = ( + 'Connection cancelled because the associated Broker began to shut down.' +) + +OPENPTY_MSG = ( + "Failed to create a PTY: %s. It is likely the maximum number of PTYs has " + "been reached. Consider increasing the 'kern.tty.ptmx_max' sysctl on OS " + "X, the 'kernel.pty.max' sysctl on Linux, or modifying your configuration " + "to avoid PTY use." +) + +SYS_EXECUTABLE_MSG = ( + "The Python sys.executable variable is unset, indicating Python was " + "unable to determine its original program name. Unless explicitly " + "configured otherwise, child contexts will be started using " + "'/usr/bin/python'" +) +_sys_executable_warning_logged = False + + +def _ioctl_cast(n): + """ + Linux ioctl() request parameter is unsigned, whereas on BSD/Darwin it is + signed. Until 2.5 Python exclusively implemented the BSD behaviour, + preventing use of large unsigned int requests like the TTY layer uses + below. So on 2.4, we cast our unsigned to look like signed for Python. + """ + if sys.version_info < (2, 5): + n, = struct.unpack('i', struct.pack('I', n)) + return n + + +# If not :data:`None`, called prior to exec() of any new child process. Used by +# :func:`mitogen.utils.reset_affinity` to allow the child to be freely +# scheduled. +_preexec_hook = None + +# Get PTY number; asm-generic/ioctls.h +LINUX_TIOCGPTN = _ioctl_cast(2147767344) + +# Lock/unlock PTY; asm-generic/ioctls.h +LINUX_TIOCSPTLCK = _ioctl_cast(1074025521) + +IS_LINUX = os.uname()[0] == 'Linux' +IS_SOLARIS = os.uname()[0] == 'SunOS' + + +SIGNAL_BY_NUM = dict( + (getattr(signal, name), name) + for name in sorted(vars(signal), reverse=True) + if name.startswith('SIG') and not name.startswith('SIG_') +) + +_core_source_lock = threading.Lock() +_core_source_partial = None + + +def get_log_level(): + return (LOG.getEffectiveLevel() or logging.INFO) + + +def get_sys_executable(): + """ + Return :data:`sys.executable` if it is set, otherwise return + ``"/usr/bin/python"`` and log a warning. + """ + if sys.executable: + return sys.executable + + global _sys_executable_warning_logged + if not _sys_executable_warning_logged: + LOG.warn(SYS_EXECUTABLE_MSG) + _sys_executable_warning_logged = True + + return '/usr/bin/python' + + +def _get_core_source(): + """ + In non-masters, simply fetch the cached mitogen.core source code via the + import mechanism. In masters, this function is replaced with a version that + performs minification directly. + """ + return inspect.getsource(mitogen.core) + + +def get_core_source_partial(): + """ + _get_core_source() is expensive, even with @lru_cache in minify.py, threads + can enter it simultaneously causing severe slowdowns. + """ + global _core_source_partial + + if _core_source_partial is None: + _core_source_lock.acquire() + try: + if _core_source_partial is None: + _core_source_partial = PartialZlib( + _get_core_source().encode('utf-8') + ) + finally: + _core_source_lock.release() + + return _core_source_partial + + +def get_default_remote_name(): + """ + Return the default name appearing in argv[0] of remote machines. + """ + s = u'%s@%s:%d' + s %= (getpass.getuser(), socket.gethostname(), os.getpid()) + # In mixed UNIX/Windows environments, the username may contain slashes. + return s.translate({ + ord(u'\\'): ord(u'_'), + ord(u'/'): ord(u'_') + }) + + +def is_immediate_child(msg, stream): + """ + Handler policy that requires messages to arrive only from immediately + connected children. + """ + return msg.src_id == stream.protocol.remote_id + + +def flags(names): + """ + Return the result of ORing a set of (space separated) :py:mod:`termios` + module constants together. + """ + i = 0 + skipped = [] + for name in names.split(): + try: + i |= getattr(termios, name) + except AttributeError: + skipped.append(name) + if skipped: + LOG.debug('Skipped termios attributes: %s', ', '.join(skipped)) + return i + + +def cfmakeraw(tflags): + """ + Given a list returned by :py:func:`termios.tcgetattr`, return a list + modified in a manner similar to the `cfmakeraw()` C library function, but + additionally disabling local echo. + """ + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = tflags + iflag &= ~flags('IMAXBEL IXOFF INPCK BRKINT PARMRK ' + 'ISTRIP INLCR ICRNL IXON IGNPAR IGNBRK') + oflag &= ~flags('OPOST') + lflag &= ~flags('ECHO ECHOE ECHOK ECHONL ICANON ISIG ' + 'IEXTEN NOFLSH TOSTOP PENDIN') + cflag &= ~flags('CSIZE PARENB') + cflag |= flags('CS8 CREAD') + return [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] + + +def disable_echo(fd): + old = termios.tcgetattr(fd) + new = cfmakeraw(old) + flags = getattr(termios, 'TCSASOFT', 0) + 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 + termios.tcsetattr(fd, flags, new) + + +def create_socketpair(size=None, blocking=None): + """ + Create a :func:`socket.socketpair` for use as a child's UNIX stdio + channels. As socketpairs are bidirectional, they are economical on file + descriptor usage as one descriptor can be used for ``stdin`` and + ``stdout``. As they are sockets their buffers are tunable, allowing large + buffers to improve file transfer throughput and reduce IO loop iterations. + """ + if size is None: + size = mitogen.core.CHUNK_SIZE + + parentfp, childfp = mitogen.core.socketpair(blocking) + for fp in parentfp, childfp: + fp.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, size) + + return parentfp, childfp + + +def create_best_pipe(escalates_privilege=False, blocking=None): + """ + By default we prefer to communicate with children over a UNIX socket, as a + single file descriptor can represent bidirectional communication, and a + cross-platform API exists to align buffer sizes with the needs of the + library. + + SELinux prevents us setting up a privileged process to inherit an AF_UNIX + socket, a facility explicitly designed as a better replacement for pipes, + because at some point in the mid 90s it might have been commonly possible + for AF_INET sockets to end up undesirably connected to a privileged + process, so let's make up arbitrary rules breaking all sockets instead. + + If SELinux is detected, fall back to using pipes. + + :param bool escalates_privilege: + If :data:`True`, the target program may escalate privileges, causing + SELinux to disconnect AF_UNIX sockets, so avoid those. + :param None|bool blocking: + If :data:`False` or :data:`True`, set non-blocking or blocking mode. + If :data:`None` (default), use default. + :returns: + `(parent_rfp, child_wfp, child_rfp, parent_wfp)` + """ + if (not escalates_privilege) or (not SELINUX_ENABLED): + parentfp, childfp = create_socketpair(blocking=blocking) + return parentfp, childfp, childfp, parentfp + + parent_rfp, child_wfp = mitogen.core.pipe(blocking) + try: + child_rfp, parent_wfp = mitogen.core.pipe(blocking) + return parent_rfp, child_wfp, child_rfp, parent_wfp + except: + parent_rfp.close() + child_wfp.close() + raise + + +def popen(**kwargs): + """ + Wrap :class:`subprocess.Popen` to ensure any global :data:`_preexec_hook` + is invoked in the child. + """ + real_preexec_fn = kwargs.pop('preexec_fn', None) + def preexec_fn(): + if _preexec_hook: + _preexec_hook() + if real_preexec_fn: + real_preexec_fn() + return subprocess.Popen(preexec_fn=preexec_fn, **kwargs) + + +def create_child(args, merge_stdio=False, stderr_pipe=False, + escalates_privilege=False, preexec_fn=None): + """ + Create a child process whose stdin/stdout is connected to a socket. + + :param list args: + Program argument vector. + :param bool merge_stdio: + If :data:`True`, arrange for `stderr` to be connected to the `stdout` + socketpair, rather than inherited from the parent process. This may be + necessary to ensure that no TTY is connected to any stdio handle, for + instance when using LXC. + :param bool stderr_pipe: + If :data:`True` and `merge_stdio` is :data:`False`, arrange for + `stderr` to be connected to a separate pipe, to allow any ongoing debug + logs generated by e.g. SSH to be output as the session progresses, + without interfering with `stdout`. + :param bool escalates_privilege: + If :data:`True`, the target program may escalate privileges, causing + SELinux to disconnect AF_UNIX sockets, so avoid those. + :param function preexec_fn: + If not :data:`None`, a function to run within the post-fork child + before executing the target program. + :returns: + :class:`Process` instance. + """ + parent_rfp, child_wfp, child_rfp, parent_wfp = create_best_pipe( + escalates_privilege=escalates_privilege + ) + + if merge_stdio: + stderr_r, stderr = None, child_wfp + elif stderr_pipe: + stderr_r, stderr = mitogen.core.pipe() + mitogen.core.set_cloexec(stderr_r.fileno()) + else: + stderr_r, stderr = None, None + + try: + proc = popen( + args=args, + stdin=child_rfp, + stdout=child_wfp, + stderr=stderr, + close_fds=True, + preexec_fn=preexec_fn, + ) + except: + child_rfp.close() + child_wfp.close() + parent_rfp.close() + parent_wfp.close() + if stderr_pipe: + stderr.close() + stderr_r.close() + raise + + child_rfp.close() + child_wfp.close() + if stderr_pipe: + stderr.close() + + return PopenProcess( + proc=proc, + stdin=parent_wfp, + stdout=parent_rfp, + stderr=stderr_r, + ) + + +def _acquire_controlling_tty(): + os.setsid() + if sys.platform in ('linux', 'linux2'): + # On Linux, the controlling tty becomes the first tty opened by a + # process lacking any prior tty. + tty_path = os.ttyname(pty.STDERR_FILENO) + tty_fd = os.open(tty_path, os.O_RDWR) + os.close(tty_fd) + if hasattr(termios, 'TIOCSCTTY') and not mitogen.core.IS_WSL and not IS_SOLARIS: + # #550: prehistoric WSL does not like TIOCSCTTY. + # On BSD an explicit ioctl is required. For some inexplicable reason, + # Python 2.6 on Travis also requires it. + fcntl.ioctl(pty.STDERR_FILENO, termios.TIOCSCTTY) + + +def _linux_broken_devpts_openpty(): + """ + #462: On broken Linux hosts with mismatched configuration (e.g. old + /etc/fstab template installed), /dev/pts may be mounted without the gid= + mount option, causing new slave devices to be created with the group ID of + the calling process. This upsets glibc, whose openpty() is required by + specification to produce a slave owned by a special group ID (which is + always the 'tty' group). + + Glibc attempts to use "pt_chown" to fix ownership. If that fails, it + chown()s the PTY directly, which fails due to non-root, causing openpty() + to fail with EPERM ("Operation not permitted"). Since we don't need the + magical TTY group to run sudo and su, open the PTY ourselves in this case. + """ + master_fd = None + try: + # Opening /dev/ptmx causes a PTY pair to be allocated, and the + # corresponding slave /dev/pts/* device to be created, owned by UID/GID + # matching this process. + master_fd = os.open('/dev/ptmx', os.O_RDWR) + # Clear the lock bit from the PTY. This a prehistoric feature from a + # time when slave device files were persistent. + fcntl.ioctl(master_fd, LINUX_TIOCSPTLCK, struct.pack('i', 0)) + # Since v4.13 TIOCGPTPEER exists to open the slave in one step, but we + # must support older kernels. Ask for the PTY number. + pty_num_s = fcntl.ioctl(master_fd, LINUX_TIOCGPTN, + struct.pack('i', 0)) + pty_num, = struct.unpack('i', pty_num_s) + pty_name = '/dev/pts/%d' % (pty_num,) + # Now open it with O_NOCTTY to ensure it doesn't change our controlling + # TTY. Otherwise when we close the FD we get killed by the kernel, and + # the child we spawn that should really attach to it will get EPERM + # during _acquire_controlling_tty(). + slave_fd = os.open(pty_name, os.O_RDWR|os.O_NOCTTY) + return master_fd, slave_fd + except OSError: + if master_fd is not None: + os.close(master_fd) + e = sys.exc_info()[1] + raise mitogen.core.StreamError(OPENPTY_MSG, e) + + +def openpty(): + """ + Call :func:`os.openpty`, raising a descriptive error if the call fails. + + :raises mitogen.core.StreamError: + Creating a PTY failed. + :returns: + `(master_fp, slave_fp)` file-like objects. + """ + try: + master_fd, slave_fd = os.openpty() + except OSError: + e = sys.exc_info()[1] + if not (IS_LINUX and e.args[0] == errno.EPERM): + raise mitogen.core.StreamError(OPENPTY_MSG, e) + master_fd, slave_fd = _linux_broken_devpts_openpty() + + master_fp = os.fdopen(master_fd, 'r+b', 0) + slave_fp = os.fdopen(slave_fd, 'r+b', 0) + if not IS_SOLARIS: + disable_echo(master_fd) + disable_echo(slave_fd) + mitogen.core.set_blocking(slave_fd, True) + return master_fp, slave_fp + + +def tty_create_child(args): + """ + Return a file descriptor connected to the master end of a pseudo-terminal, + whose slave end is connected to stdin/stdout/stderr of a new child process. + The child is created such that the pseudo-terminal becomes its controlling + TTY, ensuring access to /dev/tty returns a new file descriptor open on the + slave end. + + :param list args: + Program argument vector. + :returns: + :class:`Process` instance. + """ + master_fp, slave_fp = openpty() + try: + proc = popen( + args=args, + stdin=slave_fp, + stdout=slave_fp, + stderr=slave_fp, + preexec_fn=_acquire_controlling_tty, + close_fds=True, + ) + except: + master_fp.close() + slave_fp.close() + raise + + slave_fp.close() + return PopenProcess( + proc=proc, + stdin=master_fp, + stdout=master_fp, + ) + + +def hybrid_tty_create_child(args, escalates_privilege=False): + """ + Like :func:`tty_create_child`, except attach stdin/stdout to a socketpair + like :func:`create_child`, but leave stderr and the controlling TTY + attached to a TTY. + + This permits high throughput communication with programs that are reached + via some program that requires a TTY for password input, like many + configurations of sudo. The UNIX TTY layer tends to have tiny (no more than + 14KiB) buffers, forcing many IO loop iterations when transferring bulk + data, causing significant performance loss. + + :param bool escalates_privilege: + If :data:`True`, the target program may escalate privileges, causing + SELinux to disconnect AF_UNIX sockets, so avoid those. + :param list args: + Program argument vector. + :returns: + :class:`Process` instance. + """ + master_fp, slave_fp = openpty() + try: + parent_rfp, child_wfp, child_rfp, parent_wfp = create_best_pipe( + escalates_privilege=escalates_privilege, + ) + try: + mitogen.core.set_blocking(child_rfp.fileno(), True) + mitogen.core.set_blocking(child_wfp.fileno(), True) + proc = popen( + args=args, + stdin=child_rfp, + stdout=child_wfp, + stderr=slave_fp, + preexec_fn=_acquire_controlling_tty, + close_fds=True, + ) + except: + parent_rfp.close() + child_wfp.close() + parent_wfp.close() + child_rfp.close() + raise + except: + master_fp.close() + slave_fp.close() + raise + + slave_fp.close() + child_rfp.close() + child_wfp.close() + return PopenProcess( + proc=proc, + stdin=parent_wfp, + stdout=parent_rfp, + stderr=master_fp, + ) + + +class Timer(object): + """ + Represents a future event. + """ + #: Set to :data:`False` if :meth:`cancel` has been called, or immediately + #: prior to being executed by :meth:`TimerList.expire`. + active = True + + def __init__(self, when, func): + self.when = when + self.func = func + + def __repr__(self): + return 'Timer(%r, %r)' % (self.when, self.func) + + def __eq__(self, other): + return self.when == other.when + + def __lt__(self, other): + return self.when < other.when + + def __le__(self, other): + return self.when <= other.when + + def cancel(self): + """ + Cancel this event. If it has not yet executed, it will not execute + during any subsequent :meth:`TimerList.expire` call. + """ + self.active = False + + +class TimerList(object): + """ + Efficiently manage a list of cancellable future events relative to wall + clock time. An instance of this class is installed as + :attr:`mitogen.master.Broker.timers` by default, and as + :attr:`mitogen.core.Broker.timers` in children after a call to + :func:`mitogen.parent.upgrade_router`. + + You can use :class:`TimerList` to cause the broker to wake at arbitrary + future moments, useful for implementing timeouts and polling in an + asynchronous context. + + :class:`TimerList` methods can only be called from asynchronous context, + for example via :meth:`mitogen.core.Broker.defer`. + + The broker automatically adjusts its sleep delay according to the installed + timer list, and arranges for timers to expire via automatic calls to + :meth:`expire`. The main user interface to :class:`TimerList` is + :meth:`schedule`. + """ + _now = mitogen.core.now + + def __init__(self): + self._lst = [] + + def get_timeout(self): + """ + Return the floating point seconds until the next event is due. + + :returns: + Floating point delay, or 0.0, or :data:`None` if no events are + scheduled. + """ + while self._lst and not self._lst[0].active: + heapq.heappop(self._lst) + if self._lst: + return max(0, self._lst[0].when - self._now()) + + def schedule(self, when, func): + """ + Schedule a future event. + + :param float when: + UNIX time in seconds when event should occur. + :param callable func: + Callable to invoke on expiry. + :returns: + A :class:`Timer` instance, exposing :meth:`Timer.cancel`, which may + be used to cancel the future invocation. + """ + timer = Timer(when, func) + heapq.heappush(self._lst, timer) + return timer + + def expire(self): + """ + Invoke callbacks for any events in the past. + """ + now = self._now() + while self._lst and self._lst[0].when <= now: + timer = heapq.heappop(self._lst) + if timer.active: + timer.active = False + timer.func() + + +class PartialZlib(object): + """ + Because the mitogen.core source has a line appended to it during bootstrap, + it must be recompressed for each connection. This is not a problem for a + small number of connections, but it amounts to 30 seconds CPU time by the + time 500 targets are in use. + + For that reason, build a compressor containing mitogen.core and flush as + much of it as possible into an initial buffer. Then to append the custom + line, clone the compressor and compress just that line. + + A full compression costs ~6ms on a modern machine, this method costs ~35 + usec. + """ + def __init__(self, s): + self.s = s + if sys.version_info > (2, 5): + self._compressor = zlib.compressobj(9) + self._out = self._compressor.compress(s) + self._out += self._compressor.flush(zlib.Z_SYNC_FLUSH) + else: + self._compressor = None + + def append(self, s): + """ + Append the bytestring `s` to the compressor state and return the + final compressed output. + """ + if self._compressor is None: + return zlib.compress(self.s + s, 9) + else: + compressor = self._compressor.copy() + out = self._out + out += compressor.compress(s) + return out + compressor.flush() + + +def _upgrade_broker(broker): + """ + Extract the poller state from Broker and replace it with the industrial + strength poller for this OS. Must run on the Broker thread. + """ + # This function is deadly! The act of calling start_receive() generates log + # messages which must be silenced as the upgrade progresses, otherwise the + # poller state will change as it is copied, resulting in write fds that are + # lost. (Due to LogHandler->Router->Stream->Protocol->Broker->Poller, where + # Stream only calls start_transmit() when transitioning from empty to + # non-empty buffer. If the start_transmit() is lost, writes from the child + # hang permanently). + root = logging.getLogger() + old_level = root.level + root.setLevel(logging.CRITICAL) + try: + old = broker.poller + new = PREFERRED_POLLER() + for fd, data in old.readers: + new.start_receive(fd, data) + for fd, data in old.writers: + new.start_transmit(fd, data) + + old.close() + broker.poller = new + finally: + root.setLevel(old_level) + + broker.timers = TimerList() + LOG.debug('upgraded %r with %r (new: %d readers, %d writers; ' + 'old: %d readers, %d writers)', old, new, + len(new._rfds), len(new._wfds), len(old._rfds), len(old._wfds)) + + +@mitogen.core.takes_econtext +def upgrade_router(econtext): + if not isinstance(econtext.router, Router): # TODO + econtext.broker.defer(_upgrade_broker, econtext.broker) + econtext.router.__class__ = Router # TODO + econtext.router.upgrade( + importer=econtext.importer, + parent=econtext.parent, + ) + + +def get_connection_class(name): + """ + Given the name of a Mitogen connection method, import its implementation + module and return its Stream subclass. + """ + if name == u'local': + name = u'parent' + module = mitogen.core.import_module(u'mitogen.' + name) + return module.Connection + + +@mitogen.core.takes_econtext +def _proxy_connect(name, method_name, kwargs, econtext): + """ + Implements the target portion of Router._proxy_connect() by upgrading the + local process to a parent if it was not already, then calling back into + Router._connect() using the arguments passed to the parent's + Router.connect(). + + :returns: + Dict containing: + * ``id``: :data:`None`, or integer new context ID. + * ``name``: :data:`None`, or string name attribute of new Context. + * ``msg``: :data:`None`, or StreamError exception text. + """ + upgrade_router(econtext) + + try: + context = econtext.router._connect( + klass=get_connection_class(method_name), + name=name, + **kwargs + ) + except mitogen.core.StreamError: + return { + u'id': None, + u'name': None, + u'msg': 'error occurred on host %s: %s' % ( + socket.gethostname(), + sys.exc_info()[1], + ), + } + + return { + u'id': context.context_id, + u'name': context.name, + u'msg': None, + } + + +def returncode_to_str(n): + """ + Parse and format a :func:`os.waitpid` exit status. + """ + if n < 0: + return 'exited due to signal %d (%s)' % (-n, SIGNAL_BY_NUM.get(-n)) + return 'exited with return code %d' % (n,) + + +class EofError(mitogen.core.StreamError): + """ + Raised by :class:`Connection` when an empty read is detected from the + remote process before bootstrap completes. + """ + # inherits from StreamError to maintain compatibility. + pass + + +class CancelledError(mitogen.core.StreamError): + """ + Raised by :class:`Connection` when :meth:`mitogen.core.Broker.shutdown` is + called before bootstrap completes. + """ + pass + + +class Argv(object): + """ + Wrapper to defer argv formatting when debug logging is disabled. + """ + def __init__(self, argv): + self.argv = argv + + must_escape = frozenset('\\$"`!') + must_escape_or_space = must_escape | frozenset(' ') + + def escape(self, x): + if not self.must_escape_or_space.intersection(x): + return x + + s = '"' + for c in x: + if c in self.must_escape: + s += '\\' + s += c + s += '"' + return s + + def __str__(self): + return ' '.join(map(self.escape, self.argv)) + + +class CallSpec(object): + """ + Wrapper to defer call argument formatting when debug logging is disabled. + """ + def __init__(self, func, args, kwargs): + self.func = func + self.args = args + self.kwargs = kwargs + + def _get_name(self): + bits = [self.func.__module__] + if inspect.ismethod(self.func): + im_self = getattr(self.func, IM_SELF_ATTR) + bits.append(getattr(im_self, '__name__', None) or + getattr(type(im_self), '__name__', None)) + bits.append(self.func.__name__) + return u'.'.join(bits) + + def _get_args(self): + return u', '.join(repr(a) for a in self.args) + + def _get_kwargs(self): + s = u'' + if self.kwargs: + s = u', '.join('%s=%r' % (k, v) for k, v in self.kwargs.items()) + if self.args: + s = u', ' + s + return s + + def __repr__(self): + return '%s(%s%s)' % ( + self._get_name(), + self._get_args(), + self._get_kwargs(), + ) + + +class PollPoller(mitogen.core.Poller): + """ + Poller based on the POSIX :linux:man2:`poll` interface. Not available on + some Python/OS X combinations. Otherwise the preferred poller for small + FD counts; or if many pollers are created, used once, then closed. + There there is no setup/teardown/configuration system call overhead. + """ + SUPPORTED = hasattr(select, 'poll') + _readmask = SUPPORTED and select.POLLIN | select.POLLHUP + + def __init__(self): + super(PollPoller, self).__init__() + self._pollobj = select.poll() + + # TODO: no proof we dont need writemask too + def _update(self, fd): + mask = (((fd in self._rfds) and self._readmask) | + ((fd in self._wfds) and select.POLLOUT)) + if mask: + self._pollobj.register(fd, mask) + else: + try: + self._pollobj.unregister(fd) + except KeyError: + pass + + def _poll(self, timeout): + if timeout: + timeout *= 1000 + + events, _ = mitogen.core.io_op(self._pollobj.poll, timeout) + for fd, event in events: + if event & self._readmask: + IOLOG.debug('%r: POLLIN|POLLHUP for %r', self, fd) + data, gen = self._rfds.get(fd, (None, None)) + if gen and gen < self._generation: + yield data + if event & select.POLLOUT: + IOLOG.debug('%r: POLLOUT for %r', self, fd) + data, gen = self._wfds.get(fd, (None, None)) + if gen and gen < self._generation: + yield data + + +class KqueuePoller(mitogen.core.Poller): + """ + Poller based on the FreeBSD/Darwin :freebsd:man2:`kqueue` interface. + """ + SUPPORTED = hasattr(select, 'kqueue') + + def __init__(self): + super(KqueuePoller, self).__init__() + self._kqueue = select.kqueue() + self._changelist = [] + + def close(self): + super(KqueuePoller, self).close() + self._kqueue.close() + + def _control(self, fd, filters, flags): + mitogen.core._vv and IOLOG.debug( + '%r._control(%r, %r, %r)', self, fd, filters, flags) + # TODO: at shutdown it is currently possible for KQ_EV_ADD/KQ_EV_DEL + # pairs to be pending after the associated file descriptor has already + # been closed. Fixing this requires maintaining extra state, or perhaps + # making fd closure the poller's responsibility. In the meantime, + # simply apply changes immediately. + # self._changelist.append(select.kevent(fd, filters, flags)) + changelist = [select.kevent(fd, filters, flags)] + events, _ = mitogen.core.io_op(self._kqueue.control, changelist, 0, 0) + assert not events + + def start_receive(self, fd, data=None): + mitogen.core._vv and IOLOG.debug('%r.start_receive(%r, %r)', + 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._generation) + + def stop_receive(self, fd): + mitogen.core._vv and IOLOG.debug('%r.stop_receive(%r)', self, fd) + if fd in self._rfds: + self._control(fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE) + del self._rfds[fd] + + def start_transmit(self, fd, data=None): + mitogen.core._vv and IOLOG.debug('%r.start_transmit(%r, %r)', + 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._generation) + + def stop_transmit(self, fd): + mitogen.core._vv and IOLOG.debug('%r.stop_transmit(%r)', self, fd) + if fd in self._wfds: + self._control(fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE) + del self._wfds[fd] + + def _poll(self, timeout): + changelist = self._changelist + self._changelist = [] + events, _ = mitogen.core.io_op(self._kqueue.control, + changelist, 32, timeout) + for event in events: + fd = event.ident + 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: + data, gen = self._rfds.get(fd, (None, None)) + # Events can still be read for an already-discarded 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: + 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): + """ + Poller based on the Linux :linux:man7:`epoll` interface. + """ + SUPPORTED = hasattr(select, 'epoll') + _inmask = SUPPORTED and select.EPOLLIN | select.EPOLLHUP + + def __init__(self): + super(EpollPoller, self).__init__() + self._epoll = select.epoll(32) + self._registered_fds = set() + + def close(self): + super(EpollPoller, self).close() + self._epoll.close() + + def _control(self, fd): + mitogen.core._vv and IOLOG.debug('%r._control(%r)', self, fd) + mask = (((fd in self._rfds) and select.EPOLLIN) | + ((fd in self._wfds) and select.EPOLLOUT)) + if mask: + if fd in self._registered_fds: + self._epoll.modify(fd, mask) + else: + self._epoll.register(fd, mask) + self._registered_fds.add(fd) + elif fd in self._registered_fds: + self._epoll.unregister(fd) + self._registered_fds.remove(fd) + + 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._generation) + self._control(fd) + + def stop_receive(self, fd): + mitogen.core._vv and IOLOG.debug('%r.stop_receive(%r)', self, fd) + self._rfds.pop(fd, None) + self._control(fd) + + 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._generation) + self._control(fd) + + def stop_transmit(self, fd): + mitogen.core._vv and IOLOG.debug('%r.stop_transmit(%r)', self, fd) + self._wfds.pop(fd, None) + self._control(fd) + + 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: + 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 + + +POLLERS = (EpollPoller, KqueuePoller, PollPoller, mitogen.core.Poller) +PREFERRED_POLLER = next(cls for cls in POLLERS if cls.SUPPORTED) + + +# For processes that start many threads or connections, it's possible Latch +# will also get high-numbered FDs, and so select() becomes useless there too. +POLLER_LIGHTWEIGHT = PollPoller.SUPPORTED and PollPoller or PREFERRED_POLLER +mitogen.core.Latch.poller_class = POLLER_LIGHTWEIGHT + + +class LineLoggingProtocolMixin(object): + def __init__(self, **kwargs): + super(LineLoggingProtocolMixin, self).__init__(**kwargs) + self.logged_lines = [] + self.logged_partial = None + + def on_line_received(self, line): + self.logged_partial = None + self.logged_lines.append((mitogen.core.now(), line)) + self.logged_lines[:] = self.logged_lines[-100:] + return super(LineLoggingProtocolMixin, self).on_line_received(line) + + def on_partial_line_received(self, line): + self.logged_partial = line + return super(LineLoggingProtocolMixin, self).on_partial_line_received(line) + + def on_disconnect(self, broker): + if self.logged_partial: + self.logged_lines.append((mitogen.core.now(), self.logged_partial)) + self.logged_partial = None + super(LineLoggingProtocolMixin, self).on_disconnect(broker) + + +def get_history(streams): + history = [] + for stream in streams: + if stream: + history.extend(getattr(stream.protocol, 'logged_lines', [])) + history.sort() + + s = b('\n').join(h[1] for h in history) + return mitogen.core.to_text(s) + + +class RegexProtocol(LineLoggingProtocolMixin, mitogen.core.DelimitedProtocol): + """ + Implement a delimited protocol where messages matching a set of regular + expressions are dispatched to individual handler methods. Input is + dispatches using :attr:`PATTERNS` and :attr:`PARTIAL_PATTERNS`, before + falling back to :meth:`on_unrecognized_line_received` and + :meth:`on_unrecognized_partial_line_received`. + """ + #: A sequence of 2-tuples of the form `(compiled pattern, method)` for + #: patterns that should be matched against complete (delimited) messages, + #: i.e. full lines. + PATTERNS = [] + + #: Like :attr:`PATTERNS`, but patterns that are matched against incomplete + #: lines. + PARTIAL_PATTERNS = [] + + def on_line_received(self, line): + super(RegexProtocol, self).on_line_received(line) + for pattern, func in self.PATTERNS: + match = pattern.search(line) + if match is not None: + return func(self, line, match) + + return self.on_unrecognized_line_received(line) + + def on_unrecognized_line_received(self, line): + LOG.debug('%s: (unrecognized): %s', + self.stream.name, line.decode('utf-8', 'replace')) + + def on_partial_line_received(self, line): + super(RegexProtocol, self).on_partial_line_received(line) + LOG.debug('%s: (partial): %s', + self.stream.name, line.decode('utf-8', 'replace')) + for pattern, func in self.PARTIAL_PATTERNS: + match = pattern.search(line) + if match is not None: + return func(self, line, match) + + return self.on_unrecognized_partial_line_received(line) + + def on_unrecognized_partial_line_received(self, line): + LOG.debug('%s: (unrecognized partial): %s', + self.stream.name, line.decode('utf-8', 'replace')) + + +class BootstrapProtocol(RegexProtocol): + """ + Respond to stdout of a child during bootstrap. Wait for :attr:`EC0_MARKER` + to be written by the first stage to indicate it can receive the bootstrap, + then await :attr:`EC1_MARKER` to indicate success, and + :class:`MitogenProtocol` can be enabled. + """ + #: Sentinel value emitted by the first stage to indicate it is ready to + #: receive the compressed bootstrap. For :mod:`mitogen.ssh` this must have + #: length of at least `max(len('password'), len('debug1:'))` + EC0_MARKER = b('MITO000') + EC1_MARKER = b('MITO001') + EC2_MARKER = b('MITO002') + + def __init__(self, broker): + super(BootstrapProtocol, self).__init__() + self._writer = mitogen.core.BufferedWriter(broker, self) + + def on_transmit(self, broker): + self._writer.on_transmit(broker) + + def _on_ec0_received(self, line, match): + LOG.debug('%r: first stage started succcessfully', self) + self._writer.write(self.stream.conn.get_preamble()) + + def _on_ec1_received(self, line, match): + LOG.debug('%r: first stage received mitogen.core source', self) + + def _on_ec2_received(self, line, match): + LOG.debug('%r: new child booted successfully', self) + self.stream.conn._complete_connection() + return False + + def on_unrecognized_line_received(self, line): + LOG.debug('%s: stdout: %s', self.stream.name, + line.decode('utf-8', 'replace')) + + PATTERNS = [ + (re.compile(EC0_MARKER), _on_ec0_received), + (re.compile(EC1_MARKER), _on_ec1_received), + (re.compile(EC2_MARKER), _on_ec2_received), + ] + + +class LogProtocol(LineLoggingProtocolMixin, mitogen.core.DelimitedProtocol): + """ + For "hybrid TTY/socketpair" mode, after connection setup a spare TTY master + FD exists that cannot be closed, and to which SSH or sudo may continue + writing log messages. + + The descriptor cannot be closed since the UNIX TTY layer sends SIGHUP to + processes whose controlling TTY is the slave whose master side was closed. + LogProtocol takes over this FD and creates log messages for anything + written to it. + """ + def on_line_received(self, line): + """ + Read a line, decode it as UTF-8, and log it. + """ + super(LogProtocol, self).on_line_received(line) + LOG.info(u'%s: %s', self.stream.name, line.decode('utf-8', 'replace')) + + +class MitogenProtocol(mitogen.core.MitogenProtocol): + """ + Extend core.MitogenProtocol to cause SHUTDOWN to be sent to the child + during graceful shutdown. + """ + def on_shutdown(self, broker): + """ + Respond to the broker's request for the stream to shut down by sending + SHUTDOWN to the child. + """ + LOG.debug('%r: requesting child shutdown', self) + self._send( + mitogen.core.Message( + src_id=mitogen.context_id, + dst_id=self.remote_id, + handle=mitogen.core.SHUTDOWN, + ) + ) + + +class Options(object): + name = None + + #: The path to the remote Python interpreter. + python_path = get_sys_executable() + + #: Maximum time to wait for a connection attempt. + connect_timeout = 30.0 + + #: True to cause context to write verbose /tmp/mitogen..log. + debug = False + + #: True to cause context to write /tmp/mitogen.stats...log. + profiling = False + + #: True if unidirectional routing is enabled in the new child. + unidirectional = False + + #: Passed via Router wrapper methods, must eventually be passed to + #: ExternalContext.main(). + max_message_size = None + + #: Remote name. + remote_name = None + + #: Derived from :py:attr:`connect_timeout`; absolute floating point + #: UNIX timestamp after which the connection attempt should be abandoned. + connect_deadline = None + + def __init__(self, max_message_size, name=None, remote_name=None, + python_path=None, debug=False, connect_timeout=None, + profiling=False, unidirectional=False, old_router=None): + self.name = name + self.max_message_size = max_message_size + if python_path: + self.python_path = python_path + if connect_timeout: + self.connect_timeout = connect_timeout + if remote_name is None: + remote_name = get_default_remote_name() + if '/' in remote_name or '\\' in remote_name: + raise ValueError('remote_name= cannot contain slashes') + if remote_name: + self.remote_name = mitogen.core.to_text(remote_name) + self.debug = debug + self.profiling = profiling + self.unidirectional = unidirectional + self.max_message_size = max_message_size + self.connect_deadline = mitogen.core.now() + self.connect_timeout + + +class Connection(object): + """ + Manage the lifetime of a set of :class:`Streams ` connecting to a + remote Python interpreter, including bootstrap, disconnection, and external + tool integration. + + Base for streams capable of starting children. + """ + options_class = Options + + #: The protocol attached to stdio of the child. + stream_protocol_class = BootstrapProtocol + + #: The protocol attached to stderr of the child. + diag_protocol_class = LogProtocol + + #: :class:`Process` + proc = None + + #: :class:`mitogen.core.Stream` with sides connected to stdin/stdout. + stdio_stream = None + + #: If `proc.stderr` is set, referencing either a plain pipe or the + #: controlling TTY, this references the corresponding + #: :class:`LogProtocol`'s stream, allowing it to be disconnected when this + #: stream is disconnected. + stderr_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' + + #: :class:`Timer` that runs :meth:`_on_timer_expired` when connection + #: timeout occurs. + _timer = None + + #: When disconnection completes, instance of :class:`Reaper` used to wait + #: on the exit status of the subprocess. + _reaper = None + + #: On failure, the exception object that should be propagated back to the + #: user. + exception = None + + #: Extra text appended to :class:`EofError` if that exception is raised on + #: a failed connection attempt. May be used in subclasses to hint at common + #: problems with a particular connection method. + eof_error_hint = None + + def __init__(self, options, router): + #: :class:`Options` + self.options = options + self._router = router + + def __repr__(self): + return 'Connection(%r)' % (self.stdio_stream,) + + # 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 + # with a custom argv. + # * Optimized for minimum byte count after minification & compression. + # The script preamble_size.py measures this. + # + # macOS tweaks for Python 2.7 must be kept in sync with the the Ansible + # module test_echo_module, used by the integration tests. + # * macOS <= 10.14 (Darwin <= 18) install an unreliable Python version + # switcher as /usr/bin/python, which introspects argv0. To workaround + # it we redirect attempts to call /usr/bin/python with an explicit + # call to /usr/bin/python2.7. macOS 10.15 (Darwin 19) removed it. + # * macOS 11.x (Darwin 20, Big Sur) and macOS 12.x (Darwin 21, Montery) + # do something slightly different. The Python executable is patched to + # perform an extra execvp(). I don't fully understand the details, but + # setting PYTHON_LAUNCHED_FROM_WRAPPER=1 avoids it. + # * macOS 12.3+ (Darwin 21.4+, Monterey) doesn't ship Python. + # https://developer.apple.com/documentation/macos-release-notes/macos-12_3-release-notes#Python + # + # Locals: + # R: read side of interpreter stdin. + # W: write side of interpreter stdin. + # r: read side of core_src FD. + # w: write side of core_src FD. + + # Final os.close(STDERR_FILENO) to avoid --py-debug build corrupting stream with + # "[1234 refs]" during exit. + @staticmethod + def _first_stage(): + R,W=os.pipe() + r,w=os.pipe() + if os.fork(): + os.dup2(0,100) + os.dup2(R,0) + os.dup2(r,101) + os.close(R) + os.close(r) + os.close(W) + os.close(w) + if os.uname()[0]+os.uname()[2][:2]+sys.executable=='Darwin19/usr/bin/python':sys.executable+='2.7' + if os.uname()[0]+os.uname()[2][:2]+sys.version[:3]=='Darwin202.7':os.environ['PYTHON_LAUNCHED_FROM_WRAPPER']='1' + if os.uname()[0]+os.uname()[2][:2]+sys.version[:3]=='Darwin212.7':os.environ['PYTHON_LAUNCHED_FROM_WRAPPER']='1' + os.environ['ARGV0']=sys.executable + os.execl(sys.executable,sys.executable+'(mitogen:%s)'%sys.argv[2]) + os.write(1,'MITO000\n'.encode()) + # Size of the compressed core source to be read + n=int(sys.argv[3]) + # Read `len(compressed preamble)` bytes sent by our Mitogen parent. + # `select()` handles non-blocking stdin (e.g. sudo + log_output). + # `C` accumulates compressed bytes. + C=''.encode() + # data chunk + V='V' + # Stop looping if no more data is needed or EOF is detected (empty bytes). + while n-len(C) and V:select.select([0],[],[]);V=os.read(0,n-len(C));C+=V + # Raises `zlib.error` if compressed preamble is truncated or invalid + C=zlib.decompress(C) + f=os.fdopen(W,'wb',0) + f.write(C) + f.close() + f=os.fdopen(w,'wb',0) + f.write(C) + f.close() + os.write(1,'MITO001\n'.encode()) + os.close(2) + + def get_python_argv(self): + """ + Return the initial argument vector elements necessary to invoke Python, + by returning a 1-element list containing :attr:`python_path` if it is a + string, or simply returning it if it is already a list. + + This allows emulation of existing tools where the Python invocation may + be set to e.g. `['/usr/bin/env', 'python']`. + """ + if isinstance(self.options.python_path, list): + return self.options.python_path + return [self.options.python_path] + + def get_boot_command(self): + lines = inspect.getsourcelines(self._first_stage)[0][2:] + # Remove line comments, leading indentation, trailing newline + source = textwrap.dedent(''.join(s for s in lines if '#' not in s))[:-1] + source = source.replace(' ', ' ') + compressor = zlib.compressobj( + zlib.Z_BEST_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS, + ) + compressed = compressor.compress(source.encode()) + compressor.flush() + encoded = binascii.b2a_base64(compressed).replace(b('\n'), b('')) + + # Just enough to decode, decompress, and exec the first stage. + # Priorities: wider compatibility, faster startup, shorter length. + # `sys.path=...` for https://github.com/python/cpython/issues/115911. + # `import os,select` here (not stage 1) to save a few bytes overall. + return self.get_python_argv() + [ + '-c', + 'import sys;sys.path=[p for p in sys.path if p];' + 'import binascii,os,select,zlib;' + 'exec(zlib.decompress(binascii.a2b_base64(sys.argv[1]),-15))', + encoded.decode(), + self.options.remote_name, + str(len(self.get_preamble())), + ] + + def get_econtext_config(self): + assert self.options.max_message_size is not None + parent_ids = mitogen.parent_ids[:] + parent_ids.insert(0, mitogen.context_id) + if mitogen.is_master: import_policy = self._router.responder.policy + else: import_policy = self._router.importer.policy + return { + 'parent_ids': parent_ids, + 'context_id': self.context.context_id, + 'debug': self.options.debug, + 'import_blocks': list(import_policy.blocks), + 'import_overrides': list(import_policy.overrides), + 'profiling': self.options.profiling, + 'unidirectional': self.options.unidirectional, + 'log_level': get_log_level(), + 'max_message_size': self.options.max_message_size, + 'version': mitogen.__version__, + } + + def get_preamble(self): + suffix = ( + '\nExternalContext(%r).main()\n' % + (self.get_econtext_config(),) + ) + partial = get_core_source_partial() + return partial.append(suffix.encode('utf-8')) + + def _get_name(self): + """ + Called by :meth:`connect` after :attr:`pid` is known. Subclasses can + override it to specify a default stream name, or set + :attr:`name_prefix` to generate a default format. + """ + return u'%s.%s' % (self.name_prefix, self.proc.pid) + + def start_child(self): + args = self.get_boot_command() + LOG.debug('command line for %r: %s', self, Argv(args)) + try: + return self.create_child(args=args, **self.create_child_args) + except OSError: + e = sys.exc_info()[1] + msg = 'Child start failed: %s. Command was: %s' % (e, Argv(args)) + raise mitogen.core.StreamError(msg) + + def _adorn_eof_error(self, e): + """ + Subclasses may 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 _complete_connection(self): + self._timer.cancel() + if not self.exception: + mitogen.core.unlisten(self._router.broker, 'shutdown', + self._on_broker_shutdown) + self._router.register(self.context, self.stdio_stream) + self.stdio_stream.set_protocol( + MitogenProtocol( + router=self._router, + remote_id=self.context.context_id, + ) + ) + self._router.route_monitor.notice_stream(self.stdio_stream) + self.latch.put() + + def _fail_connection(self, exc): + """ + Fail the connection attempt. + """ + LOG.debug('failing connection %s due to %r', + self.stdio_stream and self.stdio_stream.name, exc) + if self.exception is None: + self._adorn_eof_error(exc) + self.exception = exc + mitogen.core.unlisten(self._router.broker, 'shutdown', + self._on_broker_shutdown) + for stream in self.stdio_stream, self.stderr_stream: + if stream and not stream.receive_side.closed: + stream.on_disconnect(self._router.broker) + self._complete_connection() + + eof_error_msg = 'EOF on stream; last 100 lines received:\n' + + def on_stdio_disconnect(self): + """ + Handle stdio stream disconnection by failing the Connection if the + stderr stream has already been closed. Otherwise, wait for it to close + (or timeout), to allow buffered diagnostic logs to be consumed. + + It is normal that when a subprocess aborts, stdio has nothing buffered + when it is closed, thus signalling readability, causing an empty read + (interpreted as indicating disconnection) on the next loop iteration, + even if its stderr pipe has lots of diagnostic logs still buffered in + the kernel. Therefore we must wait for both pipes to indicate they are + empty before triggering connection failure. + """ + stderr = self.stderr_stream + if stderr is None or stderr.receive_side.closed: + self._on_streams_disconnected() + + def on_stderr_disconnect(self): + """ + Inverse of :func:`on_stdio_disconnect`. + """ + if self.stdio_stream.receive_side.closed: + self._on_streams_disconnected() + + def _on_streams_disconnected(self): + """ + When disconnection has been detected for both streams, cancel the + connection timer, mark the connection failed, and reap the child + process. Do nothing if the timer has already been cancelled, indicating + some existing failure has already been noticed. + """ + if self._timer.active: + self._timer.cancel() + self._fail_connection(EofError( + self.eof_error_msg + get_history( + [self.stdio_stream, self.stderr_stream] + ) + )) + + if self._reaper: + return + + self._reaper = Reaper( + broker=self._router.broker, + proc=self.proc, + kill=not ( + (self.detached and self.child_is_immediate_subprocess) or + # Avoid killing so child has chance to write cProfile data + self._router.profiling + ), + # Don't delay shutdown waiting for a detached child, since the + # detached child may expect to live indefinitely after its parent + # exited. + wait_on_shutdown=(not self.detached), + ) + self._reaper.reap() + + def _on_broker_shutdown(self): + """ + Respond to broker.shutdown() being called by failing the connection + attempt. + """ + self._fail_connection(CancelledError(BROKER_SHUTDOWN_MSG)) + + def stream_factory(self): + return self.stream_protocol_class.build_stream( + broker=self._router.broker, + ) + + def stderr_stream_factory(self): + return self.diag_protocol_class.build_stream() + + def _setup_stdio_stream(self): + stream = self.stream_factory() + stream.conn = self + stream.name = self.options.name or self._get_name() + for fp in self.proc.stdout, self.proc.stdin: + fd = fp.fileno() + mitogen.core.set_blocking(fd, False) + stream.accept(self.proc.stdout, self.proc.stdin) + + mitogen.core.listen(stream, 'disconnect', self.on_stdio_disconnect) + self._router.broker.start_receive(stream) + return stream + + def _setup_stderr_stream(self): + stream = self.stderr_stream_factory() + stream.conn = self + stream.name = self.options.name or self._get_name() + fd = self.proc.stderr.fileno() + mitogen.core.set_blocking(fd, False) + stream.accept(self.proc.stderr, self.proc.stderr) + + mitogen.core.listen(stream, 'disconnect', self.on_stderr_disconnect) + self._router.broker.start_receive(stream) + return stream + + def _on_timer_expired(self): + self._fail_connection( + mitogen.core.TimeoutError( + 'Failed to setup connection after %.2f seconds', + self.options.connect_timeout, + ) + ) + + def _async_connect(self): + LOG.debug('creating connection to context %d using %s', + self.context.context_id, self.__class__.__module__) + mitogen.core.listen(self._router.broker, 'shutdown', + self._on_broker_shutdown) + self._timer = self._router.broker.timers.schedule( + when=self.options.connect_deadline, + func=self._on_timer_expired, + ) + + try: + self.proc = self.start_child() + except Exception: + LOG.debug('failed to start child', exc_info=True) + self._fail_connection(sys.exc_info()[1]) + return + + LOG.debug('child for %r started: pid:%r stdin:%r stdout:%r stderr:%r', + self, self.proc.pid, + self.proc.stdin, self.proc.stdout, self.proc.stderr) + + self.stdio_stream = self._setup_stdio_stream() + if self.context.name is None: + self.context.name = self.stdio_stream.name + self.proc.name = self.stdio_stream.name + if self.proc.stderr: + self.stderr_stream = self._setup_stderr_stream() + + def connect(self, context): + self.context = context + self.latch = mitogen.core.Latch() + self._router.broker.defer(self._async_connect) + self.latch.get() + if self.exception: + raise self.exception + + +class ChildIdAllocator(object): + """ + Allocate new context IDs from a block of unique context IDs allocated by + the master process. + """ + def __init__(self, router): + self.router = router + self.lock = threading.Lock() + self.it = iter(mitogen.core.range(0)) + + def allocate(self): + """ + Allocate an ID, requesting a fresh block from the master if the + existing block is exhausted. + + :returns: + The new context ID. + + .. warning:: + + This method is not safe to call from the :class:`Broker` thread, as + it may block on IO of its own. + """ + self.lock.acquire() + try: + for id_ in self.it: + return id_ + + master = self.router.context_by_id(0) + start, end = master.send_await( + mitogen.core.Message(dst_id=0, handle=mitogen.core.ALLOCATE_ID) + ) + self.it = iter(mitogen.core.range(start, end)) + finally: + self.lock.release() + + return self.allocate() + + +class CallChain(object): + """ + Deliver :data:`mitogen.core.CALL_FUNCTION` messages to a target context, + optionally threading related calls so an exception in an earlier call + cancels subsequent calls. + + :param mitogen.core.Context context: + Target context. + :param bool pipelined: + Enable pipelining. + + :meth:`call`, :meth:`call_no_reply` and :meth:`call_async` + normally issue calls and produce responses with no memory of prior + exceptions. If a call made with :meth:`call_no_reply` fails, the exception + is logged to the target context's logging framework. + + **Pipelining** + + When pipelining is enabled, if an exception occurs during a call, + subsequent calls made by the same :class:`CallChain` fail with the same + exception, including those already in-flight on the network, and no further + calls execute until :meth:`reset` is invoked. + + No exception is logged for calls made with :meth:`call_no_reply`, instead + the exception is saved and reported as the result of subsequent + :meth:`call` or :meth:`call_async` calls. + + Sequences of asynchronous calls can be made without wasting network + round-trips to discover if prior calls succeed, and chains originating from + multiple unrelated source contexts may overlap concurrently at a target + context without interference. + + In this example, 4 calls complete in one round-trip:: + + chain = mitogen.parent.CallChain(context, pipelined=True) + chain.call_no_reply(os.mkdir, '/tmp/foo') + + # If previous mkdir() failed, this never runs: + chain.call_no_reply(os.mkdir, '/tmp/foo/bar') + + # If either mkdir() failed, this never runs, and the exception is + # asynchronously delivered to the receiver. + recv = chain.call_async(subprocess.check_output, '/tmp/foo') + + # If anything so far failed, this never runs, and raises the exception. + chain.call(do_something) + + # If this code was executed, the exception would also be raised. + if recv.get().unpickle() == 'baz': + pass + + When pipelining is enabled, :meth:`reset` must be invoked to ensure any + exception is discarded, otherwise unbounded memory usage is possible in + long-running programs. The context manager protocol is supported to ensure + :meth:`reset` is always invoked:: + + with mitogen.parent.CallChain(context, pipelined=True) as chain: + chain.call_no_reply(...) + chain.call_no_reply(...) + chain.call_no_reply(...) + chain.call(...) + + # chain.reset() automatically invoked. + """ + def __init__(self, context, pipelined=False): + self.context = context + if pipelined: + self.chain_id = self.make_chain_id() + else: + self.chain_id = None + + @classmethod + def make_chain_id(cls): + return '%s-%s-%x-%x' % ( + socket.gethostname(), + os.getpid(), + thread.get_ident(), + int(1e6 * mitogen.core.now()), + ) + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, self.context) + + def __enter__(self): + return self + + def __exit__(self, _1, _2, _3): + self.reset() + + def reset(self): + """ + Instruct the target to forget any related exception. + """ + if not self.chain_id: + return + + saved, self.chain_id = self.chain_id, None + try: + self.call_no_reply(mitogen.core.Dispatcher.forget_chain, saved) + finally: + self.chain_id = saved + + closures_msg = ( + 'Mitogen cannot invoke closures, as doing so would require ' + 'serializing arbitrary program state, and no universal ' + 'method exists to recover a reference to them.' + ) + + lambda_msg = ( + 'Mitogen cannot invoke anonymous functions, as no universal method ' + 'exists to recover a reference to an anonymous function.' + ) + + method_msg = ( + 'Mitogen cannot invoke instance methods, as doing so would require ' + 'serializing arbitrary program state.' + ) + + def make_msg(self, fn, *args, **kwargs): + if getattr(fn, closure_attr, None) is not None: + raise TypeError(self.closures_msg) + if fn.__name__ == '': + raise TypeError(self.lambda_msg) + + if inspect.ismethod(fn): + im_self = getattr(fn, IM_SELF_ATTR) + if not inspect.isclass(im_self): + raise TypeError(self.method_msg) + klass = mitogen.core.to_text(im_self.__name__) + else: + klass = None + + tup = ( + self.chain_id, + mitogen.core.to_text(fn.__module__), + klass, + mitogen.core.to_text(fn.__name__), + args, + mitogen.core.Kwargs(kwargs) + ) + return mitogen.core.Message.pickled(tup, + handle=mitogen.core.CALL_FUNCTION) + + def call_no_reply(self, fn, *args, **kwargs): + """ + Like :meth:`call_async`, but do not wait for a return value, and inform + the target context no reply is expected. If the call fails and + pipelining is disabled, the exception will be logged to the target + context's logging framework. + """ + LOG.debug('starting no-reply function call to %r: %r', + self.context.name or self.context.context_id, + CallSpec(fn, args, kwargs)) + self.context.send(self.make_msg(fn, *args, **kwargs)) + + def call_async(self, fn, *args, **kwargs): + """ + Arrange for `fn(*args, **kwargs)` to be invoked on the context's main + thread. + + :param fn: + A free function in module scope or a class method of a class + directly reachable from module scope: + + .. code-block:: python + + # mymodule.py + + def my_func(): + '''A free function reachable as mymodule.my_func''' + + class MyClass: + @classmethod + def my_classmethod(cls): + '''Reachable as mymodule.MyClass.my_classmethod''' + + def my_instancemethod(self): + '''Unreachable: requires a class instance!''' + + class MyEmbeddedClass: + @classmethod + def my_classmethod(cls): + '''Not directly reachable from module scope!''' + + :param tuple args: + Function arguments, if any. See :ref:`serialization-rules` for + permitted types. + :param dict kwargs: + Function keyword arguments, if any. See :ref:`serialization-rules` + for permitted types. + :returns: + :class:`mitogen.core.Receiver` configured to receive the result of + the invocation: + + .. code-block:: python + + recv = context.call_async(os.check_output, 'ls /tmp/') + try: + # Prints output once it is received. + msg = recv.get() + print(msg.unpickle()) + except mitogen.core.CallError, e: + print('Call failed:', str(e)) + + Asynchronous calls may be dispatched in parallel to multiple + contexts and consumed as they complete using + :class:`mitogen.select.Select`. + """ + LOG.debug('starting function call to %s: %r', + self.context.name or self.context.context_id, + CallSpec(fn, args, kwargs)) + return self.context.send_async(self.make_msg(fn, *args, **kwargs)) + + def call(self, fn, *args, **kwargs): + """ + Like :meth:`call_async`, but block until the return value is available. + Equivalent to:: + + call_async(fn, *args, **kwargs).get().unpickle() + + :returns: + The function's return value. + :raises mitogen.core.CallError: + An exception was raised in the remote context during execution. + """ + receiver = self.call_async(fn, *args, **kwargs) + return receiver.get().unpickle(throw_dead=False) + + +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): + super(Context, self).__init__(*args, **kwargs) + self.default_call_chain = self.call_chain_class(self) + + def __ne__(self, other): + return not (self == other) + + def __eq__(self, other): + return ( + isinstance(other, mitogen.core.Context) and + (other.context_id == self.context_id) and + (other.router == self.router) + ) + + def __hash__(self): + 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)) + self.send( + mitogen.core.Message( + handle=mitogen.core.SHUTDOWN, + ) + ) + + if wait: + latch.get() + else: + return latch + + +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. + + By virtue of only being active while responding to messages from a handler, + RouteMonitor lives entirely on the broker thread, so its data requires no + locking. + + :param mitogen.master.Router router: + Router to install handlers on. + :param mitogen.core.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 + self._log = logging.getLogger('mitogen.route_monitor') + #: Mapping of Stream instance to integer context IDs reachable via the + #: stream; used to cleanup routes during disconnection. + self._routes_by_stream = {} + self.router.add_handler( + fn=self._on_add_route, + handle=mitogen.core.ADD_ROUTE, + persist=True, + policy=is_immediate_child, + overwrite=True, + ) + self.router.add_handler( + fn=self._on_del_route, + handle=mitogen.core.DEL_ROUTE, + persist=True, + policy=is_immediate_child, + overwrite=True, + ) + + def __repr__(self): + return 'RouteMonitor()' + + 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`. + """ + if not stream: + # We may not have a stream during shutdown. + return + + data = str(target_id) + if name: + data = '%s:%s' % (target_id, name) + stream.protocol.send( + mitogen.core.Message( + handle=handle, + data=data.encode('utf-8'), + dst_id=stream.protocol.remote_id, + ) + ) + + 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 _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 self.router.get_streams(): + if target_id in stream.protocol.egress_ids and ( + (self.parent is None) or + (self.parent.context_id != stream.protocol.remote_id) + ): + 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 + :data:`mitogen.core.DEL_ROUTE` upstream when that child disconnects. + """ + self._routes_by_stream[stream] = set([stream.protocol.remote_id]) + self._propagate_up(mitogen.core.ADD_ROUTE, stream.protocol.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 propagating DEL_ROUTE for + any contexts we know were attached to it. + """ + # During a stream crash it is possible for disconnect signal to fire + # twice, in which case ignore the second instance. + routes = self._routes_by_stream.pop(stream, None) + if routes is None: + return + + self._log.debug('stream %s is gone; propagating DEL_ROUTE for %r', + stream.name, routes) + for target_id in routes: + self.router.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 + + target_id_s, _, target_name = bytes_partition(msg.data, b(':')) + target_name = target_name.decode() + target_id = int(target_id_s) + self.router.context_by_id(target_id).name = target_name + stream = self.router.stream_by_id(msg.src_id) + current = self.router.stream_by_id(target_id) + if current and current.protocol.remote_id != mitogen.parent_id: + self._log.error('Cannot add duplicate route to %r via %r, ' + 'already have existing route via %r', + target_id, stream, current) + return + + self._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_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 + + 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.src_id) + if registered_stream != stream: + self._log.error('received DEL_ROUTE for %d from %r, expected %r', + target_id, stream, registered_stream) + return + + context = self.router.context_by_id(target_id, create=False) + if context: + self._log.debug('firing local disconnect signal for %r', context) + mitogen.core.fire(context, 'disconnect') + + self._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.protocol.remote_id != mitogen.parent_id: + self._propagate_up(mitogen.core.DEL_ROUTE, target_id) + self._propagate_down(mitogen.core.DEL_ROUTE, target_id) + + +class Router(mitogen.core.Router): + context_class = Context + debug = False + profiling = False + + id_allocator = None + responder = None + log_forwarder = None + route_monitor = None + + def upgrade(self, importer, parent): + LOG.debug('upgrading %r with capabilities to start new children', self) + self.id_allocator = ChildIdAllocator(router=self) + self.responder = ModuleForwarder( + router=self, + parent_context=parent, + importer=importer, + ) + self.resource_responder = ResourceForwarder( + self, + parent, + importer._resource_requester, + ) + self.route_monitor = RouteMonitor(self, parent) + self.add_handler( + fn=self._on_detaching, + handle=mitogen.core.DETACHING, + persist=True, + ) + + def _on_detaching(self, msg): + if msg.is_dead: + return + stream = self.stream_by_id(msg.src_id) + if stream.protocol.remote_id != msg.src_id or stream.conn.detached: + LOG.warning('bad DETACHING received on %r: %r', stream, msg) + return + LOG.debug('%r: marking as detached', stream) + stream.conn.detached = True + msg.reply(None) + + def get_streams(self): + """ + Return an atomic snapshot of all streams in existence at time of call. + This is safe to call from any thread. + """ + self._write_lock.acquire() + try: + return itervalues(self._stream_by_id) + finally: + self._write_lock.release() + + def disconnect(self, context): + """ + Disconnect a context and forget its stream, assuming the context is + directly connected. + """ + stream = self.stream_by_id(context) + if stream is None or stream.protocol.remote_id != context.context_id: + return + + l = mitogen.core.Latch() + mitogen.core.listen(stream, 'disconnect', l.put) + def disconnect(): + LOG.debug('Starting disconnect of %r', stream) + stream.on_disconnect(self.broker) + self.broker.defer(disconnect) + l.get() + + def add_route(self, target_id, stream): + """ + Arrange for messages whose `dst_id` is `target_id` to be forwarded on a + directly connected :class:`Stream`. Safe to call from any thread. + + This is called automatically by :class:`RouteMonitor` 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. + + :param int target_id: + Target context ID to add a route for. + :param mitogen.core.Stream stream: + Stream over which messages to the target should be routed. + """ + LOG.debug('%r: adding route to context %r via %r', + self, target_id, stream) + assert isinstance(target_id, int) + assert isinstance(stream, mitogen.core.Stream) + + self._write_lock.acquire() + try: + self._stream_by_id[target_id] = stream + finally: + self._write_lock.release() + + def del_route(self, target_id): + """ + Delete any route that exists for `target_id`. It is not an error to + delete a route that does not currently exist. Safe to call from any + thread. + + This is called automatically by :class:`RouteMonitor` in response to + :data:`mitogen.core.DEL_ROUTE` messages, but remains public while the + design has not yet settled, and situations may arise where routing is + not fully automatic. + + :param int target_id: + Target context ID to delete route for. + """ + LOG.debug('%r: deleting route to %r', 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._write_lock.acquire() + try: + self._stream_by_id.pop(target_id, None) + finally: + self._write_lock.release() + + def allocate_id(self): + return self.id_allocator.allocate() + + connection_timeout_msg = u"Connection timed out." + + def _connect(self, klass, **kwargs): + context_id = self.allocate_id() + context = self.context_class(self, context_id) + context.name = kwargs.get('name') + + kwargs['old_router'] = self + kwargs['max_message_size'] = self.max_message_size + conn = klass(klass.options_class(**kwargs), self) + try: + conn.connect(context=context) + except mitogen.core.TimeoutError: + raise mitogen.core.StreamError(self.connection_timeout_msg) + + return context + + def connect(self, method_name, name=None, **kwargs): + if name: + name = mitogen.core.to_text(name) + + klass = get_connection_class(method_name) + kwargs.setdefault(u'debug', self.debug) + kwargs.setdefault(u'profiling', self.profiling) + kwargs.setdefault(u'unidirectional', self.unidirectional) + kwargs.setdefault(u'name', name) + + via = kwargs.pop(u'via', None) + if via is not None: + return self.proxy_connect(via, method_name, + **mitogen.core.Kwargs(kwargs)) + return self._connect(klass, **mitogen.core.Kwargs(kwargs)) + + def proxy_connect(self, via_context, method_name, name=None, **kwargs): + resp = via_context.call(_proxy_connect, + name=name, + method_name=method_name, + kwargs=mitogen.core.Kwargs(kwargs), + ) + if resp['msg'] is not None: + raise mitogen.core.StreamError(resp['msg']) + + name = u'%s.%s' % (via_context.name, resp['name']) + context = self.context_class(self, resp['id'], name=name) + context.via = via_context + self._write_lock.acquire() + try: + self._context_by_id[context.context_id] = context + finally: + self._write_lock.release() + return context + + def buildah(self, **kwargs): + return self.connect(u'buildah', **kwargs) + + def doas(self, **kwargs): + return self.connect(u'doas', **kwargs) + + def docker(self, **kwargs): + return self.connect(u'docker', **kwargs) + + def kubectl(self, **kwargs): + return self.connect(u'kubectl', **kwargs) + + def fork(self, **kwargs): + return self.connect(u'fork', **kwargs) + + def jail(self, **kwargs): + return self.connect(u'jail', **kwargs) + + def local(self, **kwargs): + return self.connect(u'local', **kwargs) + + def lxc(self, **kwargs): + return self.connect(u'lxc', **kwargs) + + def lxd(self, **kwargs): + return self.connect(u'lxd', **kwargs) + + def incus(self, **kwargs): + return self.connect(u'incus', **kwargs) + + def setns(self, **kwargs): + return self.connect(u'setns', **kwargs) + + def su(self, **kwargs): + return self.connect(u'su', **kwargs) + + def sudo(self, **kwargs): + return self.connect(u'sudo', **kwargs) + + def ssh(self, **kwargs): + return self.connect(u'ssh', **kwargs) + + def podman(self, **kwargs): + return self.connect(u'podman', **kwargs) + + +class Reaper(object): + """ + Asynchronous logic for reaping :class:`Process` objects. This is necessary + to prevent uncontrolled buildup of zombie processes in long-lived parents + that will eventually reach an OS limit, preventing creation of new threads + and processes, and to log the exit status of the child in the case of an + error. + + To avoid modifying process-global state such as with + :func:`signal.set_wakeup_fd` or installing a :data:`signal.SIGCHLD` handler + that might interfere with the user's ability to use those facilities, + Reaper polls for exit with backoff using timers installed on an associated + :class:`Broker`. + + :param mitogen.core.Broker broker: + The :class:`Broker` on which to install timers + :param mitogen.parent.Process proc: + The process to reap. + :param bool kill: + If :data:`True`, send ``SIGTERM`` and ``SIGKILL`` to the process. + :param bool wait_on_shutdown: + If :data:`True`, delay :class:`Broker` shutdown if child has not yet + exited. If :data:`False` simply forget the child. + """ + #: :class:`Timer` that invokes :meth:`reap` after some polling delay. + _timer = None + + def __init__(self, broker, proc, kill, wait_on_shutdown): + self.broker = broker + self.proc = proc + self.kill = kill + self.wait_on_shutdown = wait_on_shutdown + self._tries = 0 + + def _signal_child(self, signum): + # For processes like sudo we cannot actually send sudo a signal, + # because it is setuid, so this is best-effort only. + LOG.debug('%r: sending %s', self.proc, SIGNAL_BY_NUM[signum]) + try: + self.proc.send_signal(signum) + except OSError: + e = sys.exc_info()[1] + if e.args[0] != errno.EPERM: + raise + + def _calc_delay(self, count): + """ + Calculate a poll delay given `count` attempts have already been made. + These constants have no principle, they just produce rapid but still + relatively conservative retries. + """ + delay = 0.05 + factor = 1.72 + return delay * factor ** count + + def _on_broker_shutdown(self): + """ + Respond to :class:`Broker` shutdown by cancelling the reap timer if + :attr:`Router.await_children_at_shutdown` is disabled. Otherwise + shutdown is delayed for up to :attr:`Broker.shutdown_timeout` for + subprocesses may have no intention of exiting any time soon. + """ + if not self.wait_on_shutdown: + self._timer.cancel() + + def _install_timer(self, delay): + new = self._timer is None + self._timer = self.broker.timers.schedule( + when=mitogen.core.now() + delay, + func=self.reap, + ) + if new: + mitogen.core.listen(self.broker, 'shutdown', + self._on_broker_shutdown) + + def _remove_timer(self): + if self._timer and self._timer.active: + self._timer.cancel() + mitogen.core.unlisten(self.broker, 'shutdown', + self._on_broker_shutdown) + + def reap(self): + """ + Reap the child process during disconnection. + """ + status = self.proc.poll() + if status is not None: + LOG.debug('%r: %s', self.proc, returncode_to_str(status)) + mitogen.core.fire(self.proc, 'exit') + self._remove_timer() + return + + self._tries += 1 + if self._tries > 20: + LOG.warning('%r: child will not exit, giving up', self) + self._remove_timer() + return + + delay = self._calc_delay(self._tries - 1) + LOG.debug('%r still running after IO disconnect, recheck in %.03fs', + self.proc, delay) + self._install_timer(delay) + + if not self.kill: + pass + elif self._tries == 2: + self._signal_child(signal.SIGTERM) + elif self._tries == 6: # roughly 4 seconds + self._signal_child(signal.SIGKILL) + + +class Process(object): + """ + Process objects provide a uniform interface to the :mod:`subprocess` and + :mod:`mitogen.fork`. This class is extended by :class:`PopenProcess` and + :class:`mitogen.fork.Process`. + + :param int pid: + The process ID. + :param file stdin: + File object attached to standard input. + :param file stdout: + File object attached to standard output. + :param file stderr: + File object attached to standard error, or :data:`None`. + """ + #: Name of the process used in logs. Set to the stream/context name by + #: :class:`Connection`. + name = None + + def __init__(self, pid, stdin, stdout, stderr=None): + #: The process ID. + self.pid = pid + #: File object attached to standard input. + self.stdin = stdin + #: File object attached to standard output. + self.stdout = stdout + #: File object attached to standard error. + self.stderr = stderr + + def __repr__(self): + return '%s %s pid %d' % ( + type(self).__name__, + self.name, + self.pid, + ) + + def poll(self): + """ + Fetch the child process exit status, or :data:`None` if it is still + running. This should be overridden by subclasses. + + :returns: + Exit status in the style of the :attr:`subprocess.Popen.returncode` + attribute, i.e. with signals represented by a negative integer. + """ + raise NotImplementedError() + + def send_signal(self, sig): + os.kill(self.pid, sig) + + def terminate(self): + "Ask the process to gracefully shutdown." + self.send_signal(signal.SIGTERM) + + def kill(self): + "Ask the operating system to forcefully destroy the process." + self.send_signal(signal.SIGKILL) + + +class PopenProcess(Process): + """ + :class:`Process` subclass wrapping a :class:`subprocess.Popen` object. + + :param subprocess.Popen proc: + The subprocess. + """ + def __init__(self, proc, stdin, stdout, stderr=None): + super(PopenProcess, self).__init__(proc.pid, stdin, stdout, stderr) + #: The subprocess. + self.proc = proc + + def poll(self): + return self.proc.poll() + + def send_signal(self, sig): + self.proc.send_signal(sig) + + +class ModuleForwarder(object): + """ + Respond to :data:`mitogen.core.GET_MODULE` requests in a child by + forwarding the request to our parent context, or satisfying the request + from our local Importer cache. + """ + def __init__(self, router, parent_context, importer): + self.router = router + self.parent_context = parent_context + self.importer = importer + router.add_handler( + fn=self._on_forward_module, + handle=mitogen.core.FORWARD_MODULE, + persist=True, + policy=mitogen.core.has_parent_authority, + ) + router.add_handler( + fn=self._on_get_module, + handle=mitogen.core.GET_MODULE, + persist=True, + policy=is_immediate_child, + ) + + def __repr__(self): + return 'ModuleForwarder' + + def _on_forward_module(self, msg): + if msg.is_dead: + return + + context_id_s, _, fullname = bytes_partition(msg.data, b('\x00')) + fullname = mitogen.core.to_text(fullname) + context_id = int(context_id_s) + stream = self.router.stream_by_id(context_id) + if stream.protocol.remote_id == mitogen.parent_id: + LOG.error('%r: dropping FORWARD_MODULE(%d, %r): no route to child', + self, context_id, fullname) + return + + if fullname in stream.protocol.sent_modules: + return + + LOG.debug('%r._on_forward_module() sending %r to %r via %r', + self, fullname, context_id, stream.protocol.remote_id) + self._send_module_and_related(stream, fullname) + if stream.protocol.remote_id != context_id: + stream.protocol._send( + mitogen.core.Message( + data=msg.data, + handle=mitogen.core.FORWARD_MODULE, + dst_id=stream.protocol.remote_id, + ) + ) + + def _on_get_module(self, msg): + if msg.is_dead: + return + + fullname = msg.data.decode('utf-8') + LOG.debug('%r: %s requested by context %d', self, fullname, msg.src_id) + callback = lambda: self._on_cache_callback(msg, fullname) + self.importer._request_module(fullname, callback) + + def _on_cache_callback(self, msg, fullname): + stream = self.router.stream_by_id(msg.src_id) + LOG.debug('%r: sending %s to %r', self, fullname, stream) + self._send_module_and_related(stream, fullname) + + def _send_module_and_related(self, stream, fullname): + tup = self.importer._cache[fullname] + for related in tup[4]: + rtup = self.importer._cache.get(related) + if rtup: + self._send_one_module(stream, rtup) + else: + LOG.debug('%r: %s not in cache (for %s)', + self, related, fullname) + + self._send_one_module(stream, tup) + + def _send_one_module(self, stream, tup): + if tup[0] not in stream.protocol.sent_modules: + stream.protocol.sent_modules.add(tup[0]) + self.router._async_route( + mitogen.core.Message.pickled( + tup, + dst_id=stream.protocol.remote_id, + handle=mitogen.core.LOAD_MODULE, + ) + ) + + +class ResourceForwarder(object): + """ + Handle :data:`mitogen.core.GET_RESOURCE` requests from children by + forwarding the request to our parent, or satisfying the request from + our local :class:`mitogen.core.ResourceRequester` cache. + """ + def __init__(self, router, parent_context, requester): + self.router = router + self.parent_context = parent_context + self.requester = requester + router.add_handler( + fn=self._on_get_resource, + handle=mitogen.core.GET_RESOURCE, + persist=True, + policy=is_immediate_child, + ) + + def _on_get_resource(self, msg): + if msg.is_dead: + return + + fullname, resource = msg.unpickle() + callback = lambda: self._on_cache_callback(msg, fullname, resource) + self.requester._request_resource(fullname, resource, callback) + + def _on_cache_callback(self, msg, fullname, resource): + stream = self.router.stream_by_id(msg.src_id) + self._send_resource(stream, fullname, resource) + + def _send_resource(self, stream, fullname, resource): + content = self.requester._cache[(fullname, resource)] + + msg = mitogen.core.Message.pickled( + (fullname, resource), content, + dst_id=stream.protocol.remote_id, + handle=mitogen.core.LOAD_RESOURCE, + ) + self.router._async_route(msg) diff --git a/plugins/mitogen-0.3.44/mitogen/podman.py b/plugins/mitogen-0.3.44/mitogen/podman.py new file mode 100644 index 0000000..acc46a3 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/podman.py @@ -0,0 +1,73 @@ +# Copyright 2019, David Wilson +# Copyright 2021, Mitogen contributors +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import logging + +import mitogen.parent + + +LOG = logging.getLogger(__name__) + + +class Options(mitogen.parent.Options): + container = None + username = None + podman_path = 'podman' + + def __init__(self, container=None, podman_path=None, username=None, + **kwargs): + super(Options, self).__init__(**kwargs) + assert container is not None + self.container = container + if podman_path: + self.podman_path = podman_path + if username: + self.username = username + + +class Connection(mitogen.parent.Connection): + options_class = Options + child_is_immediate_subprocess = False + + # TODO: better way of capturing errors such as "No such container." + create_child_args = { + 'merge_stdio': True + } + + def _get_name(self): + return u'podman.' + self.options.container + + def get_boot_command(self): + args = [self.options.podman_path, 'exec'] + if self.options.username: + args += ['--user=' + self.options.username] + args += ["--interactive", "--", self.options.container] + return args + super(Connection, self).get_boot_command() diff --git a/plugins/mitogen-0.3.44/mitogen/profiler.py b/plugins/mitogen-0.3.44/mitogen/profiler.py new file mode 100644 index 0000000..bbf6086 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/profiler.py @@ -0,0 +1,164 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +""" +mitogen.profiler + Record and report cProfile statistics from a run. Creates one aggregated + output file, one aggregate containing only workers, and one for the + top-level process. + +Usage: + mitogen.profiler record [args ..] + mitogen.profiler report [sort_mode] + mitogen.profiler stat [args ..] + +Mode: + record: Record a trace. + report: Report on a previously recorded trace. + stat: Record and report in a single step. + +Where: + dest_path: Filesystem prefix to write .pstats files to. + sort_mode: Sorting mode; defaults to "cumulative". See: + https://docs.python.org/2/library/profile.html#pstats.Stats.sort_stats + +Example: + mitogen.profiler record /tmp/mypatch ansible-playbook foo.yml + mitogen.profiler dump /tmp/mypatch-worker.pstats +""" + +from __future__ import print_function +import os +import pstats +import shutil +import subprocess +import sys +import tempfile +import time + + +def try_merge(stats, path): + try: + stats.add(path) + return True + except Exception as e: + print('%s failed. Will retry. %s' % (path, e)) + return False + + +def merge_stats(outpath, inpaths): + first, rest = inpaths[0], inpaths[1:] + for x in range(1): + try: + stats = pstats.Stats(first) + except EOFError: + time.sleep(0.2) + continue + + print("Writing %r..." % (outpath,)) + for path in rest: + #print("Merging %r into %r.." % (os.path.basename(path), outpath)) + for x in range(5): + if try_merge(stats, path): + break + time.sleep(0.2) + + stats.dump_stats(outpath) + + +def generate_stats(outpath, tmpdir): + print('Generating stats..') + all_paths = [] + paths_by_ident = {} + + for name in os.listdir(tmpdir): + if name.endswith('-dump.pstats'): + ident, _, pid = name.partition('-') + path = os.path.join(tmpdir, name) + all_paths.append(path) + paths_by_ident.setdefault(ident, []).append(path) + + merge_stats('%s-all.pstat' % (outpath,), all_paths) + for ident, paths in paths_by_ident.items(): + merge_stats('%s-%s.pstat' % (outpath, ident), paths) + + +def do_record(tmpdir, path, *args): + env = os.environ.copy() + fmt = '%(identity)s-%(pid)s.%(now)s-dump.%(ext)s' + env['MITOGEN_PROFILING'] = '1' + env['MITOGEN_PROFILE_FMT'] = os.path.join(tmpdir, fmt) + rc = subprocess.call(args, env=env) + generate_stats(path, tmpdir) + return rc + + +def do_report(tmpdir, path, sort='cumulative'): + stats = pstats.Stats(path).sort_stats(sort) + stats.print_stats(100) + + +def do_stat(tmpdir, sort, *args): + valid_sorts = pstats.Stats.sort_arg_dict_default + if sort not in valid_sorts: + sys.stderr.write('Invalid sort %r, must be one of %s\n' % + (sort, ', '.join(sorted(valid_sorts)))) + sys.exit(1) + + outfile = os.path.join(tmpdir, 'combined') + do_record(tmpdir, outfile, *args) + aggs = ('app.main', 'mitogen.broker', 'mitogen.child_main', + 'mitogen.service.pool', 'Strategy', 'WorkerProcess', + 'all') + for agg in aggs: + path = '%s-%s.pstat' % (outfile, agg) + if os.path.exists(path): + print() + print() + print('------ Aggregation %r ------' % (agg,)) + print() + do_report(tmpdir, path, sort) + print() + + +def main(): + if len(sys.argv) < 2 or sys.argv[1] not in ('record', 'report', 'stat'): + sys.stderr.write(__doc__.lstrip()) + sys.exit(1) + + func = globals()['do_' + sys.argv[1]] + tmpdir = tempfile.mkdtemp(prefix='mitogen.profiler') + try: + sys.exit(func(tmpdir, *sys.argv[2:]) or 0) + finally: + shutil.rmtree(tmpdir) + +if __name__ == '__main__': + main() diff --git a/plugins/mitogen-0.3.44/mitogen/select.py b/plugins/mitogen-0.3.44/mitogen/select.py new file mode 100644 index 0000000..d719f78 --- /dev/null +++ b/plugins/mitogen-0.3.44/mitogen/select.py @@ -0,0 +1,348 @@ +# Copyright 2019, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# !mitogen: minify_safe + +import mitogen.core + + +class Error(mitogen.core.Error): + pass + + +class Event(object): + """ + Represents one selected event. + """ + #: The first Receiver or Latch the event traversed. + source = None + + #: The :class:`mitogen.core.Message` delivered to a receiver, or the object + #: posted to a latch. + data = None + + +class Select(object): + """ + Support scatter/gather asynchronous calls and waiting on multiple + :class:`receivers `, + :class:`channels `, + :class:`latches `, and + :class:`sub-selects