From d047e7c4aa883087a45aecc478cec7bb09bb6e45 Mon Sep 17 00:00:00 2001 From: Tim Dittler Date: Tue, 14 Jan 2020 13:57:04 +0100 Subject: [PATCH] Add mitogen plugin --- ansible.cfg | 15 + mitogen/LICENSE | 26 + mitogen/MANIFEST.in | 1 + mitogen/PKG-INFO | 23 + mitogen/README.md | 13 + mitogen/ansible_mitogen/__init__.py | 0 .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 135 bytes .../__pycache__/affinity.cpython-36.pyc | Bin 0 -> 9923 bytes .../__pycache__/connection.cpython-36.pyc | Bin 0 -> 26868 bytes .../__pycache__/loaders.cpython-36.pyc | Bin 0 -> 794 bytes .../__pycache__/logging.cpython-36.pyc | Bin 0 -> 2399 bytes .../__pycache__/mixins.cpython-36.pyc | Bin 0 -> 14202 bytes .../__pycache__/module_finder.cpython-36.pyc | Bin 0 -> 3189 bytes .../__pycache__/parsing.cpython-36.pyc | Bin 0 -> 1430 bytes .../__pycache__/planner.cpython-36.pyc | Bin 0 -> 17350 bytes .../__pycache__/process.cpython-36.pyc | Bin 0 -> 21353 bytes .../__pycache__/runner.cpython-36.pyc | Bin 0 -> 33158 bytes .../__pycache__/services.cpython-36.pyc | Bin 0 -> 15849 bytes .../__pycache__/strategy.cpython-36.pyc | Bin 0 -> 11834 bytes .../__pycache__/target.cpython-36.pyc | Bin 0 -> 20116 bytes .../transport_config.cpython-36.pyc | Bin 0 -> 23317 bytes mitogen/ansible_mitogen/affinity.py | 286 ++ mitogen/ansible_mitogen/compat/__init__.py | 0 .../compat/simplejson/__init__.py | 318 ++ .../compat/simplejson/decoder.py | 354 ++ .../compat/simplejson/encoder.py | 440 ++ .../compat/simplejson/scanner.py | 65 + mitogen/ansible_mitogen/connection.py | 1056 +++++ mitogen/ansible_mitogen/loaders.py | 62 + mitogen/ansible_mitogen/logging.py | 128 + mitogen/ansible_mitogen/mixins.py | 428 ++ mitogen/ansible_mitogen/module_finder.py | 157 + mitogen/ansible_mitogen/parsing.py | 76 + mitogen/ansible_mitogen/planner.py | 576 +++ mitogen/ansible_mitogen/plugins/__init__.py | 0 .../plugins/action/__init__.py | 0 .../plugins/action/mitogen_fetch.py | 162 + .../plugins/action/mitogen_get_stack.py | 55 + .../plugins/connection/__init__.py | 0 .../__pycache__/mitogen_ssh.cpython-36.pyc | Bin 0 -> 1422 bytes .../plugins/connection/mitogen_buildah.py | 44 + .../plugins/connection/mitogen_doas.py | 44 + .../plugins/connection/mitogen_docker.py | 51 + .../plugins/connection/mitogen_jail.py | 44 + .../plugins/connection/mitogen_kubectl.py | 79 + .../plugins/connection/mitogen_local.py | 86 + .../plugins/connection/mitogen_lxc.py | 44 + .../plugins/connection/mitogen_lxd.py | 44 + .../plugins/connection/mitogen_machinectl.py | 44 + .../plugins/connection/mitogen_setns.py | 44 + .../plugins/connection/mitogen_ssh.py | 67 + .../plugins/connection/mitogen_su.py | 44 + .../plugins/connection/mitogen_sudo.py | 44 + .../plugins/strategy/__init__.py | 0 .../__pycache__/mitogen_linear.cpython-36.pyc | Bin 0 -> 734 bytes .../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/ansible_mitogen/process.py | 745 +++ mitogen/ansible_mitogen/runner.py | 1020 +++++ mitogen/ansible_mitogen/services.py | 559 +++ mitogen/ansible_mitogen/strategy.py | 373 ++ mitogen/ansible_mitogen/target.py | 777 ++++ mitogen/ansible_mitogen/transport_config.py | 699 +++ mitogen/mitogen.egg-info/PKG-INFO | 23 + mitogen/mitogen.egg-info/SOURCES.txt | 81 + mitogen/mitogen.egg-info/dependency_links.txt | 1 + mitogen/mitogen.egg-info/not-zip-safe | 1 + mitogen/mitogen.egg-info/top_level.txt | 2 + mitogen/mitogen/__init__.py | 120 + .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 2192 bytes .../mitogen/__pycache__/core.cpython-36.pyc | Bin 0 -> 121555 bytes .../mitogen/__pycache__/debug.cpython-36.pyc | Bin 0 -> 6318 bytes .../mitogen/__pycache__/fork.cpython-36.pyc | Bin 0 -> 5764 bytes .../mitogen/__pycache__/master.cpython-36.pyc | Bin 0 -> 39739 bytes .../mitogen/__pycache__/minify.cpython-36.pyc | Bin 0 -> 2872 bytes .../mitogen/__pycache__/parent.cpython-36.pyc | Bin 0 -> 84814 bytes .../mitogen/__pycache__/select.cpython-36.pyc | Bin 0 -> 10518 bytes .../__pycache__/service.cpython-36.pyc | Bin 0 -> 34010 bytes .../mitogen/__pycache__/ssh.cpython-36.pyc | Bin 0 -> 7030 bytes .../mitogen/__pycache__/sudo.cpython-36.pyc | Bin 0 -> 6051 bytes .../mitogen/__pycache__/unix.cpython-36.pyc | Bin 0 -> 5960 bytes .../mitogen/__pycache__/utils.cpython-36.pyc | Bin 0 -> 5971 bytes mitogen/mitogen/buildah.py | 73 + mitogen/mitogen/compat/__init__.py | 0 mitogen/mitogen/compat/pkgutil.py | 594 +++ mitogen/mitogen/compat/tokenize.py | 453 ++ mitogen/mitogen/core.py | 3997 +++++++++++++++++ mitogen/mitogen/debug.py | 236 + mitogen/mitogen/doas.py | 142 + mitogen/mitogen/docker.py | 83 + mitogen/mitogen/fakessh.py | 456 ++ mitogen/mitogen/fork.py | 250 ++ mitogen/mitogen/jail.py | 65 + mitogen/mitogen/kubectl.py | 67 + mitogen/mitogen/lxc.py | 74 + mitogen/mitogen/lxd.py | 76 + mitogen/mitogen/master.py | 1357 ++++++ mitogen/mitogen/minify.py | 143 + mitogen/mitogen/os_fork.py | 187 + mitogen/mitogen/parent.py | 2770 ++++++++++++ mitogen/mitogen/profiler.py | 164 + mitogen/mitogen/select.py | 348 ++ mitogen/mitogen/service.py | 1146 +++++ mitogen/mitogen/setns.py | 241 + mitogen/mitogen/ssh.py | 294 ++ mitogen/mitogen/su.py | 160 + mitogen/mitogen/sudo.py | 271 ++ mitogen/mitogen/unix.py | 226 + mitogen/mitogen/utils.py | 226 + mitogen/setup.cfg | 15 + mitogen/setup.py | 67 + 113 files changed, 23754 insertions(+) create mode 100644 mitogen/LICENSE create mode 100644 mitogen/MANIFEST.in create mode 100644 mitogen/PKG-INFO create mode 100644 mitogen/README.md create mode 100644 mitogen/ansible_mitogen/__init__.py create mode 100644 mitogen/ansible_mitogen/__pycache__/__init__.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/affinity.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/connection.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/loaders.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/logging.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/mixins.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/module_finder.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/parsing.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/planner.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/process.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/runner.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/services.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/strategy.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/target.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/__pycache__/transport_config.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/affinity.py create mode 100644 mitogen/ansible_mitogen/compat/__init__.py create mode 100644 mitogen/ansible_mitogen/compat/simplejson/__init__.py create mode 100644 mitogen/ansible_mitogen/compat/simplejson/decoder.py create mode 100644 mitogen/ansible_mitogen/compat/simplejson/encoder.py create mode 100644 mitogen/ansible_mitogen/compat/simplejson/scanner.py create mode 100644 mitogen/ansible_mitogen/connection.py create mode 100644 mitogen/ansible_mitogen/loaders.py create mode 100644 mitogen/ansible_mitogen/logging.py create mode 100644 mitogen/ansible_mitogen/mixins.py create mode 100644 mitogen/ansible_mitogen/module_finder.py create mode 100644 mitogen/ansible_mitogen/parsing.py create mode 100644 mitogen/ansible_mitogen/planner.py create mode 100644 mitogen/ansible_mitogen/plugins/__init__.py create mode 100644 mitogen/ansible_mitogen/plugins/action/__init__.py create mode 100644 mitogen/ansible_mitogen/plugins/action/mitogen_fetch.py create mode 100644 mitogen/ansible_mitogen/plugins/action/mitogen_get_stack.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/__init__.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/__pycache__/mitogen_ssh.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_buildah.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_doas.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_docker.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_jail.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_kubectl.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_local.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_lxc.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_lxd.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_machinectl.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_setns.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_ssh.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_su.py create mode 100644 mitogen/ansible_mitogen/plugins/connection/mitogen_sudo.py create mode 100644 mitogen/ansible_mitogen/plugins/strategy/__init__.py create mode 100644 mitogen/ansible_mitogen/plugins/strategy/__pycache__/mitogen_linear.cpython-36.pyc create mode 100644 mitogen/ansible_mitogen/plugins/strategy/mitogen.py create mode 100644 mitogen/ansible_mitogen/plugins/strategy/mitogen_free.py create mode 100644 mitogen/ansible_mitogen/plugins/strategy/mitogen_host_pinned.py create mode 100644 mitogen/ansible_mitogen/plugins/strategy/mitogen_linear.py create mode 100644 mitogen/ansible_mitogen/process.py create mode 100644 mitogen/ansible_mitogen/runner.py create mode 100644 mitogen/ansible_mitogen/services.py create mode 100644 mitogen/ansible_mitogen/strategy.py create mode 100644 mitogen/ansible_mitogen/target.py create mode 100644 mitogen/ansible_mitogen/transport_config.py create mode 100644 mitogen/mitogen.egg-info/PKG-INFO create mode 100644 mitogen/mitogen.egg-info/SOURCES.txt create mode 100644 mitogen/mitogen.egg-info/dependency_links.txt create mode 100644 mitogen/mitogen.egg-info/not-zip-safe create mode 100644 mitogen/mitogen.egg-info/top_level.txt create mode 100644 mitogen/mitogen/__init__.py create mode 100644 mitogen/mitogen/__pycache__/__init__.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/core.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/debug.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/fork.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/master.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/minify.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/parent.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/select.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/service.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/ssh.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/sudo.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/unix.cpython-36.pyc create mode 100644 mitogen/mitogen/__pycache__/utils.cpython-36.pyc create mode 100644 mitogen/mitogen/buildah.py create mode 100644 mitogen/mitogen/compat/__init__.py create mode 100644 mitogen/mitogen/compat/pkgutil.py create mode 100644 mitogen/mitogen/compat/tokenize.py create mode 100644 mitogen/mitogen/core.py create mode 100644 mitogen/mitogen/debug.py create mode 100644 mitogen/mitogen/doas.py create mode 100644 mitogen/mitogen/docker.py create mode 100644 mitogen/mitogen/fakessh.py create mode 100644 mitogen/mitogen/fork.py create mode 100644 mitogen/mitogen/jail.py create mode 100644 mitogen/mitogen/kubectl.py create mode 100644 mitogen/mitogen/lxc.py create mode 100644 mitogen/mitogen/lxd.py create mode 100644 mitogen/mitogen/master.py create mode 100644 mitogen/mitogen/minify.py create mode 100644 mitogen/mitogen/os_fork.py create mode 100644 mitogen/mitogen/parent.py create mode 100644 mitogen/mitogen/profiler.py create mode 100644 mitogen/mitogen/select.py create mode 100644 mitogen/mitogen/service.py create mode 100644 mitogen/mitogen/setns.py create mode 100644 mitogen/mitogen/ssh.py create mode 100644 mitogen/mitogen/su.py create mode 100644 mitogen/mitogen/sudo.py create mode 100644 mitogen/mitogen/unix.py create mode 100644 mitogen/mitogen/utils.py create mode 100644 mitogen/setup.cfg create mode 100644 mitogen/setup.py diff --git a/ansible.cfg b/ansible.cfg index 8d36fb2..8c78588 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,3 +1,18 @@ [defaults] inventory = hosts host_key_checking = False +ask_pass = True +retry_files_enabled = False + +# Enable Mitogen usage +# * Download https://networkgenomics.com/try/mitogen-0.2.8.tar.gz +# * Untar to ./mitogen (no version number) +strategy_plugins = ./mitogen/ansible_mitogen/plugins/strategy +strategy = mitogen_linear + +[connection] +pipelining = True + +[privilege_escalation] +become = True +become_ask_pass = True diff --git a/mitogen/LICENSE b/mitogen/LICENSE new file mode 100644 index 0000000..70e43a9 --- /dev/null +++ b/mitogen/LICENSE @@ -0,0 +1,26 @@ +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. diff --git a/mitogen/MANIFEST.in b/mitogen/MANIFEST.in new file mode 100644 index 0000000..1aba38f --- /dev/null +++ b/mitogen/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE diff --git a/mitogen/PKG-INFO b/mitogen/PKG-INFO new file mode 100644 index 0000000..3346b33 --- /dev/null +++ b/mitogen/PKG-INFO @@ -0,0 +1,23 @@ +Metadata-Version: 1.1 +Name: mitogen +Version: 0.2.9 +Summary: Library for writing distributed self-replicating programs. +Home-page: https://github.com/dw/mitogen/ +Author: David Wilson +Author-email: UNKNOWN +License: New BSD +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Environment :: Console +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.4 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Topic :: System :: Distributed Computing +Classifier: Topic :: System :: Systems Administration diff --git a/mitogen/README.md b/mitogen/README.md new file mode 100644 index 0000000..da93a80 --- /dev/null +++ b/mitogen/README.md @@ -0,0 +1,13 @@ + +# Mitogen + + +Please see the documentation. + +![](https://i.imgur.com/eBM6LhJ.gif) + +[![Total alerts](https://img.shields.io/lgtm/alerts/g/dw/mitogen.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/dw/mitogen/alerts/) + +[![Build Status](https://travis-ci.org/dw/mitogen.svg?branch=master)](https://travis-ci.org/dw/mitogen) + +[![Pipelines Status](https://dev.azure.com/dw-mitogen/Mitogen/_apis/build/status/dw.mitogen?branchName=master)](https://dev.azure.com/dw-mitogen/Mitogen/_build/latest?definitionId=1?branchName=master) diff --git a/mitogen/ansible_mitogen/__init__.py b/mitogen/ansible_mitogen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mitogen/ansible_mitogen/__pycache__/__init__.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d8005d06a52709ad8566e2f1d6bd8c06747e011 GIT binary patch literal 135 zcmXr!<>mT#cyBBN5IhDEFu(|8H~?`m3y?@*2xib^^jpbL1QJFNzfANq@^e%5OZ1)d zQ&RQIQnK}PGfVQ*Q}gr_^NKT*a#G`A{P_6Hyv&mLc)fzkTO2mI`6;D2sdgaKih-B` E0BfQl1^@s6 literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/affinity.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/affinity.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a68efdf840dd976039a6394188690fd76a71b67 GIT binary patch literal 9923 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 literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/connection.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/connection.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88dce073be84cb4c2cea5a0ba92a314c6eddce34 GIT binary patch literal 26868 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 F{{r1Rf~)`l literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/module_finder.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/module_finder.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9106b77798565b9bb595a34dbd91090630a6ebfb GIT binary patch literal 3189 zcmZuz&5s;M74NF9>FMeD*tN4>CjqQhBm;9`Hi8Jk$}%9^A?9OkHUT@MSd-qVn%$o5 zot~|#G3!;2gcx%q2d?lBAaTwC2?>cSM@}VfD2LqOniIcQJzm>^8CCVG*VV7y=kL|G zy4~bIfBVa$PhSawe+4i6LeSsGFdt)*fn-H+!iJ0y%|Y`a_jFivAFu5T&L2t-Q z@-lX8%2(tTo|k0!UeLet8FUKz%xz=`rYsg#jq;OusV!KWi&;J{r5Y8vRXQuo-uabm zX7YnV4NgkADAZ_S^TG^V^i#h)zcR~CR65V>QF@%uBq*(BcFgDu{5S?V;}(XwiOB|4 zFl8%t$d>Hv4bWTAiRAH$l`+ zm2AbA?0nmDa_B0s0&Z-qnB*(O9lU52u;3xG1&$QwZ+YWyl_&GOP-%wPX=N9B_L4tg zNm?FE)!43m#r1&`>P(II`b{TF_{Rx~zH5$aNls{$NgY8g-NfV?517!w)J**4d&fxa-Ca*FcFn@z%CUSQdjA9EPQ&cC-3Cdv?q8?oL{*Qk+x_x*5&aL$H9)820u|TAlMVw5}@?SF;5F%>q!*Cfc#j^2a z%Zh2E3T@;Qo-d;Z!E8$ewt1ZBcNn6F(txoKgDS!|&1H@ns;OnwoW>O4itEQ! zT=8k^7?i~RstvgbRv+8;v^DKiu?&$bvF%nNcD5_N63b52(4SoF{KWPuaG(d}PKPok zdmRyY(Z|mUFU?Vk*=(#*smxgC2X(orbdqah)051OkBC*LeL5)@NVal@id0^lQJEee z`H~$Zp0kSz%uZ62WJ(pW>zP#db)|b|9klh%BF90LhtIeEgjnSyl{qlLmimeR%A~eT z*9T5YO**XgdP&~;oEiw8%e*(Yc{u~TtWBxk*I()e;=6a=9*y!@Zbu{3b$`PpIl!m( zj%(P(yil(917nmXL3gw+wQE6&&1tQ_1QD)HxG<_uaDpsz?YkbK{eFEFoAeeL1O!#&%;lDs}o?d$pRH5mSHERxaxxm|W-necC*A0NExo#Ec zG?_swc9#r#n@7oYw>JHT6NH+sQJ^su!xrL2){cyeC%QbL#Hp=)*-+Q@qMmIzBWrxO zVXvaoY$J5~+hiq$h<l5Q;5Ym=pcf$%5Gpv1%`VWXaUAK# zr@K{S+ODt*_$CO!JQYv7%huWLs-^z{ylUaZ1llE4Yr284`Ak2^XzIUv3D9IMp{80S z;q-S8g410L1jk;IT!v4Bs&N}(T`@wwJx#smA`uAbDH#k6N}DQpbZyBjni1jlRqD0e zhD1+7A9Q#Ma@by9xaj`=opX^&EL~`bhu#W zS1E{t8%2rBj=4Fw*wer4aiB($*8LAg@7??P{;fN>-YDzBc{%sN^^Mgfz$I1zH7w*m(bWC~>`YDGusN)O4_S zmrF=>=|R1AI^EK|YyB<2yR(KBDky*W3-+}YA@cTOfUrM9oFG7)Q@=_im(&L NZTy?PuXmz2{x9K`32*=a literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/parsing.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/parsing.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cddb7586fcaedb5994d20521d6ab59cafa3f374 GIT binary patch literal 1430 zcmds1&1xGl5SFy-AB^n=nnH4DQJfyS5VD8#WI}1lsZdNI4HVO|mMuA!v#VVsZ4!eE z1-B2=hv=zqvDcn_EWP#8j@GepPrX;5+0i$e(P-vdowQnwAK$*d_nHWOM_Yq{{v4)x z3W6h!W0c@N#!e@olRhbRFZTLA_j%Q{J`*Wg>_-Z={DOYlSiF7C0RN#op#NG4gT zEkHZDlB1N1D3(@e7MtD(yWKE6m_E!im}U-Qj;3%tC)`_jAMhOW>J`zCE#{sjbF{z< z0w^Rk$DheQntBT~g><+NIDs8xfw>PJ?`&mSoz~`r!!E!oV{(rE#x^V8?Qr&>#x5YS z28HpOX(One2x>-JW|m3?xo28{r0!laRWuz_MlX6V-_i*)lOa==G&d5Y^V4}OOd0Ey zRQWAfggI`MKsRHWCDd3=O`7YG=+bkmontJo1nq>m(cw_4P^fE~&FmyqowX}ukZWbS zWgwU7n>>q!OJ(q6E5{?s^kpzOqBj$v1syokD*FMIX6s^`I9tw?DNY9vpxi61fi&R^ z0P;jAsKuBeg6;t(T)ND literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/planner.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/planner.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c74facd844e8ef5140dee04e672d011dd2a3b3e0 GIT binary patch literal 17350 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 G@qYke_ko@O literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/process.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/process.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5116c393b95d5fd45fa99322a6835673ab5a265 GIT binary patch literal 21353 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 XOSSphvAOY?Gc(W6JTvp_);s?XW}lK= literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/runner.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/runner.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c5d7bfe03728a4c96647b596979c2b306565b66 GIT binary patch literal 33158 zcmbt-3y@q_T3+AVkLl@oXf%>W56kwo9=1l7TC!}fc4uX;WIb%{+9Q`nUhfX}_Vo0< zGt)CY-K}$POPXf1F`mt?N!SgLf)E1KQV9uh=brPQ|NZ~x+-JweN`L$_Kl%1MpUY%^C)4wH3da}m z3*UD$89y_h@g3i7IP;F|-Fa8`+4-#ObMrab=jZdXFU%KYKQuoi`{I02_QUhTvMduZ0INV%##N z1bf#?>c^Z6|9y9U&nGiY_Y+B}ee?TJ>aZ#GWmAehnwsBdN>1RdLyi6G`{(yN8I(Sv zOMkPYrfm74#sPi5=b2(7zkYE3(7-bvwa*kAkLdgQnc%QLyyeV4D*b+J{&Bw)Jih9! zK2bS>Rv!)a;C(+5>{&hLyC`-1-gEv3u4LXmHUH$sc<@wkJa}>orO>)5wC==O38lYZ zze(5r&Ccz2-prunv_BS1ub$k>&OaSIx_T-&joQloxWDT&nfWs>XZ#6&_h&Nx?%*s+ zJcIK|e-F<0$oaY8oWB?4pT&M3_RslK{(e07ynnzyi2Vmp{*eC&%0D9I&*S{C|0vEM zmGckc{4xJ=oIft-AHuokKY{Znsi>To;t~}>Ik1NlcD_4AXE;D_;`k0f+ly0pAVc^vo)i4Y~ zufE=H1nWUF@`8IotrOK-O>e2Cyr5cJ@xo}M5qPa7?@}|YFXC{$<#!rE*+U&0UR4F9 zdNYiw&8S|D0^h4Qy=W!yqN-XBBCpnJM!~(vTio#ORySCSzEN&(%*?1xv!W|IF4b5R@1LHm%Y~;)n+qLY00*}5TZ`Jx*Blk zMJ;c!Q*Zca=5mJ}45eT7=#yqTf`+u)uSV5VUJ%vFrLT~0%uYM8vxv>E#f>Nk zEA6U^>Vym~jfat{sNO3>QLDm^;%0F!QfTM(8#vD2?zEyH9%{6fmvOD@e*OKph!
Wta3Nf21oz5eyHSr$OcQW& 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 DTT4H9 literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/strategy.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/strategy.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2c97677a24adda06600c42d67f27db364b427c0 GIT binary patch literal 11834 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 V^-A+qVAh#`)oOFTd2RW({vTK>Z^{4w literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/target.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/target.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..955df01250c0816855a954586b67821406a5a9bc GIT binary patch literal 20116 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 literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/__pycache__/transport_config.cpython-36.pyc b/mitogen/ansible_mitogen/__pycache__/transport_config.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fea8a8071e30a312f335d5e30eb074adf18f7e2 GIT binary patch literal 23317 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 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 +import ctypes +import logging +import mmap +import multiprocessing +import os +import struct + +import mitogen.core +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 mitogen.core.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/mitogen/ansible_mitogen/compat/__init__.py b/mitogen/ansible_mitogen/compat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mitogen/ansible_mitogen/compat/simplejson/__init__.py b/mitogen/ansible_mitogen/compat/simplejson/__init__.py new file mode 100644 index 0000000..d5b4d39 --- /dev/null +++ b/mitogen/ansible_mitogen/compat/simplejson/__init__.py @@ -0,0 +1,318 @@ +r"""JSON (JavaScript Object Notation) is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +:mod:`simplejson` exposes an API familiar to users of the standard library +:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained +version of the :mod:`json` library contained in Python 2.6, but maintains +compatibility with Python 2.4 and Python 2.5 and (currently) has +significant performance advantages, even without using the optional C +extension for speedups. + +Encoding basic Python object hierarchies:: + + >>> import simplejson as json + >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print json.dumps("\"foo\bar") + "\"foo\bar" + >>> print json.dumps(u'\u1234') + "\u1234" + >>> print json.dumps('\\') + "\\" + >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> json.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson as json + >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson as json + >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4) + >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson as json + >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj + True + >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' + True + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> json.load(io)[0] == 'streaming API' + True + +Specializing JSON object decoding:: + + >>> import simplejson as json + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> import decimal + >>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1') + True + +Specializing JSON object encoding:: + + >>> import simplejson as json + >>> def encode_complex(obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... raise TypeError(repr(o) + " is not JSON serializable") + ... + >>> json.dumps(2 + 1j, default=encode_complex) + '[2.0, 1.0]' + >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) + '[2.0, 1.0]' + >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) + '[2.0, 1.0]' + + +Using simplejson.tool from the shell to validate and pretty-print:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) +""" +__version__ = '2.0.9' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONEncoder', +] + +__author__ = 'Bob Ippolito ' + +from decoder import JSONDecoder +from encoder import JSONEncoder + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and object + members will be pretty-printed with that indent level. An indent level + of 0 will only insert newlines. ``None`` is the most compact representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is false then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + If the contents of ``fp`` is encoded with an ASCII based encoding other + than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must + be specified. Encodings that are not ASCII based (such as UCS-2) are + not allowed, and should be wrapped with + ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` + object and passed to ``loads()`` + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding + other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name + must be specified. Encodings that are not ASCII based (such as UCS-2) + are not allowed and should be decoded to ``unicode`` first. + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN, null, true, false. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + return cls(encoding=encoding, **kw).decode(s) diff --git a/mitogen/ansible_mitogen/compat/simplejson/decoder.py b/mitogen/ansible_mitogen/compat/simplejson/decoder.py new file mode 100644 index 0000000..b769ea4 --- /dev/null +++ b/mitogen/ansible_mitogen/compat/simplejson/decoder.py @@ -0,0 +1,354 @@ +"""Implementation of JSONDecoder +""" +import re +import sys +import struct + +from simplejson.scanner import make_scanner +try: + from simplejson._speedups import scanstring as c_scanstring +except ImportError: + c_scanstring = None + +__all__ = ['JSONDecoder'] + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + # Note that this function is called from _speedups + lineno, colno = linecol(doc, pos) + if end is None: + #fmt = '{0}: line {1} column {2} (char {3})' + #return fmt.format(msg, lineno, colno, pos) + fmt = '%s: line %d column %d (char %d)' + return fmt % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' + #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) + fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' + return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, +} + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == '"': + break + elif terminator != '\\': + if strict: + msg = "Invalid control character %r at" % (terminator,) + #msg = "Invalid control character {0!r} at".format(terminator) + raise ValueError(errmsg(msg, s, end)) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: " + repr(esc) + raise ValueError(errmsg(msg, s, end)) + end += 1 + else: + # Unicode escape sequence + esc = s[end + 1:end + 5] + next_end = end + 5 + if len(esc) != 4: + msg = "Invalid \\uXXXX escape" + raise ValueError(errmsg(msg, s, end)) + uni = int(esc, 16) + # Check for surrogate pair on UCS-4 systems + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise ValueError(errmsg(msg, s, end)) + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise ValueError(errmsg(msg, s, end)) + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + char = unichr(uni) + end = next_end + # Append the unescaped character + _append(char) + return u''.join(chunks), end + + +# Use speedup if available +scanstring = c_scanstring or py_scanstring + +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + +def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + pairs = {} + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + return pairs, end + 1 + elif nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end)) + end += 1 + while True: + key, end = scanstring(s, end, encoding, strict) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise ValueError(errmsg("Expecting : delimiter", s, end)) + + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + pairs[key] = value + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end - 1)) + + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + +def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + values = [] + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return values, end + 1 + _append = values.append + while True: + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end)) + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return values, end + +class JSONDecoder(object): + """Simple JSON decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + + """ + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True): + """``encoding`` determines the encoding used to interpret any ``str`` + objects decoded by this instance (utf-8 by default). It has no + effect when decoding ``unicode`` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as ``unicode``. + + ``object_hook``, if specified, will be called with the result + of every JSON object decoded and its return value will be used in + place of the given ``dict``. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + """ + self.encoding = encoding + self.object_hook = object_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.strict = strict + self.parse_object = JSONObject + self.parse_array = JSONArray + self.parse_string = scanstring + self.scan_once = make_scanner(self) + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise ValueError(errmsg("Extra data", s, end, len(s))) + return obj + + def raw_decode(self, s, idx=0): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning + with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + obj, end = self.scan_once(s, idx) + except StopIteration: + raise ValueError("No JSON object could be decoded") + return obj, end diff --git a/mitogen/ansible_mitogen/compat/simplejson/encoder.py b/mitogen/ansible_mitogen/compat/simplejson/encoder.py new file mode 100644 index 0000000..cf58290 --- /dev/null +++ b/mitogen/ansible_mitogen/compat/simplejson/encoder.py @@ -0,0 +1,440 @@ +"""Implementation of JSONEncoder +""" +import re + +try: + from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii +except ImportError: + c_encode_basestring_ascii = None +try: + from simplejson._speedups import make_encoder as c_make_encoder +except ImportError: + c_make_encoder = None + +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +} +for i in range(0x20): + #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +# Assume this produces an infinity on all machines (probably not guaranteed) +INFINITY = float('1e66666') +FLOAT_REPR = repr + +def encode_basestring(s): + """Return a JSON representation of a Python string + + """ + def replace(match): + return ESCAPE_DCT[match.group(0)] + return '"' + ESCAPE.sub(replace, s) + '"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + #return '\\u{0:04x}'.format(n) + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii + +class JSONEncoder(object): + """Extensible JSON encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is true, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a non-negative integer, then JSON array + elements and object members will be pretty-printed with that + indent level. An indent level of 0 will only insert newlines. + None is the most compact representation. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + if default is not None: + self.default = default + self.encoding = encoding + + def default(self, o): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + + """ + raise TypeError(repr(o) + " is not JSON serializable") + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + return ''.join(chunks) + + def iterencode(self, o, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor- and/or + # platform-specific, so do tests which don't depend on the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys: + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot) + return _iterencode(o, 0) + +def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + ## HACK: hand-optimized bytecode; turn globals into locals + False=False, + True=True, + ValueError=ValueError, + basestring=basestring, + dict=dict, + float=float, + id=id, + int=int, + isinstance=isinstance, + list=list, + long=long, + str=str, + tuple=tuple, + ): + + def _iterencode_list(lst, _current_indent_level): + if not lst: + yield '[]' + return + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + buf = '[' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + else: + newline_indent = None + separator = _item_separator + first = True + for value in lst: + if first: + first = False + else: + buf = separator + if isinstance(value, basestring): + yield buf + _encoder(value) + elif value is None: + yield buf + 'null' + elif value is True: + yield buf + 'true' + elif value is False: + yield buf + 'false' + elif isinstance(value, (int, long)): + yield buf + str(value) + elif isinstance(value, float): + yield buf + _floatstr(value) + else: + yield buf + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield ']' + if markers is not None: + del markers[markerid] + + def _iterencode_dict(dct, _current_indent_level): + if not dct: + yield '{}' + return + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + item_separator = _item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + item_separator = _item_separator + first = True + if _sort_keys: + items = dct.items() + items.sort(key=lambda kv: kv[0]) + else: + items = dct.iteritems() + for key, value in items: + if isinstance(key, basestring): + pass + # JavaScript is weakly typed for these, so it makes sense to + # also allow them. Many encoders seem to do something like this. + elif isinstance(key, float): + key = _floatstr(key) + elif key is True: + key = 'true' + elif key is False: + key = 'false' + elif key is None: + key = 'null' + elif isinstance(key, (int, long)): + key = str(key) + elif _skipkeys: + continue + else: + raise TypeError("key " + repr(key) + " is not a string") + if first: + first = False + else: + yield item_separator + yield _encoder(key) + yield _key_separator + if isinstance(value, basestring): + yield _encoder(value) + elif value is None: + yield 'null' + elif value is True: + yield 'true' + elif value is False: + yield 'false' + elif isinstance(value, (int, long)): + yield str(value) + elif isinstance(value, float): + yield _floatstr(value) + else: + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield '}' + if markers is not None: + del markers[markerid] + + def _iterencode(o, _current_indent_level): + if isinstance(o, basestring): + yield _encoder(o) + elif o is None: + yield 'null' + elif o is True: + yield 'true' + elif o is False: + yield 'false' + elif isinstance(o, (int, long)): + yield str(o) + elif isinstance(o, float): + yield _floatstr(o) + elif isinstance(o, (list, tuple)): + for chunk in _iterencode_list(o, _current_indent_level): + yield chunk + elif isinstance(o, dict): + for chunk in _iterencode_dict(o, _current_indent_level): + yield chunk + else: + if markers is not None: + markerid = id(o) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = o + o = _default(o) + for chunk in _iterencode(o, _current_indent_level): + yield chunk + if markers is not None: + del markers[markerid] + + return _iterencode diff --git a/mitogen/ansible_mitogen/compat/simplejson/scanner.py b/mitogen/ansible_mitogen/compat/simplejson/scanner.py new file mode 100644 index 0000000..adbc6ec --- /dev/null +++ b/mitogen/ansible_mitogen/compat/simplejson/scanner.py @@ -0,0 +1,65 @@ +"""JSON token scanner +""" +import re +try: + from simplejson._speedups import make_scanner as c_make_scanner +except ImportError: + c_make_scanner = None + +__all__ = ['make_scanner'] + +NUMBER_RE = re.compile( + r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', + (re.VERBOSE | re.MULTILINE | re.DOTALL)) + +def py_make_scanner(context): + parse_object = context.parse_object + parse_array = context.parse_array + parse_string = context.parse_string + match_number = NUMBER_RE.match + encoding = context.encoding + strict = context.strict + parse_float = context.parse_float + parse_int = context.parse_int + parse_constant = context.parse_constant + object_hook = context.object_hook + + def _scan_once(string, idx): + try: + nextchar = string[idx] + except IndexError: + raise StopIteration + + if nextchar == '"': + return parse_string(string, idx + 1, encoding, strict) + elif nextchar == '{': + return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) + elif nextchar == '[': + return parse_array((string, idx + 1), _scan_once) + elif nextchar == 'n' and string[idx:idx + 4] == 'null': + return None, idx + 4 + elif nextchar == 't' and string[idx:idx + 4] == 'true': + return True, idx + 4 + elif nextchar == 'f' and string[idx:idx + 5] == 'false': + return False, idx + 5 + + m = match_number(string, idx) + if m is not None: + integer, frac, exp = m.groups() + if frac or exp: + res = parse_float(integer + (frac or '') + (exp or '')) + else: + res = parse_int(integer) + return res, m.end() + elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': + return parse_constant('NaN'), idx + 3 + elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': + return parse_constant('Infinity'), idx + 8 + elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': + return parse_constant('-Infinity'), idx + 9 + else: + raise StopIteration + + return _scan_once + +make_scanner = c_make_scanner or py_make_scanner diff --git a/mitogen/ansible_mitogen/connection.py b/mitogen/ansible_mitogen/connection.py new file mode 100644 index 0000000..5e08eb1 --- /dev/null +++ b/mitogen/ansible_mitogen/connection.py @@ -0,0 +1,1056 @@ +# 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 +from __future__ import unicode_literals + +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 ansible.utils.shlex + +import mitogen.core +import mitogen.fork +import mitogen.utils + +import ansible_mitogen.mixins +import ansible_mitogen.parsing +import ansible_mitogen.process +import ansible_mitogen.services +import ansible_mitogen.target +import ansible_mitogen.transport_config + + +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 C.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.ansible_ssh_timeout(), + 'ssh_args': spec.ssh_args(), + 'ssh_debug_level': spec.mitogen_ssh_debug_level(), + '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.ansible_ssh_timeout() or 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(), + 'connect_timeout': spec.ansible_ssh_timeout() or 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.ansible_ssh_timeout() or 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.ansible_ssh_timeout() or 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.ansible_ssh_timeout() or 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.ansible_ssh_timeout() or 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_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 = { + 'buildah': _connect_buildah, + 'docker': _connect_docker, + 'kubectl': _connect_kubectl, + 'jail': _connect_jail, + 'local': _connect_local, + 'lxc': _connect_lxc, + 'lxd': _connect_lxd, + 'machinectl': _connect_machinectl, + 'setns': _connect_setns, + 'ssh': _connect_ssh, + 'smart': _connect_ssh, # issue #548. + 'su': _connect_su, + 'sudo': _connect_sudo, + 'doas': _connect_doas, + '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. + 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 + + def __del__(self): + """ + Ansible cannot be trusted to always call close() e.g. the synchronize + action constructs a local connection like this. So provide a destructor + in the hopes of catching these cases. + """ + # https://github.com/dw/mitogen/issues/140 + self.close() + + 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: + 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): + 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. + """ + task_vars = self._get_task_vars() + if self.delegate_to_hostname is None: + if key in task_vars: + return 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] + if key in task_vars: + return 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): + 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 + 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 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=mitogen.utils.cast(list(stack)), + ) + except mitogen.core.CallError: + LOG.warning('Connection failed; stack configuration was:\n%s', + pprint.pformat(stack)) + raise + + if dct['msg']: + if dct['method_name'] in self.become_methods: + 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._play_context.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. + """ + if self.connected: + return + + inventory_name, stack = self._build_stack() + worker_model = ansible_mitogen.process.get_worker_model() + self.binding = worker_model.get_binding( + mitogen.utils.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. + """ + self._put_connection() + if self.binding: + self.binding.close() + self.binding = None + + reset_compat_msg = ( + 'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later' + ) + + def reset(self): + """ + Explicitly terminate the connection to the remote host. This discards + 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. + """ + 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 + ) + + # Clear out state in case we were ever connected. + self.close() + + inventory_name, stack = self._build_stack() + if self._play_context.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=mitogen.utils.cast(list(stack)), + ) + finally: + binding.close() + + # 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) + """ + emulate_tty = (not in_data and sudoable) + rc, stdout, stderr = self.get_chain().call( + ansible_mitogen.target.exec_command, + cmd=mitogen.utils.cast(cmd), + in_data=mitogen.utils.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. + """ + self._connect() + ansible_mitogen.target.transfer_file( + context=self.context, + # in_path may be AnsibleUnicode + in_path=mitogen.utils.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, + mitogen.utils.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. + """ + 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: + 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=mitogen.utils.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=in_path, + out_path=out_path + ) diff --git a/mitogen/ansible_mitogen/loaders.py b/mitogen/ansible_mitogen/loaders.py new file mode 100644 index 0000000..9ce6b1f --- /dev/null +++ b/mitogen/ansible_mitogen/loaders.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. + +""" +Stable names for PluginLoader instances across Ansible versions. +""" + +from __future__ import absolute_import + +__all__ = [ + 'action_loader', + 'connection_loader', + 'module_loader', + 'module_utils_loader', + 'shell_loader', + 'strategy_loader', +] + +try: + from ansible.plugins.loader import action_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 +except ImportError: # Ansible <2.4 + from ansible.plugins import action_loader + from ansible.plugins import connection_loader + from ansible.plugins import module_loader + from ansible.plugins import module_utils_loader + from ansible.plugins import shell_loader + from ansible.plugins import strategy_loader + + +# These are original, unwrapped implementations +action_loader__get = action_loader.get +connection_loader__get = connection_loader.get diff --git a/mitogen/ansible_mitogen/logging.py b/mitogen/ansible_mitogen/logging.py new file mode 100644 index 0000000..00a7018 --- /dev/null +++ b/mitogen/ansible_mitogen/logging.py @@ -0,0 +1,128 @@ +# 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 +import logging +import os + +import mitogen.core +import mitogen.utils + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + 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/mitogen/ansible_mitogen/mixins.py b/mitogen/ansible_mitogen/mixins.py new file mode 100644 index 0000000..cfdf838 --- /dev/null +++ b/mitogen/ansible_mitogen/mixins.py @@ -0,0 +1,428 @@ +# 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 +import logging +import os +import pwd +import random +import traceback + +try: + from shlex import quote as shlex_quote +except ImportError: + from pipes import quote as shlex_quote + +from ansible.module_utils._text import to_bytes +from ansible.parsing.utils.jsonify import jsonify + +import ansible +import ansible.constants +import ansible.plugins +import ansible.plugins.action + +import mitogen.core +import mitogen.select +import mitogen.utils + +import ansible_mitogen.connection +import ansible_mitogen.planner +import ansible_mitogen.target +from ansible.module_utils._text import to_text + +try: + from ansible.utils.unsafe_proxy import wrap_var +except ImportError: + from ansible.vars.unsafe_proxy import wrap_var + + +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__ + + 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, + mitogen.utils.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.__version__ > '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 isinstance(data, dict): + data = jsonify(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, 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._play_context.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, + mitogen.utils.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.__version__ > '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.__version__ > '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): + """ + 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 = {} + + 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) + + self._connection._connect() + result = ansible_mitogen.planner.invoke( + ansible_mitogen.planner.Invocation( + action=self, + connection=self._connection, + module_name=mitogen.core.to_text(module_name), + module_args=mitogen.utils.cast(module_args), + task_vars=task_vars, + templar=self._templar, + env=mitogen.utils.cast(env), + wrap_async=wrap_async, + timeout_secs=self.get_task_timeout_secs(), + ) + ) + + if tmp and ansible.__version__ < '2.5' and delete_remote_tmp: + # Built-in actions expected tmpdir to be cleaned up automatically + # on _execute_module(). + self._remove_tmp_path(tmp) + + return 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" + } + """ + 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) + + rc, stdout, stderr = self._connection.exec_command( + cmd=cmd, + in_data=in_data, + sudoable=sudoable, + mitogen_chdir=chdir, + ) + stdout_text = to_text(stdout, errors=encoding_errors) + + return { + 'rc': rc, + 'stdout': stdout_text, + 'stdout_lines': stdout_text.splitlines(), + 'stderr': stderr, + } diff --git a/mitogen/ansible_mitogen/module_finder.py b/mitogen/ansible_mitogen/module_finder.py new file mode 100644 index 0000000..89aa2be --- /dev/null +++ b/mitogen/ansible_mitogen/module_finder.py @@ -0,0 +1,157 @@ +# 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 +from __future__ import unicode_literals + +import collections +import imp +import os + +import mitogen.master + + +PREFIX = 'ansible.module_utils.' + + +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): + for level, modname_s, fromlist in mitogen.master.scan_code_imports(code): + for name in fromlist: + yield level, '%s.%s' % (modname_s, name) + if not fromlist: + yield level, modname_s + + +def scan(module_name, module_path, search_path): + 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/mitogen/ansible_mitogen/parsing.py b/mitogen/ansible_mitogen/parsing.py new file mode 100644 index 0000000..27fca7c --- /dev/null +++ b/mitogen/ansible_mitogen/parsing.py @@ -0,0 +1,76 @@ +# 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 +from __future__ import unicode_literals + +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/mitogen/ansible_mitogen/planner.py b/mitogen/ansible_mitogen/planner.py new file mode 100644 index 0000000..8febbdb --- /dev/null +++ b/mitogen/ansible_mitogen/planner.py @@ -0,0 +1,576 @@ +# 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 +from __future__ import unicode_literals + +import json +import logging +import os +import random + +from ansible.executor import module_common +import ansible.errors +import ansible.module_utils +import ansible.release +import mitogen.core +import mitogen.select + +import ansible_mitogen.loaders +import ansible_mitogen.parsing +import ansible_mitogen.target + + +LOG = logging.getLogger(__name__) +NO_METHOD_MSG = 'Mitogen: no invocation method found for: ' +NO_INTERPRETER_MSG = 'module (%s) is missing interpreter line' +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 + + 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() + + 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 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=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 original interpreter binary extracted from the script's + interpreter line, look up the associated `ansible_*_interpreter` + variable, render it and return it. + + :param str path: + Absolute UNIX path to original interpreter. + + :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. + """ + key = u'ansible_%s_interpreter' % os.path.basename(path).strip() + try: + template = self._inv.task_vars[key] + except KeyError: + return path + + return mitogen.utils.cast(self._inv.templar.template(template)) + + 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 + + return fragment, path.startswith('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 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 = b'from ansible.module_utils.' + + @classmethod + def detect(cls, path, source): + return cls.marker in source + + 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 + ]) + + 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=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 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, +] + + +try: + _get_ansible_module_fqn = module_common._get_ansible_module_fqn +except AttributeError: + _get_ansible_module_fqn = None + + +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. + + * For Ansible <2.7, this is an unpackaged module named like + "ansible_module_%s". + + * For Ansible <2.9, this is an unpackaged module named like + "ansible.modules.%s" + + * 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". + """ + # 2.9+ + if _get_ansible_module_fqn: + try: + return _get_ansible_module_fqn(path) + except ValueError: + pass + + if ansible.__version__ < '2.7': + return 'ansible_module_' + name + + 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 mitogen.core.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(), + ) + + +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=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(name, path, source): + for klass in _planners: + if klass.detect(path, source): + LOG.debug('%r accepted %r (filename %r)', klass, name, path) + return klass + LOG.debug('%r rejected %r', klass, name) + raise ansible.errors.AnsibleError(NO_METHOD_MSG + repr(invocation)) + + +def invoke(invocation): + """ + Find a Planner subclass corresnding 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: + _planner_by_path[invocation.module_path] = _get_planner( + invocation.module_name, + invocation.module_path, + invocation.get_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/mitogen/ansible_mitogen/plugins/__init__.py b/mitogen/ansible_mitogen/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mitogen/ansible_mitogen/plugins/action/__init__.py b/mitogen/ansible_mitogen/plugins/action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mitogen/ansible_mitogen/plugins/action/mitogen_fetch.py b/mitogen/ansible_mitogen/plugins/action/mitogen_fetch.py new file mode 100644 index 0000000..1844efd --- /dev/null +++ b/mitogen/ansible_mitogen/plugins/action/mitogen_fetch.py @@ -0,0 +1,162 @@ +# (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 + +from ansible.module_utils._text import to_bytes +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.hashing import checksum, md5, secure_hash +from ansible.utils.path import makedirs_safe + + +REMOTE_CHECKSUM_ERRORS = { + '0': "unable to calculate the checksum of the remote file", + '1': "the remote file does not exist", + '2': "no read permission on remote file", + '3': "remote file is a directory, fetch cannot work on directories", + '4': "python isn't present on the system. Unable to compute checksum", + '5': "stdlib json was not found on the remote machine. Only the raw module can work without those installed", +} + + +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) + try: + if self._play_context.check_mode: + result['skipped'] = True + result['msg'] = 'check mode not (yet) supported for this module' + return result + + 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) + + # validate source and dest are strings FIXME: use basic.py and module specs + source = self._task.args.get('src') + if not isinstance(source, string_types): + result['msg'] = "Invalid type supplied for source option, it must be a string" + + dest = self._task.args.get('dest') + if not isinstance(dest, string_types): + result['msg'] = "Invalid type supplied for dest option, it must be a string" + + if result.get('msg'): + result['failed'] = True + return result + + source = self._connection._shell.join_path(source) + source = self._remote_expand_user(source) + + # calculate checksum for the remote file, don't bother if using + # become as slurp will be used Force remote_checksum to follow + # symlinks because fetch always follows symlinks + remote_checksum = self._remote_checksum(source, all_vars=task_vars, follow=True) + + # 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 + + dest = os.path.expanduser(dest) + if flat: + if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict')) and not dest.endswith(os.sep): + result['msg'] = "dest is an existing directory, use a trailing slash if you want to fetch src into that directory" + result['file'] = dest + result['failed'] = True + return result + 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 = dest.replace("//", "/") + + if remote_checksum in REMOTE_CHECKSUM_ERRORS: + result['changed'] = False + result['file'] = source + result['msg'] = REMOTE_CHECKSUM_ERRORS[remote_checksum] + # 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 fail_on_missing: + result['failed'] = True + del result['changed'] + else: + result['msg'] += ", not transferring, ignored" + return result + + # 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 + self._connection.fetch_file(source, dest) + 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/mitogen/ansible_mitogen/plugins/action/mitogen_get_stack.py b/mitogen/ansible_mitogen/plugins/action/mitogen_get_stack.py new file mode 100644 index 0000000..171f84e --- /dev/null +++ b/mitogen/ansible_mitogen/plugins/action/mitogen_get_stack.py @@ -0,0 +1,55 @@ +# 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 +from __future__ import unicode_literals + +""" +Fetch the connection configuration stack that would be used to connect to a +target, without actually connecting to it. +""" + +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, + } diff --git a/mitogen/ansible_mitogen/plugins/connection/__init__.py b/mitogen/ansible_mitogen/plugins/connection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mitogen/ansible_mitogen/plugins/connection/__pycache__/mitogen_ssh.cpython-36.pyc b/mitogen/ansible_mitogen/plugins/connection/__pycache__/mitogen_ssh.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebd2eb47f9bca5654eb429c4b1163280476f6f9a GIT binary patch literal 1422 zcmZuxL5~|X6t-t(C$p1eTM7b%gs^%d5=h56|6$T;88GDn-PV8Vi zDOvTFUg6AN;74%cU--(2BUes5dy;ObI*FY(_Vat+_r2$Q+VAK8{Q1X^Do5xqwDa`9 zK7wI>gNY-K8#Kcu#@>#asEosJQYJhu_g1~|mX_&LB)>?I=v?JmV&p#}o@Ud1k*{&t z6$#kj8`k<^J`i1=tfMtvM;QIk{T|8ayEezIG7V90m+4oC@A32n;;BfdZ%ps3~67T6+f&9cLOFqlE$l?;L-L3%YfhfljZu=^%*x^ASt}m6)S4;<${V0rsXT$2{WkFHzYM z8Bh4$3k1!v&r@Gemv2Oc#4ZMm7f{{2YM3!p$!7I3PXwW37-v4u0ZbN6@icmlp5tFJ zM8ek(zEt`d)11&s3kI#R5tPoEoqRYTRs}tvVHz<5!I>{Asijw@n6HXKtbGF4-zY`{ z3isCQ!VMyqFg-Rdy@Hfu^E=XTqtL(K3@2(PhIV+YxEL<^)o{~RL)KP}Zr|HdnBk@^ z0Ls?YcOc-=9SHaUhVj%OgpT9pTHdW4q?yU>78dy1~3OyO(gB4wZ#R>t)oogIJsL zf2_6AE(!h}UTaUz)4!|(ctIV_s*+(TUGKK pr@(H9vFE5gZI|6^g}?tkyqR}l>fsD$y(Efo4 (3,): + viewkeys = dict.keys +elif sys.version_info > (2, 7): + viewkeys = dict.viewkeys +else: + viewkeys = lambda dct: set(dct) + + +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/mitogen/ansible_mitogen/plugins/connection/mitogen_lxc.py b/mitogen/ansible_mitogen/plugins/connection/mitogen_lxc.py new file mode 100644 index 0000000..696c9ab --- /dev/null +++ b/mitogen/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 +import os.path +import sys + +try: + import ansible_mitogen +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'lxc' diff --git a/mitogen/ansible_mitogen/plugins/connection/mitogen_lxd.py b/mitogen/ansible_mitogen/plugins/connection/mitogen_lxd.py new file mode 100644 index 0000000..95e692a --- /dev/null +++ b/mitogen/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 +import os.path +import sys + +try: + import ansible_mitogen +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'lxd' diff --git a/mitogen/ansible_mitogen/plugins/connection/mitogen_machinectl.py b/mitogen/ansible_mitogen/plugins/connection/mitogen_machinectl.py new file mode 100644 index 0000000..0f5a0d2 --- /dev/null +++ b/mitogen/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 +import os.path +import sys + +try: + import ansible_mitogen.connection +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'machinectl' diff --git a/mitogen/ansible_mitogen/plugins/connection/mitogen_setns.py b/mitogen/ansible_mitogen/plugins/connection/mitogen_setns.py new file mode 100644 index 0000000..20c6f13 --- /dev/null +++ b/mitogen/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 +import os.path +import sys + +try: + import ansible_mitogen.connection +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'setns' diff --git a/mitogen/ansible_mitogen/plugins/connection/mitogen_ssh.py b/mitogen/ansible_mitogen/plugins/connection/mitogen_ssh.py new file mode 100644 index 0000000..1c81dae --- /dev/null +++ b/mitogen/ansible_mitogen/plugins/connection/mitogen_ssh.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 +import os.path +import sys + +DOCUMENTATION = """ + author: David Wilson + connection: mitogen_ssh + 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. + version_added: "2.5" + options: +""" + +try: + import ansible_mitogen +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection +import ansible_mitogen.loaders + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'ssh' + vanilla_class = ansible_mitogen.loaders.connection_loader__get( + '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/mitogen/ansible_mitogen/plugins/connection/mitogen_su.py b/mitogen/ansible_mitogen/plugins/connection/mitogen_su.py new file mode 100644 index 0000000..4ab2711 --- /dev/null +++ b/mitogen/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 +import os.path +import sys + +try: + import ansible_mitogen.connection +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'mitogen_su' diff --git a/mitogen/ansible_mitogen/plugins/connection/mitogen_sudo.py b/mitogen/ansible_mitogen/plugins/connection/mitogen_sudo.py new file mode 100644 index 0000000..130f544 --- /dev/null +++ b/mitogen/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 +import os.path +import sys + +try: + import ansible_mitogen.connection +except ImportError: + base_dir = os.path.dirname(__file__) + sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..'))) + del base_dir + +import ansible_mitogen.connection + + +class Connection(ansible_mitogen.connection.Connection): + transport = 'mitogen_sudo' diff --git a/mitogen/ansible_mitogen/plugins/strategy/__init__.py b/mitogen/ansible_mitogen/plugins/strategy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mitogen/ansible_mitogen/plugins/strategy/__pycache__/mitogen_linear.cpython-36.pyc b/mitogen/ansible_mitogen/plugins/strategy/__pycache__/mitogen_linear.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa553029c6c8644ae2636539f7e089629313f3e1 GIT binary patch literal 734 zcmZ8ev69m;5S3&nR!p35;ec!SfHZbN%Q43R!!Qh2T&Og|XnZGK&OX@+lFbm>(87Q4 z5meOtfLba_S}N8`JTTEnPj7dn)!Ws>B#D22|MvDrjL=W?-xvTtfn~n{2qdsV6=oQB zoG8KqzlSW8K{+Ug9wU!bjy6~X8?wPHmaz=W!A)e&yS=<6kc>BEmu1WnpJl=lnUvG= zc6lH|F%ZMggw0+c5efPU2@0`0<)NHyLUvb>1)7h3fhIJ^Zkk_Mt=dNNqFQOwoPDHe zn)%6ws;Fgd-oUZ=QsvfiU90tFce5WD$O$Yv29Sthffx}OULbC=XiVP7<@&W2t&(T+ zz){ZYypo)|nDffJfsZ+V*XC*)`HBpMTps?s%C2=KvnG40h0NZIo2)7ty_9v9*S5G& zlK1vXwacQmnccnFALQE_($(68koLIKN&qPK_WWL_J) literal 0 HcmV?d00001 diff --git a/mitogen/ansible_mitogen/plugins/strategy/mitogen.py b/mitogen/ansible_mitogen/plugins/strategy/mitogen.py new file mode 100644 index 0000000..6687266 --- /dev/null +++ b/mitogen/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 +import os.path +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. +# + +BASE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../..') +) + +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +import ansible_mitogen.strategy +import ansible.plugins.strategy.linear + + +class StrategyModule(ansible_mitogen.strategy.StrategyMixin, + ansible.plugins.strategy.linear.StrategyModule): + pass diff --git a/mitogen/ansible_mitogen/plugins/strategy/mitogen_free.py b/mitogen/ansible_mitogen/plugins/strategy/mitogen_free.py new file mode 100644 index 0000000..ffe2fbd --- /dev/null +++ b/mitogen/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 +import os.path +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. +# + +BASE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../..') +) + +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +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/mitogen/ansible_mitogen/plugins/strategy/mitogen_host_pinned.py b/mitogen/ansible_mitogen/plugins/strategy/mitogen_host_pinned.py new file mode 100644 index 0000000..23eccd3 --- /dev/null +++ b/mitogen/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 +import os.path +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. +# + +BASE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../..') +) + +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +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/mitogen/ansible_mitogen/plugins/strategy/mitogen_linear.py b/mitogen/ansible_mitogen/plugins/strategy/mitogen_linear.py new file mode 100644 index 0000000..1b198e6 --- /dev/null +++ b/mitogen/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 +import os.path +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. +# + +BASE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../..') +) + +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +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/mitogen/ansible_mitogen/process.py b/mitogen/ansible_mitogen/process.py new file mode 100644 index 0000000..1fc7bf8 --- /dev/null +++ b/mitogen/ansible_mitogen/process.py @@ -0,0 +1,745 @@ +# 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 +import atexit +import logging +import multiprocessing +import os +import resource +import socket +import signal +import sys + +try: + import faulthandler +except ImportError: + 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 + +from mitogen.core import b +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_simplejson(responder): + """ + We support serving simplejson for Python 2.4 targets on Ansible 2.3, at + least so the package's own CI Docker scripts can run without external + help, however newer versions of simplejson no longer support Python + 2.4. Therefore override any installed/loaded version with a + 2.4-compatible version we ship in the compat/ directory. + """ + responder.whitelist_prefix('simplejson') + + # issue #536: must be at end of sys.path, in case existing newer + # version is already loaded. + compat_path = os.path.join(os.path.dirname(__file__), 'compat') + sys.path.append(compat_path) + + for fullname, is_pkg, suffix in ( + (u'simplejson', True, '__init__.py'), + (u'simplejson.decoder', False, 'decoder.py'), + (u'simplejson.encoder', False, 'encoder.py'), + (u'simplejson.scanner', False, 'scanner.py'), + ): + path = os.path.join(compat_path, 'simplejson', suffix) + fp = open(path, 'rb') + try: + source = fp.read() + finally: + fp.close() + + responder.add_source_override( + fullname=fullname, + path=path, + source=source, + is_pkg=is_pkg, + ) + + +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_mitogen') + _setup_simplejson(responder) + + # 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 at most 2 file descriptors, therefore does not need + the exuberant syscall expense of EpollPoller, so override it and restore + the poll() poller. + """ + poller_class = mitogen.core.Poller + + +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 = socket.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/mitogen/ansible_mitogen/runner.py b/mitogen/ansible_mitogen/runner.py new file mode 100644 index 0000000..0640234 --- /dev/null +++ b/mitogen/ansible_mitogen/runner.py @@ -0,0 +1,1020 @@ +# 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. +""" + +import atexit +import imp +import os +import re +import shlex +import shutil +import sys +import tempfile +import traceback +import types + +import mitogen.core +import ansible_mitogen.target # TODO: circular import +from mitogen.core import b +from mitogen.core import bytes_partition +from mitogen.core import str_rpartition +from mitogen.core import to_text + +try: + import ctypes +except ImportError: + # Python 2.4 + ctypes = None + +try: + import json +except ImportError: + # Python 2.4 + import simplejson as json + +try: + # Cannot use cStringIO as it does not support Unicode. + from StringIO import StringIO +except ImportError: + from io import StringIO + +try: + from shlex import quote as shlex_quote +except ImportError: + from pipes import quote as shlex_quote + +# Absolute imports for <2.5. +logging = __import__('logging') + + +# Prevent accidental import of an Ansible module from hanging on stdin read. +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 +if ctypes: + libc = ctypes.CDLL(None) + for symbol in 'res_init', '__res_init': + try: + libc__res_init = getattr(libc, symbol) + except AttributeError: + pass + +iteritems = getattr(dict, 'iteritems', dict.items) +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 mitogen.core.PY3: + 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 = bytes_partition(b(' ').join(bits), 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): + if fullname in self._by_fullname: + return self + + def load_module(self, fullname): + 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(str_rpartition(to_text(fullname), '.')[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 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) + 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 + ansible.module_utils.basic._ANSIBLE_ARGS = '{}' + + +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 = bytes_partition(s, 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. + """ + for fullname, _, _ in self.module_map['custom']: + mitogen.core.import_module(fullname) + for fullname in self.module_map['builtin']: + try: + mitogen.core.import_module(fullname) + except ImportError: + # #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': + raise + + 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_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( + b(r'reload\s*\(\s*sys\s*\)\s*' + r'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 mitogen.core.PY3: + 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 mitogen.core.PY3: + 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, modname = str_rpartition(self.py_module_name, '.') + if not sep: + return None + if mitogen.core.PY3: + 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/mitogen/ansible_mitogen/services.py b/mitogen/ansible_mitogen/services.py new file mode 100644 index 0000000..5217190 --- /dev/null +++ b/mitogen/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 +from __future__ import unicode_literals + +import logging +import os +import os.path +import sys +import threading + +import ansible.constants + +import mitogen +import mitogen.service +import mitogen.utils +import ansible_mitogen.loaders +import ansible_mitogen.module_finder +import ansible_mitogen.target + + +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') + + +if sys.version_info[0] == 3: + def reraise(tp, value, tb): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value +else: + exec( + "def reraise(tp, value, tb=None):\n" + " raise tp, value, tb\n" + ) + + +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 mitogen.utils.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) + + 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', + ) + + 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/mitogen/ansible_mitogen/strategy.py b/mitogen/ansible_mitogen/strategy.py new file mode 100644 index 0000000..d82e611 --- /dev/null +++ b/mitogen/ansible_mitogen/strategy.py @@ -0,0 +1,373 @@ +# 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 +import distutils.version +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.mixins +import ansible_mitogen.process + +import ansible +import ansible.executor.process.worker + +try: + # 2.8+ has a standardized "unset" object. + from ansible.utils.sentinel import Sentinel +except ImportError: + Sentinel = None + + +ANSIBLE_VERSION_MIN = (2, 3) +ANSIBLE_VERSION_MAX = (2, 9) +NEW_VERSION_MSG = ( + "Your Ansible version (%s) is too recent. The most recent version\n" + "supported by Mitogen for Ansible is %s.x. Please check the Mitogen\n" + "release notes to see if a new version is available, otherwise\n" + "subscribe to the corresponding GitHub issue to be notified when\n" + "support becomes available.\n" + "\n" + " https://mitogen.rtfd.io/en/latest/changelog.html\n" + " https://github.com/dw/mitogen/issues/\n" +) +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.__version__ + if not isinstance(v, tuple): + v = tuple(distutils.version.LooseVersion(v).version) + + if v[:2] < ANSIBLE_VERSION_MIN: + raise ansible.errors.AnsibleError( + OLD_VERSION_MSG % (v, ANSIBLE_VERSION_MIN) + ) + + if v[:2] > ANSIBLE_VERSION_MAX: + raise ansible.errors.AnsibleError( + NEW_VERSION_MSG % (ansible.__version__, ANSIBLE_VERSION_MAX) + ) + + +def _patch_awx_callback(): + """ + issue #400: AWX loads a display callback that suffers from thread-safety + issues. Detect the presence of older AWX versions and patch the bug. + """ + # AWX uses sitecustomize.py to force-load this package. If it exists, we're + # running under AWX. + try: + from awx_display_callback.events import EventContext + from awx_display_callback.events import event_context + except ImportError: + return + + if hasattr(EventContext(), '_local'): + # Patched version. + return + + def patch_add_local(self, **kwargs): + tls = vars(self._local) + ctx = tls.setdefault('_ctx', {}) + ctx.update(kwargs) + + EventContext._local = threading.local() + EventContext.add_local = patch_add_local + +_patch_awx_callback() + + +def wrap_action_loader__get(name, *args, **kwargs): + """ + While the mitogen strategy is active, trap action_loader.get() calls, + 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 + if ansible.__version__ >= '2.8': + get_kwargs['collection_list'] = kwargs.pop('collection_list', None) + + klass = ansible_mitogen.loaders.action_loader__get(name, **get_kwargs) + if klass: + bases = (ansible_mitogen.mixins.ActionModuleMixin, klass) + adorned_klass = type(str(name), bases, {}) + if kwargs.get('class_only'): + return adorned_klass + return adorned_klass(*args, **kwargs) + + +REDIRECTED_CONNECTION_PLUGINS = ( + 'buildah', + 'docker', + 'kubectl', + 'jail', + 'local', + 'lxc', + 'lxd', + 'machinectl', + 'setns', + 'ssh', +) + + +def wrap_connection_loader__get(name, *args, **kwargs): + """ + While a Mitogen strategy is active, rewrite connection_loader.get() calls + for some transports into requests for a compatible Mitogen transport. + """ + if name in REDIRECTED_CONNECTION_PLUGINS: + name = 'mitogen_' + name + + return ansible_mitogen.loaders.connection_loader__get(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 = wrap_action_loader__get + ansible_mitogen.loaders.connection_loader.get = wrap_connection_loader__get + + 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 = ( + ansible_mitogen.loaders.action_loader__get + ) + ansible_mitogen.loaders.connection_loader.get = ( + ansible_mitogen.loaders.connection_loader__get + ) + 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 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. + """ + _assert_supported_release() + 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) diff --git a/mitogen/ansible_mitogen/target.py b/mitogen/ansible_mitogen/target.py new file mode 100644 index 0000000..652b5ad --- /dev/null +++ b/mitogen/ansible_mitogen/target.py @@ -0,0 +1,777 @@ +# 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. +""" + +import errno +import grp +import operator +import os +import pwd +import re +import signal +import stat +import subprocess +import sys +import tempfile +import traceback +import types + +# Absolute imports for <2.5. +logging = __import__('logging') + +import mitogen.core +import mitogen.fork +import mitogen.parent +import mitogen.service +from mitogen.core import b + +try: + import json +except ImportError: + import simplejson as json + +try: + reduce +except NameError: + # Python 3.x. + from functools import reduce + +try: + BaseException +except NameError: + # Python 2.4 + BaseException = Exception + + +# 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 > 2 and fd != but: + try: + os.close(fd) + except OSError: + pass + + +if ( + sys.platform.startswith(u'linux') and + sys.version < u'3.0' 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) + + # issue #536: if the json module is available, remove simplejson from the + # importer whitelist to avoid confusing certain Ansible modules. + if json.__name__ == 'json': + econtext.importer.whitelist.remove('simplejson') + + 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. + """ + return open(path, 'rb').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 and hasattr(os, 'fchown'): + os.fchown(fd, (uid, gid)) + else: + # Python<2.6 + 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 = reduce(operator.or_, (bits[p] for p in perms), 0) + 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, int): + new_mode = spec + elif not mitogen.core.PY3 and isinstance(spec, long): + 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 and hasattr(os, 'fchmod'): + 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/mitogen/ansible_mitogen/transport_config.py b/mitogen/ansible_mitogen/transport_config.py new file mode 100644 index 0000000..aa4a16d --- /dev/null +++ b/mitogen/ansible_mitogen/transport_config.py @@ -0,0 +1,699 @@ +# 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 +from __future__ import unicode_literals + +""" +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. +""" + +import abc +import os +import ansible.utils.shlex +import ansible.constants as C + +from ansible.module_utils.six import with_metaclass + + +import mitogen.core + + +def parse_python_path(s): + """ + Given the string set for ansible_python_interpeter, parse it using shell + syntax and return an appropriate argument vector. + """ + if s: + 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_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 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 become + invocation. + """ + # TODO: split out into sudo_args/become_args. + + @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_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_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. + """ + + +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 + + def transport(self): + return self._transport + + def inventory_name(self): + return self._inventory_name + + def remote_addr(self): + return self._play_context.remote_addr + + def remote_user(self): + return self._play_context.remote_user + + def become(self): + return self._play_context.become + + def become_method(self): + return self._play_context.become_method + + def become_user(self): + return self._play_context.become_user + + def become_pass(self): + return optional_secret(self._play_context.become_pass) + + def password(self): + return optional_secret(self._play_context.password) + + def port(self): + return self._play_context.port + + def python_path(self): + s = self._connection.get_task_var('ansible_python_interpreter') + # #511, #536: executor/module_common.py::_get_shebang() hard-wires + # "/usr/bin/python" as the default interpreter path if no other + # interpreter is specified. + return parse_python_path(s or '/usr/bin/python') + + def private_key_file(self): + return self._play_context.private_key_file + + def ssh_executable(self): + return self._play_context.ssh_executable + + def timeout(self): + return self._play_context.timeout + + def ansible_ssh_timeout(self): + return ( + self._connection.get_task_var('ansible_timeout') or + self._connection.get_task_var('ansible_ssh_timeout') or + self.timeout() + ) + + def ssh_args(self): + return [ + mitogen.core.to_text(term) + for s in ( + getattr(self._play_context, 'ssh_args', ''), + getattr(self._play_context, 'ssh_common_args', ''), + getattr(self._play_context, 'ssh_extra_args', '') + ) + for term in ansible.utils.shlex.shlex_split(s or '') + ] + + def become_exe(self): + # In Ansible 2.8, PlayContext.become_exe always has a default value due + # to the new options mechanism. Previously it was only set if a value + # ("somewhere") had been specified for the task. + # For consistency in the tests, here we make older Ansibles behave like + # newer Ansibles. + exe = self._play_context.become_exe + if exe is None and self._play_context.become_method == 'sudo': + exe = 'sudo' + return exe + + def sudo_args(self): + return [ + mitogen.core.to_text(term) + for term in ansible.utils.shlex.shlex_split( + first_true(( + self._play_context.become_flags, + # Ansible <=2.7. + getattr(self._play_context, 'sudo_flags', ''), + # Ansible <=2.3. + getattr(C, 'DEFAULT_BECOME_FLAGS', ''), + getattr(C, 'DEFAULT_SUDO_FLAGS', '') + ), default='') + ) + ] + + 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_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_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') + ) + + +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, become_method, become_user, + play_context): + """ + :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 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. + """ + self._inventory_name = inventory_name + self._host_vars = host_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 + + 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_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_password') or + self._host_vars.get('ansible_become_pass') + ) + + def password(self): + return optional_secret( + 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): + s = self._host_vars.get('ansible_python_interpreter') + # #511, #536: executor/module_common.py::_get_shebang() hard-wires + # "/usr/bin/python" as the default interpreter path if no other + # interpreter is specified. + return parse_python_path(s or '/usr/bin/python') + + 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 ( + self._host_vars.get('ansible_ssh_executable') or + C.ANSIBLE_SSH_EXECUTABLE + ) + + 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): + return [ + mitogen.core.to_text(term) + for s in ( + ( + self._host_vars.get('ansible_ssh_args') or + getattr(C, 'ANSIBLE_SSH_ARGS', None) or + os.environ.get('ANSIBLE_SSH_ARGS') + # TODO: ini entry. older versions. + ), + ( + self._host_vars.get('ansible_ssh_common_args') or + os.environ.get('ANSIBLE_SSH_COMMON_ARGS') + # TODO: ini entry. + ), + ( + self._host_vars.get('ansible_ssh_extra_args') or + os.environ.get('ANSIBLE_SSH_EXTRA_ARGS') + # TODO: ini entry. + ), + ) + 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._host_vars.get('ansible_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_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_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') + ) diff --git a/mitogen/mitogen.egg-info/PKG-INFO b/mitogen/mitogen.egg-info/PKG-INFO new file mode 100644 index 0000000..3346b33 --- /dev/null +++ b/mitogen/mitogen.egg-info/PKG-INFO @@ -0,0 +1,23 @@ +Metadata-Version: 1.1 +Name: mitogen +Version: 0.2.9 +Summary: Library for writing distributed self-replicating programs. +Home-page: https://github.com/dw/mitogen/ +Author: David Wilson +Author-email: UNKNOWN +License: New BSD +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Environment :: Console +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.4 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Topic :: System :: Distributed Computing +Classifier: Topic :: System :: Systems Administration diff --git a/mitogen/mitogen.egg-info/SOURCES.txt b/mitogen/mitogen.egg-info/SOURCES.txt new file mode 100644 index 0000000..afaccd9 --- /dev/null +++ b/mitogen/mitogen.egg-info/SOURCES.txt @@ -0,0 +1,81 @@ +LICENSE +MANIFEST.in +README.md +setup.cfg +setup.py +ansible_mitogen/__init__.py +ansible_mitogen/affinity.py +ansible_mitogen/connection.py +ansible_mitogen/loaders.py +ansible_mitogen/logging.py +ansible_mitogen/mixins.py +ansible_mitogen/module_finder.py +ansible_mitogen/parsing.py +ansible_mitogen/planner.py +ansible_mitogen/process.py +ansible_mitogen/runner.py +ansible_mitogen/services.py +ansible_mitogen/strategy.py +ansible_mitogen/target.py +ansible_mitogen/transport_config.py +ansible_mitogen/compat/__init__.py +ansible_mitogen/compat/simplejson/__init__.py +ansible_mitogen/compat/simplejson/decoder.py +ansible_mitogen/compat/simplejson/encoder.py +ansible_mitogen/compat/simplejson/scanner.py +ansible_mitogen/plugins/__init__.py +ansible_mitogen/plugins/action/__init__.py +ansible_mitogen/plugins/action/mitogen_fetch.py +ansible_mitogen/plugins/action/mitogen_get_stack.py +ansible_mitogen/plugins/connection/__init__.py +ansible_mitogen/plugins/connection/mitogen_buildah.py +ansible_mitogen/plugins/connection/mitogen_doas.py +ansible_mitogen/plugins/connection/mitogen_docker.py +ansible_mitogen/plugins/connection/mitogen_jail.py +ansible_mitogen/plugins/connection/mitogen_kubectl.py +ansible_mitogen/plugins/connection/mitogen_local.py +ansible_mitogen/plugins/connection/mitogen_lxc.py +ansible_mitogen/plugins/connection/mitogen_lxd.py +ansible_mitogen/plugins/connection/mitogen_machinectl.py +ansible_mitogen/plugins/connection/mitogen_setns.py +ansible_mitogen/plugins/connection/mitogen_ssh.py +ansible_mitogen/plugins/connection/mitogen_su.py +ansible_mitogen/plugins/connection/mitogen_sudo.py +ansible_mitogen/plugins/strategy/__init__.py +ansible_mitogen/plugins/strategy/mitogen.py +ansible_mitogen/plugins/strategy/mitogen_free.py +ansible_mitogen/plugins/strategy/mitogen_host_pinned.py +ansible_mitogen/plugins/strategy/mitogen_linear.py +mitogen/__init__.py +mitogen/buildah.py +mitogen/core.py +mitogen/debug.py +mitogen/doas.py +mitogen/docker.py +mitogen/fakessh.py +mitogen/fork.py +mitogen/jail.py +mitogen/kubectl.py +mitogen/lxc.py +mitogen/lxd.py +mitogen/master.py +mitogen/minify.py +mitogen/os_fork.py +mitogen/parent.py +mitogen/profiler.py +mitogen/select.py +mitogen/service.py +mitogen/setns.py +mitogen/ssh.py +mitogen/su.py +mitogen/sudo.py +mitogen/unix.py +mitogen/utils.py +mitogen.egg-info/PKG-INFO +mitogen.egg-info/SOURCES.txt +mitogen.egg-info/dependency_links.txt +mitogen.egg-info/not-zip-safe +mitogen.egg-info/top_level.txt +mitogen/compat/__init__.py +mitogen/compat/pkgutil.py +mitogen/compat/tokenize.py \ No newline at end of file diff --git a/mitogen/mitogen.egg-info/dependency_links.txt b/mitogen/mitogen.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mitogen/mitogen.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/mitogen/mitogen.egg-info/not-zip-safe b/mitogen/mitogen.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mitogen/mitogen.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/mitogen/mitogen.egg-info/top_level.txt b/mitogen/mitogen.egg-info/top_level.txt new file mode 100644 index 0000000..2360b3f --- /dev/null +++ b/mitogen/mitogen.egg-info/top_level.txt @@ -0,0 +1,2 @@ +ansible_mitogen +mitogen diff --git a/mitogen/mitogen/__init__.py b/mitogen/mitogen/__init__.py new file mode 100644 index 0000000..f18c5a9 --- /dev/null +++ b/mitogen/mitogen/__init__.py @@ -0,0 +1,120 @@ +# 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, 2, 9) + + +#: 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.parent + 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/mitogen/mitogen/__pycache__/__init__.cpython-36.pyc b/mitogen/mitogen/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d78a510b264abbb2e04931e65509e8ce2cf92f42 GIT binary patch literal 2192 zcmZ`)O>f&a7^ZCbOIfnE+khR3T@nL6_m_j^koDied^Ji&KlRywk7gY< zWTW>%60zP7XuNazTXZ6^U2yzTST*Mo7t~m;4`EFh?EXcmwB-!Xv?_3#7R|m1AwmjE zNL*f|DW(RWt14&F8OQuh$ul?+<5slE>6#y6VV%6QO5_%^3rP!+(LBGvtkOcxv9VO! zs*Iyg;2rwZam@&t)8pjs%cIki=TDExtJf#bj$RxcKd-~1<7X$C+vFaH%=K&T!Q=z_ z5P+`%>%1Sn?Ob)h*DkF>gdz;j6D8MN3NAB_8PAlaR%u*nQBWIK14l>=Z@Ybxj5ctgB|1Fb5E&N#(ZVSrl5wOTo0MZuY%mgg6sP}qf+ zGc7NsSx$|aUPmo%jEi4;F)7ZJG*(xc6-r_%8CbWSFJK`D;mfLZ7lI5{JE_i_tqv4-H?>x8A-aWoyit-m(gDjrMuXZ$#LdXW_|zq1 zMuC?GQgC965^~PDU|J$%nnUy%2FUaUFb=?=4M!bxF26ZMSIscRrpg>K9l;@Fo-N)# zPsHq%_n)U27*bZ&Q_lKdUEw-ttv$s13u{X=olJOxMy&LFvOjiD zV_FutA_b4DS5Sn|7x0=Np}9g!#DY~@h=qQ2=L)^+LbdB1*Td_oqMiD2Q1^VwTn=ejdVt2C z)e-FR)rOE{?;1QvLm+mtdp-C0U58L7d2)wKR|Cn0!gaslcG7Q(NNO{KZsc6O z+aBd)q0}n*{6A%5+^IuXC+b0~9TrBLYM>+VD5qh$k>IF3@OyZ%=;-JsfvY(2fDxT~*_qdO!$M86ou!A&|EQN+X0} zBn;dJ?h;NSr~;1QM6hJkM8yrG&Ct{xW$FQ!YoV2_yFkh09s#z~D!1Dhc?-O~HyUs4 b@Ie#)N8f-+{^ktBupjpPuR9Fx1fA}`T|kXb literal 0 HcmV?d00001 diff --git a/mitogen/mitogen/__pycache__/core.cpython-36.pyc b/mitogen/mitogen/__pycache__/core.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdf5204be51b0baeee16dd76d20bd66437f852f6 GIT binary patch literal 121555 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 literal 0 HcmV?d00001 diff --git a/mitogen/mitogen/__pycache__/debug.cpython-36.pyc b/mitogen/mitogen/__pycache__/debug.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce95f59cc7993a6713b36a6c30a884528f1e4a0d GIT binary patch literal 6318 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⁣%?yq%onXCi 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 literal 0 HcmV?d00001 diff --git a/mitogen/mitogen/__pycache__/select.cpython-36.pyc b/mitogen/mitogen/__pycache__/select.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..561df4719e3f1810bdbd2d86240aa1ce87157a18 GIT binary patch literal 10518 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 literal 0 HcmV?d00001 diff --git a/mitogen/mitogen/__pycache__/ssh.cpython-36.pyc b/mitogen/mitogen/__pycache__/ssh.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14f51363a1af66001a32811873b9d67f90b62f95 GIT binary patch literal 7030 zcma)A%X1vZd7szr&OWht5FquSMlz{rEeVjUB#seEB1M7}EOG$@kR`1|jt1L}#VmGb zR^79pFj?hbu*yE99Oo4Kmdeey+s#K*)H|H3g^80#b2Mdt4vs?2`_t%f< z@9XdVPQ70FpMU@T!{5E8Y5%3o{FczZiAR2^YZ}u!8q=9E&^x+%8y(}MiIzDqhgPSc zYoBO=V`;p2R1tS{js9x1l#YI;1;(f84tA%E5o=HxRy$Q)JF$5!UtP!QpXI9ym{aRu zG+oX0euK}Q>YXMlcIH{Bv%u`mA}e>6Sfz7`8~tToWz|z1t(8-wb2-D*a?F(sQ_nHa zWtc{ec|OD7JNVKI_cYdI^G`H3&&&N6`&UoR&P%{8utne&bDRU*61xQ4CE#A}zk+$+ z18$kE0JoxW*MPeQ++}tJxGM^G9k}bv*wtFk#cx$^hk=a5xEnbEf9!M*{Q(m^aE^V? zi4M85HR8eU?uSly;PW6_t$a(D)o$rY;j3{NaWwRO0MiRj;+JTmH^#Cc^1EK-hk@fu z#}B%p5WE}3S7^e>lk#yW*!iK=b3>0k=?MPSn9GQ>H=O-3P8SB;kt5#q4}8wz-_iVh ziZkM3=u1goj6%ojcKIlB#=%h#J`SA2P)5!XKS?+09`f#yOE?!e`L5p!Lcv#^y+b;y zbO!zrADlRB%xNnr4_#065TFCx3wwbdqaO+fln%WJSB14x z+Ex>L%Gmfvp5XLtM~<_`V=st^8jyhFgEBM%+z9RP#cL;At_9&2tuNk>uTJ-Kj=2D* zvs*mye4Or!)%e<@*WO?M{L#&auiyOq(fY&J*PYh;+h@FIJfO>|Jz{)+-243PAH4H$ zy-N}^{Xwf!{jQ8ke?@QRSB1pd-g%0VmS&7+rmspuq@U~f%)$lI!3NIfRyvmD!HC{R}dS<828{9^Jju+53V94rVcO$9q z@@PET7GV^2!$CZ$IQS_g7-cvTOfQN&$T?HG5HT)<6CNnbKvJ4n1!psYF3M3DFo}t3 zr(;j};h3~P;=+Td9did84@VI;O|{z>oUN+Kce^IYrXRiOxs6BO2bgGmt*`fuzRC0> zL;Q7Q^$W~U*y5?7r{kr*9hI3$<`XVJ+nuheMAZnUc#3UV?WC9*hj{tAT+a-}tRBS{ zw4u_fCq-CA?hO-*R3#~eBQj?)nY*C$iFtqPUQ$r(N4Nj(x?AR&3K^chbIkA)8w z5K&3fB~5at+7|{&Fp>q*Iv67wIPiP!Q1&4GmgIwj#2WYkHc<*cBU&U?$&woexg~Qm zYd9j#kql~j6aNL>D#WjxyX)0wEqnG2EMuGL4Mb*S&G1rhw+dno=- zt3p(aN-0&r&B*STCy*BL>A5v_-ly4>Z1zGnhj0i0av1|(YBPM9Cu$1-NquUdT?q%7 zGK>sSvYZd(u9BFRtZd)f-Th>1=Z?F*v-Mzm&)vSYx3{qa8J64=-NWQkzL4&o)Jd&d zARp|W+pA^%nG3i?7jWi9-PDr*J&q?SCL>uY>b71pDtgnv+qCugdrx1=Q#+iy7BX^5 zu^*ukf&EMe6QSB*x~Wvz=tI5xMO>6E{!)!u{Za(wP8qB)m3}oFp_#^t*01HNZ%s6} zP6nV=XvbGb#b2B9D6>ATb?3TF^h#V9M+a~I&0eb^7I7LuRHoNfb5{s5x)a?d|mNJko*|Oi@fRc_&G8Mn@v<#h1ZMWtxIM<})x-9Iv zu41=%5xa<&2oTw#MPLmeso&Z7;NyF_!YqBbwY&Gx#%J!`TbuWD?85fO&VxG}?ad8n zSf*Nb*4P!V&_0XPg*zKR$>%h;Z|&@D-pV)HouOFSHRbZ_SnuPJWEpMC(k;WT+NM2k zSL{L&+ElTMx}aHIJo%3h^pkf1o(=>M1Xu_FEI5A)j^9FFV|6Ncs;tndvD(kI1D#b^ z^+~x?|2d?h(||72lhRh^zlx)i=n#H~0kP*TnD-HGa}_nF9`g+Z zsRyCxatcAypgAdJsJ(B=y?smGpi6*$q(OAe#0>chfJloVgQrH{IM60@zl5irfS99_ zxE7g^P573d=@ZOw3lsQvw>TkXE=>x6_M`|{o|FJ9lR98^VguGD6~Oug2ElDiYJhVS zQ;a6q&uvc1BA%4dnx8bplSu=uh0HXF2F@ea0g_@$3N$yziFYy3T1*OZ40|SiNYfCC z`T<-zLeZakgE3dZU$S_Cb;|tOpis%23$FBa&lsV_iefKWU-aSsq#Cv#cX z=Mu^?I7zBqL`n#$D5FW{k2oKB14K}6mM0{0XQ*yC4x(h?451>fV{eeyl)Q|*=rC2`9(a1K7j|ki$kRMlUQ1>1I3dzKW1U3o$m;gztcmR+Tj~>%k zkPwz#w>v;Q4^2H&iT5$*_jn}IeoZTorK=dE3Qzwn;L;~ zd*W}gnYaP4*KW;SG#J%X;1wU#AyWd+=vAaD0_kQ_0&h`^PMg#-I=Xx?n9@Q7wREvX zla#8Hg!~qdyb55O7X1w<{Pb^FhAK{=$e^U@oWhgl&`;h3m?}IsI~FfM(RC=g!Auq9 zTdY7)K12MEcSWhX&B{;gPF1OT4XR#AD5F=oT^QW$lA6=cz(XIR8pf77bq*;`$S?$=OTNEvvQxD#|#o8UKq)G$_Z}LEa+*XPtSGxBlgx81pY3|G@E_?Y++^%|X^P4W}F! zJS027q>*`+#lMm7wa>dAl-%*7)M(CRJE$RKb%<$BEy;35CnJt;O?`Qq|Io3N{Xy27 z@-5dr2$~9=l@{fy1i8%4DBV$X^5396^{K_09$%hi+U&HPal|yT&7|w{EdXNHDWaN4 zWm>S$`Y&T|7U}+%p?Jb@8?XobsYLkagSK>808;l6vA|&qCx!h)UfBs zE^HVI4g(*dMp_NYw@9i|C715VA91L0LHrb_QWBLEN2q7O#H}2j>{NRLbz_E&mlFmkL==R|D0+43 z^{nj7jZ^Jcx-6>M^_d;!SmVg}l`b4LGIwsIsYY(7k@<8act!0!#}?GsBQ>^&Z!Y1v z)GOv3YGP0DLCtO|tlpnjy#>m%qQz)QMG}`zwQmsTrX|mbaR<@v3I+d{+u!MLCi<~* z4YBcN7&q>Rz5C?woQ<8GtsUS7ct-KUX6lIHdAHzr9c2H&`|;fEGY71Y%e!2lCU%S5 z@FvwX;9(It$Fum`mr`kUmUs=1UVxeSLm4^fVHOg@)1tVOdL;+Z1^)0c7)fd$7yULA$ zFC&U<-g^~#q;mVB7Nc4>by%z93HHMm7x)e-ApK#MLtsO5W0EsOj(kb zSo?k?0a5XtMgzHh77R=O7muXRX?g{@^VLEPu&LMJ+M1M0t6usx5GqsfJ|i^$s+T;9_62^_?M0(FDi5D`qK4>Lyj4+ zuyVYQSzSb;;$sk!nq{)1ibC-dYUNgT$_8@lDf7^fB-%F7fiTD}BKs+;vDT}CJWo;@ zguNcrATf}37L-5<5=8NkMys1c3Y}HB-6@cFkVzv;RaaBTka~&Q6iMl;sN~v0zuwLmt(dQFMX}(Fb7_nCme@DaO80woC`lphp2vMS<*&&Ln1PENkrMlbY0ckV zibQ}HQ(>QaV#1DzIn12qFcH4^Ft<7QWd8<7OkL;XqhoH2iLk$ySO}_pb|K!&%$M8C zmv2i<#bS}a{NeZC|Jwse`d?|_OQZe*;O)zj#3UwHB(qb>rYzcusfbcFRZ(hYNR+x6 za^!N@G0tRjh$&`-spc@#ScvH?%nUZfB5asNS&WUaD=f}N*%-UZ#@R>g8oSPJu$vfr zi`@o%jFETPUG@o^0QFP$8T*{wV_&c@*-zMgmcVL1We?a_7?orX*&{Z|9v9PQ)KSYZ zCsZCOUty^;xg2LxEd5?`q6^ZQd?urJ^h`0w*d8<4x9mIiJ^LB6*w2fGd39N0uh{GN z5_|0^X=JZJ_Ta`Zn1X`nJ$_t9%=M zw?TW$wn5t#w2wi%1KJMT1#MT*?t*rg$(vHb{xe-bu64_6vid~B_Plp>&L&)M!mSnR zoI3@-b@NAk+&OHz++nR~VR@-lm|{nT>iTRenqQuows)TJz4h5yb3C11-AJc1Pf#8K zo&jFB?xi#KA>aW3o%U8b4CERCaOJ6g%$i zlp1WMQEJ5}vyaxj-16LH_UU?aX8Db2fTNRqYQ_U^sU>f3H&@D67iU{H(is=~y4csn zzV2fH_H(huUTUVvR$iA1sY)$heKy;Qo7I`}-p;$CSzT%rs&8?!$I~mjb>=vapWvJy zr!$9G>kw-lVy#20b%?c^;QjDNJk7srVay4}r_g_baVH+?D=1U1R_DDYpz6%~WoO>2 zpe#L@_ZoAvv#tB-%=9(@(wW8%4_>0QQDPNQW>B6`d*Bvo>$9fu^!3i$r>2{3jZbEe z){p%8smW}4{fNy^PiE(4&3N8fp60vjMLSh4H4iI~TSg(Z-L#h>CBt6cZsnIDgXk0c z!Sa*o%(JIYPi*sEIy;{R%mcR5+2s|K2Y^FB70?9jrHR=~t>eAdM}somZaOns8m02#C=0XT&{-Mx*}%!x_27-y-avc0k~1)UU>o`xQmXr0V%I=lK3 zB_z3u(`F!5s%NTnD@#pt`2creSGT>w6kqV(!LH)@otep3%NvzfD=$lhsa&NMw|8IG zq3I2l+CH#hd#z__b9x7m0?eh&2b(BOz&>EF+g?PS`q$9TzB^nf)t>Cl;-)h;BzCw^ z9yU(ShrR~YId1T8T-u#j(PTb^Vk^+{^?K!eB$;e*$8-3RlXPlF=Sp(%Jd#X$j-RAW z&|X^|Sd&NK6mWuBJq__1?Pe8`L^Hm{@w}13I7#L zcp8u-M?Vyvkh{`3LP8G=%c|K`$FhQmWmW5}S)qE=vJRVerPISd!9x5~LL|NckqP0^ zjd42muK}J8WXOhMD1uf%Q+y#{>Jvg*LdAzdoJz;feR)CpKJ}e;DxWH+>Z#U-(j6!~ zg=KQ`&7uBP1loaHV`jKguN5)OG`te72(^Ke3pI^b?E}Z6 z%d|R+{5v)i`Yy@3JSH3Rm=c%yBT)OMLCQimBji9SrH})B5&RLdvF{W}LGl%<^GRPV zYvmA=eVy9Z0l$E1;nYg5b$d{GL<(f{VzcVheD8T8EH(`yf-MUjUZ7T-8vhI%^3Ms~ zBQy#GjnilVPofcMIHvcbb9R;y2i>pOMejc`)FZLeIY)5LahcOu3PkFCWF#*DLLe6o zg}4u5Yu!{%sC5qa^d&~}pAfoF=)x>7bnFQ_eg_biX~>+~m*h&aU8VtK-p4oEq*?J( z7$1`khPY0WJJh>+Bj>~(WN*ybx0@B8r!Xr}A&Y_C#XajCW%(~!nh25{!I{CqGt@7g znS48a-ZwxOXNCh+;5=2hP0iE+VSs`55Fo!w5?%ARLqEq8L@TF{BU`2i? z(5M!`Ooh4mr3+;a!hYTIont>RdQXHP+Ka0e;%ZD=ilZc1 z;69DwX`m5KL_48^u9LqY^pem#p#`8WJHmbziAbJ}uTYPE@J^7Y(11r8vaCV{N{_=` zX!I=l{s<6>iKLFnQDqGFqycnz?lJLe#k*?ouqU3xP_FkSVjo)03#`Wlzn59%m-*wg zcM$7xRd6bm9%sZJB4w7=A#E3T^WUg=*MJOg*Z(P~DG2Mb7<0)I6pVdAlRg8wYza)I zRKf{4+P+eh$sVBXp&+*D_|3)|ulw~vy|Q#Eyy{qlgzo(kh(antEaF31h2Inr;!HV1 zj1aU?SuclS@dkgF+0>1!3+qDt5sMFp*O8jJ*eRogF=KJl zV6APezFFI{HWq{U+T6z0tGTSTHn+94xRKi&;Pt}Va+v-nKsQIx6yq8v+9kbJL7>kg z^X}=bqmsUwx`Y34$U)+NoMB3hUD=GngU9H<9O(3j@Z-_{CH%(e{;cA z$1B(s+jl&O%sp}|PSM#~>W483n#6)o_Hk4Y{5u(>R7NsDNdN`VTnl~^kBmgP!X<>RPO)LD?%yq>+uT?ANB|InIc=JKtqgq{|p4{=-D8w zu&ADrHG)BY4~aKy`l-~VXnYzJb$}A$E1+lt6rKMZ6xbD3(2GicZkQi~j?4%2aF;H# za1kQ#VbLPcMb0E45F^*Zdj_xwSOK^I4)6iV#BfI}FTi@oHIjIR1OLz~eoY+=D2S7s z@JRDvch#z0W8G&1UT&`M^&Dhhu)K&#e+6`WxklL^WeGhXQ-~|A+XI6B(FUU*YCUOZ zaFw}sk?c$O#ZDCI#LHOMOLS<6Uw9R`g{tG1>P+Z`NN&jqTrZvExyvlyKd}n%I=*8S zN^XS((Mzq-!B!`2yd%b0u4lQ`ssmOq)obR#PQmd!z7Gz?(fJ#y^_8tB9}z%Ep8p5% zMu0j2B#H+D3{VxaPH0;qiRYBba7t_Xlo(NHT0&dujJZM-dgl>^5?DS)=;|Pa42ORt zDBYDQbn)wh6#6fK-xQSYI^?(bZK8a_N!0_q0E&2b7Xt%=7CCUB;6)bgKF}W0`>Z49 zksb-7NZv!Z+bkv{vk{JLotFNV&>oNpe|5=+Z3bZ|Sp&{MBo51+G^XU7A9(mq> z08wTuUU0IZ!g;kkc4s~ec89SZH^b7P0bf(j9&{Hq%EoaR1)RT|B_SE0o1u4lfIr|5T-Nk!v-}}Ay z_Q&({-hcn`_uu{df?@o}IQFyA{{SWZ4iz`r24~#tu{Kk!*)~;cwJp`!ZCkaac1g8P z+fi+~T~@8zuBg^)SA-eQiRvM1&wp%ii`$P4Zi`A>i%%U|?Nb;laR+0LpyxU-bN8{) zUf>n(pN zjK*?!kr{?}OUQnd`JH4i5S=Va27c1@L(U`Whdp|ciAP!LXS-qM_rrZZ>}3AKDBHz| z@WWBIo5(2J$1*S5jne6Yrhg-m{vdgHS;9)|m=-%q&kufBWr z>SaG_il*NQ@%ce--`^Hq*y)Hg72Mz6_hph~KDMJTreRsqkYugAv>|08$M1PQejkOY@Ij_8XfX9<7!_=-f&aDp5Pz~G z2F=kRdUU_3mb4mn?gqgCtOh~u1py|GdepB5!S6?5uXs}q0-kh&;4g+OgA!H!@6wgs zq%W>ySFS<2S03=aE5#RPEq$pu+?OWS{1hc6O?NGusgW0Rx`ZhwIfcG!ZmZX{@@S^- zD-LdJCLivKfxp%r4LT?JY!>=<-|vP|j}B<6fz+jA=tXHJ2GL+gZKpM7Dx8wEK#e4- zWr->WRbGO82g#8jbkC*t6;$t|Xhv<@R3jZaM*7ii3nol#vx(1?s4?oY#h13+QiOj8Z zKh0h7s1rnkZj#%f+)1T}t@7%|wVSQh?Ts6Ew>GZl&PMCzM(a*q>hzLSKtJtanC<2) z&6zkRFA`N=jnaVL23&-E10(4oDudOTiGK}kmAzzoOrFQ+300xvP!g=6L((I-F-hh7>!EJ6`}QhxF-TF zFGcZfd^RPOHkf<`^$Fr=`~#Hqq#knlm?Odtw&qv8icq4%{@VD`7??P61EOH9hLcyN=!ob6=#RL_ z&F(OF2=9hj?gDqRq?7dWdNK&~V9@DBfF!w$Zvz?ZRP?&Jvn`W7A@Kw`<}5fSBqbjY z%ZsmkfpuvWl|IHgbC}Dq-k@!~fqO7bpK&nF~L21>t_ z&S#1s!Xj_kE(wR%pBP=nZC-j@fk!NJhnI0CE_ZnaEqorIL+kM>pGP~#Yy1@2>JuDG zdmi#!$lWO;cUsRlKGLBBqaVQnecnPz|BPy4re0&6kHSg>$Avtw8=9iaRq zD;F*7mopr#hi3~mZpU-_h$nVj)jf>OPt2@FntlkZUl?HZnf5D%X52dLjex`DqsRgug1=Ho3@WbgGSIrP`7X zQ_fRG#+I8&D$i2K5s#q5sWQ&o+PwL3UgBbVw4;tQcSeI=G}z1S?P$P(2D@-8xfKnv z-03OR&r9iO2pD~n_Ns1=q8<-YWeoBnee8!cJ`cKNHq|XCv(@RPxg(QNhNX&(@^;wS z>m@t#k|IiP(O`KcfGUS+8Wh?}EXns!(Xpk3D28c6JL_x-#RJSNqFw~#RN45<^q8B+ z37dg9K`0%(vQn~g4*165v6yPJYb}%YnXPeUHEph@msVw?VQDpA$81G-gUL6jKXay1 znG#&F6hj#V5AZUjAw!{|!^R6UqNZ~?q;2%0mLNnCM1cadzzCp&s1Yz|C@}MKa7=Qb zND=UJOji`WfRtzuWkGO&8Oog*kk<(^^!u4yh=e&5MaC!S?AHmLNhSqay#r@v!Igbf zxH6bR;mR1?m7`o)Id=Cfxqbr4td^vWmGf#;WRv>uP^ER?=9UTwBzaazPDGxjiu{v2 zgQ{U_URP+~If>WcB}i2ZeE$Qc6aZ#IF>0)8*4g;%k?c>*o&h(pK0ATe@c?#|N+_En z0RWtpiU7Q+KRuOFUQM%PIMs3a3nGyOC|{*Y;c!7cnZ)zd&jCu)-BHGqhl4-jeM&%N zK!<_4OEaR6O`1_m6G=8&XQ0wVF=s^Y{yD-cZej!u>yRCqU$FxdvB*^%AB$~7AT}Zm zPu?z$(TQztXE-<_$Myw69@`n%NRqo}VErNx4*LTE2~}%^aq+Sqq20Ort#wf1m5JG9%fP+hEHLpL zECc)iF`amY18p&OmJnA_dLD>bno{4GYuy=}>zmKW)W?^5g~wh~aU_xjM33;@992Kb zgb(jJ$dH|^`Kzf?*0H_5xw$!h`$Y=@OwrC@wubh)_33qgmD6fiCK+X3(K{n!$je3Q zFs`YPMmy=|Dj&afw7YiTAgl=8D8cH=q0!DKi zUi^V10MW32m3l`;^#M^;jd&3DutS(3E_92opiluwk8ltBl#2i?_dpBfbl^c5ZSL%d zYzT&&VMwV@UcRO$I>wzTyZRai3RdQsuUDZ zaI6T==L@&i5xw5jwGo4GC(5;u$51}?J~}F%CI)O1Y%RED*LEvzNxh%>4q8E;-J%6= z00;Kh5lq4!6mpq}T}+?=qo@V8s(>6LfNGU{XmqeLH>ey~2R1O`I;m3iunDgqS{8Zrv#>!pFGiogY2W{lfRFFg-YJ8!SjCvfm z5wmLnGVaw>!2+afj9=H*yXKP(^?aB;ZYfsf{(jQY{&M}K2y~y47dtrZ@hwJ~M{ZNeY z`JniE^L@oYx{i*X)Obyv!W4Ovs$Wrciz?-UgR$8DquFNt031DPBWKucOk*E`|Cq0~1LFj1vW6{(-q)&B(YY z0YJj9jSoiZd@nCVWO{C@gvDV}5}@?7Q9hFEbFsf7O-Cfw^FktF=-5Od5UIg;L^B}^ zg(H;H6M!OZqj%raSH&O&I}}PIW;IcpJ76~`F1p75)R#R1{>v}i3tjCL-o+0G9NrGO zD^GoO6_kwRaZd>5=&nbr4=hgYhC7O^>Em7!Gy8q`qZD_7{>7iYt!@%=m$uVOwnJPR z4TN_1{e-Ue^j2EeaMP%^!hOP9qhX56q&>`_+sSQ#O@%+A+fLl>g>qkCDyblr^4=pG z5MW$(sXCIQ>M3DHc#)&%*v@evMoD9o-=9|(Qu-#Qj5KHm2dNECL6R~;WmhlLCe<$?~kFa=qftyY`Z|)N)_*y#E8BA6XFq literal 0 HcmV?d00001 diff --git a/mitogen/mitogen/__pycache__/utils.cpython-36.pyc b/mitogen/mitogen/__pycache__/utils.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..def2bcdfdad7ecb7125afbb001a0a3a3560800f1 GIT binary patch literal 5971 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", 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/mitogen/mitogen/core.py b/mitogen/mitogen/core.py new file mode 100644 index 0000000..d8c57ba --- /dev/null +++ b/mitogen/mitogen/core.py @@ -0,0 +1,3997 @@ +# 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 binascii +import collections +import encodings.latin_1 +import encodings.utf_8 +import errno +import fcntl +import itertools +import linecache +import logging +import os +import pickle as py_pickle +import pstats +import signal +import socket +import struct +import sys +import syslog +import threading +import time +import traceback +import warnings +import weakref +import zlib + +# Python >3.7 deprecated the imp module. +warnings.filterwarnings('ignore', message='the imp module is deprecated') +import imp + +# Absolute imports for <2.5. +select = __import__('select') + +try: + import cProfile +except ImportError: + cProfile = None + +try: + import thread +except ImportError: + import threading as thread + +try: + import cPickle as pickle +except ImportError: + import pickle + +try: + from cStringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO + +try: + BaseException +except NameError: + BaseException = Exception + +try: + ModuleNotFoundError +except NameError: + ModuleNotFoundError = ImportError + +# 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) + +# str.encode() may take import lock. Deadlock possible if broker calls +# .encode() on behalf of thread currently waiting for module. +LATIN1_CODEC = encodings.latin_1.Codec() + +_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 + +#: 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 + +try: + BaseException +except NameError: + BaseException = Exception + +PY24 = sys.version_info < (2, 5) +PY3 = sys.version_info > (3,) +if PY3: + b = str.encode + BytesType = bytes + UnicodeType = str + FsPathTypes = (str,) + BufferType = lambda buf, start: memoryview(buf)[start:] + long = int +else: + b = str + BytesType = str + FsPathTypes = (str, unicode) + BufferType = buffer + UnicodeType = unicode + +AnyTextType = (BytesType, UnicodeType) + +try: + next +except NameError: + next = lambda it: it.next() + +# #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 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 not PY3: + # 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 PY3: + 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, _ = encodings.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) + + +# Documented in api.rst to work around Sphinx limitation. +now = getattr(time, 'monotonic', time.time) + + +# Python 2.4 +try: + any +except NameError: + def any(it): + for elem in it: + if elem: + return True + + +def _partition(s, sep, find): + """ + (str|unicode).(partition|rpartition) for Python 2.4/2.5. + """ + idx = find(sep) + if idx != -1: + left = s[0:idx] + return left, sep, s[len(left)+len(sep):] + + +if hasattr(UnicodeType, 'rpartition'): + str_partition = UnicodeType.partition + str_rpartition = UnicodeType.rpartition + bytes_partition = BytesType.partition +else: + def str_partition(s, sep): + return _partition(s, sep, s.find) or (s, u'', u'') + def str_rpartition(s, sep): + return _partition(s, sep, s.rfind) or (u'', u'', s) + def bytes_partition(s, sep): + return _partition(s, sep, s.find) or (s, '', '') + + +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 _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 is_blacklisted_import(importer, fullname): + """ + Return :data:`True` if `fullname` is part of a blacklisted package, or if + any packages have been whitelisted and `fullname` is not part of one. + + NB: + - If a package is on both lists, then it is treated as blacklisted. + - If any package is whitelisted, then all non-whitelisted packages are + treated as blacklisted. + """ + return ((not any(fullname.startswith(s) for s in importer.whitelist)) or + (any(fullname.startswith(s) for s in importer.blacklist))) + + +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`. + """ + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + assert fd > 2, 'fd %r <= 2' % (fd,) + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + +def set_nonblock(fd): + """ + Set the file descriptor `fd` to non-blocking mode. For most underlying file + types, this causes :func:`os.read` or :func:`os.write` to raise + :class:`OSError` with :data:`errno.EAGAIN` rather than block the thread + when the underlying kernel buffer is exhausted. + """ + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + +def set_block(fd): + """ + Inverse of :func:`set_nonblock`, i.e. cause `fd` to block the thread when + the underlying kernel buffer is exhausted. + """ + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + + +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(): + """ + 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() + return ( + os.fdopen(rfd, 'rb', 0), + os.fdopen(wfd, 'wb', 0) + ) + + +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 Py24Pickler(py_pickle.Pickler): + """ + Exceptions were classic classes until Python 2.5. Sadly for 2.4, cPickle + offers little control over how a classic instance is pickled. Therefore 2.4 + uses a pure-Python pickler, so CallError can be made to look as it does on + newer Pythons. + + This mess will go away once proper serialization exists. + """ + @classmethod + def dumps(cls, obj, protocol): + bio = BytesIO() + self = cls(bio, protocol=protocol) + self.dump(obj) + return bio.getvalue() + + def save_exc_inst(self, obj): + if isinstance(obj, CallError): + func, args = obj.__reduce__() + self.save(func) + self.save(args) + self.write(py_pickle.REDUCE) + else: + py_pickle.Pickler.save_inst(self, obj) + + if PY24: + dispatch = py_pickle.Pickler.dispatch.copy() + dispatch[py_pickle.InstanceType] = save_exc_inst + + +if PY3: + # In 3.x Unpickler is a class exposing find_class as an overridable, but it + # cannot be overridden without subclassing. + class _Unpickler(pickle.Unpickler): + def find_class(self, module, func): + return self.find_global(module, func) + pickle__dumps = pickle.dumps +elif PY24: + # On Python 2.4, we must use a pure-Python pickler. + pickle__dumps = Py24Pickler.dumps + _Unpickler = pickle.Unpickler +else: + pickle__dumps = pickle.dumps + # In 2.x Unpickler is a function exposing a writeable find_global + # attribute. + _Unpickler = pickle.Unpickler + + +class Message(object): + """ + Messages are the fundamental unit of communication, comprising fields from + the :ref:`stream-protocol` header, an optional reference to the receiving + :class:`mitogen.core.Router` for ingress messages, and helper methods for + deserialization and generating replies. + """ + #: Integer target context ID. :class:`Router` delivers messages locally + #: when their :attr:`dst_id` matches :data:`mitogen.context_id`, otherwise + #: they are routed up or downstream. + dst_id = None + + #: Integer source context ID. Used as the target of replies if any are + #: generated. + src_id = None + + #: Context ID under whose authority the message is acting. See + #: :ref:`source-verification`. + auth_id = None + + #: Integer target handle in the destination context. This is one of the + #: :ref:`standard-handles`, or a dynamically generated handle used to + #: receive a one-time reply, such as the return value of a function call. + handle = None + + #: Integer target handle to direct any reply to this message. Used to + #: receive a one-time reply, such as the return value of a function call. + #: :data:`IS_DEAD` has a special meaning when it appears in this field. + reply_to = None + + #: Raw message data bytes. + data = b('') + + _unpickled = object() + + #: The :class:`Router` responsible for routing the message. This is + #: :data:`None` for locally originated messages. + router = None + + #: The :class:`Receiver` over which the message was last received. Part of + #: the :class:`mitogen.select.Select` interface. Defaults to :data:`None`. + receiver = None + + HEADER_FMT = '>hLLLLLL' + HEADER_LEN = struct.calcsize(HEADER_FMT) + HEADER_MAGIC = 0x4d49 # 'MI' + + 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' + + def pack(self): + return ( + struct.pack(self.HEADER_FMT, self.HEADER_MAGIC, 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 = LATIN1_CODEC.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'], _ = encodings.utf_8.encode(reason or u'') + return cls(reply_to=IS_DEAD, **kwargs) + + @classmethod + def pickled(cls, obj, **kwargs): + """ + Construct a pickled message, setting :attr:`data` to the serialization + of `obj`, and setting remaining fields using `kwargs`. + + :returns: + The new message. + """ + self = cls(**kwargs) + try: + self.data = pickle__dumps(obj, protocol=2) + except pickle.PicklingError: + e = sys.exc_info()[1] + self.data = pickle__dumps(CallError(e), protocol=2) + return self + + def reply(self, msg, router=None, **kwargs): + """ + Compose a reply to this message and send it using :attr:`router`, or + `router` is :attr:`router` is :data:`None`. + + :param obj: + Either a :class:`Message`, or an object to be serialized in order + to construct a new message. + :param router: + Optional router to use if :attr:`router` is :data:`None`. + :param kwargs: + Optional keyword parameters overriding message fields in the reply. + """ + if not isinstance(msg, Message): + msg = Message.pickled(msg) + msg.dst_id = self.src_id + 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) + + if PY3: + UNPICKLER_KWARGS = {'encoding': 'bytes'} + else: + UNPICKLER_KWARGS = {} + + 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 unpickle(self, throw=True, throw_dead=True): + """ + Unpickle :attr:`data`, optionally raising any exceptions present. + + :param bool throw_dead: + If :data:`True`, raise exceptions, otherwise it is the caller's + responsibility. + + :raises CallError: + The serialized data contained CallError exception. + :raises ChannelError: + The `is_dead` field was set. + """ + _vv and IOLOG.debug('%r.unpickle()', self) + if throw_dead and self.is_dead: + self._throw_dead() + + obj = self._unpickled + if obj is Message._unpickled: + fp = BytesIO(self.data) + unpickler = _Unpickler(fp, **self.UNPICKLER_KWARGS) + unpickler.find_global = self._find_global + try: + # Must occur off the broker thread. + try: + obj = unpickler.load() + except: + LOG.error('raw pickle was: %r', self.data) + raise + self._unpickled = obj + except (TypeError, ValueError): + e = sys.exc_info()[1] + raise StreamError('invalid message: %s', e) + + if throw: + if isinstance(obj, CallError): + raise obj + + return obj + + def __repr__(self): + return 'Message(%r, %r, %r, %r, %r, %r..%d)' % ( + self.dst_id, self.src_id, self.auth_id, self.handle, + self.reply_to, (self.data or '')[:50], len(self.data) + ) + + +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): + """ + Send `data` to the remote end. + """ + _vv and IOLOG.debug('%r.send(%r..)', self, repr(data)[:100]) + self.context.send(Message.pickled(data, handle=self.dst_handle)) + + 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, (int, long)) and context_id >= 0 and + isinstance(dst_handle, (int, long)) 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 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', + 'jail', + 'lxc', + 'lxd', + 'master', + 'minify', + 'os_fork', + 'parent', + '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__', + 'thread', + + # org.python.core imported by copy, pickle, xml.sax; breaks Jython, but + # very unlikely to trigger a bug report. + 'org', + ] + + if PY3: + ALWAYS_BLACKLIST += ['cStringIO'] + + def __init__(self, router, context, core_src, whitelist=(), blacklist=()): + self._log = logging.getLogger('mitogen.importer') + self._context = context + self._present = {'mitogen': self.MITOGEN_PKG_CONTENT} + self._lock = threading.Lock() + self.whitelist = list(whitelist) or [''] + self.blacklist = list(blacklist) + self.ALWAYS_BLACKLIST + + # Preserve copies of the original server-supplied whitelist/blacklist + # for later use by children. + self.master_whitelist = self.whitelist[:] + self.master_blacklist = self.blacklist[:] + + # Presence of an entry in this map indicates in-flight GET_MODULE. + self._callbacks = {} + self._cache = {} + if core_src: + self._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 _update_linecache(self, path, data): + """ + The Python 2.4 linecache module, used to fetch source code for + tracebacks and :func:`inspect.getsource`, does not support PEP-302, + meaning it needs extra help to for Mitogen-loaded modules. Directly + populate its cache if a loaded module belongs to the Mitogen package. + """ + if PY24 and 'mitogen' in path: + linecache.cache[path] = ( + len(data), + 0.0, + [line+'\n' for line in data.splitlines()], + path, + ) + + 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' + + 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() + + parent, _, modname = str_rpartition(fullname, '.') + if parent: + path = sys.modules[parent].__path__ + else: + path = None + + fp, pathname, description = imp.find_module(modname, path) + if fp: + fp.close() + + def find_module(self, fullname, path=None): + if hasattr(_tls, 'running'): + return None + + _tls.running = True + try: + #_v and self._log.debug('Python requested %r', fullname) + fullname = to_text(fullname) + pkgname, dot, _ = 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 + + suffix = fullname[len(pkgname+dot):] + if pkgname and suffix not in self._present.get(pkgname, ()): + self._log.debug('%s has no submodule %s', pkgname, suffix) + return None + + # #114: explicitly whitelisted prefixes override any + # system-installed package. + if self.whitelist != ['']: + if any(fullname.startswith(s) for s in self.whitelist): + 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 + + blacklisted_msg = ( + '%r is present in the Mitogen importer blacklist, therefore this ' + 'context will not attempt to request it from the master, as the ' + 'request will always be refused.' + ) + 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): + if is_blacklisted_import(self, fullname): + raise ModuleNotFoundError(self.blacklisted_msg % (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 tup[2] is not None and PY24: + self._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 load_module(self, fullname): + 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() + + ret = self._cache[fullname] + if ret[2] is None: + raise ModuleNotFoundError(self.absent_msg % (fullname,)) + + pkg_present = ret[1] + 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 mod.__package__ and not PY3: + # 2.x requires __package__ to be exactly a string. + mod.__package__, _ = encodings.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 PY3: + 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 PY3: + return to_text(source) + return source + + +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_nonblock` and :func:`set_cloexec` + on the underlying file descriptors during construction. + + 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. + :param bool blocking: + If :data:`False`, the descriptor has its :data:`os.O_NONBLOCK` flag + enabled using :func:`fcntl.fcntl`. + """ + _fork_refs = weakref.WeakValueDictionary() + closed = False + + def __init__(self, stream, fp, cloexec=True, keep_alive=True, blocking=False): + #: 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) + if not blocking: + set_nonblock(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 + (magic, 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 magic != Message.HEADER_MAGIC: + 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, (int, long)) and context_id >= 0 and ( + (name is None) or + (isinstance(name, UnicodeType) and len(name) < 100)) + ): + raise TypeError('cannot unpickle Context: bad input') + + 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. The default implementation is based on + :func:`select.poll`. + + 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. + """ + SUPPORTED = True + + # This changed from select() to poll() in Mitogen 0.2.4. Since poll() has + # no upper FD limit, it is suitable for use with Latch, which must handle + # FDs larger than select's limit during many-host runs. We want this + # because poll() requires no setup and teardown: just a single system call, + # which is important because Latch.get() creates a Poller on each + # invocation. In a microbenchmark, poll() vs. epoll_ctl() is 30% faster in + # this scenario. If select() must return in future, it is important + # Latch.poller_class is set from parent.py to point to the industrial + # strength poller for the OS, otherwise Latch will fail randomly. + + #: 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 for waiting. Since the poller + #: will be very short-lived, we prefer :class:`mitogen.parent.PollPoller` + #: if it is available, or :class:`mitogen.core.Poller` otherwise, since + #: these implementations require no system calls to create, configure or + #: destroy. + 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 = socket.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.currentThread().getName(), + ) + + +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) + stream.accept(*pipe()) + 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] 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. + """ + rsock, wsock = socket.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_fp = os.fdopen(os.dup(self.config.get('out_fd', 1)), 'wb', 0) + self.stream = MitogenProtocol.build_stream( + self.router, + parent_id, + local_id=self.config['context_id'], + parent_ids=self.config['parent_ids'] + ) + 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', 1) + 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 + + importer = Importer( + self.router, + self.parent, + core_src, + self.config.get('whitelist', ()), + self.config.get('blacklist', ()), + ) + + self.importer = importer + self.router.importer = importer + sys.meta_path.insert(0, self.importer) + + def _setup_package(self): + global mitogen + mitogen = imp.new_module('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 stdfd, mode in ((0, os.O_RDONLY), (1, os.O_RDWR), (2, os.O_RDWR)): + 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(2): + self.reserve_tty_fp = os.fdopen(os.dup(2), '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 name, fd in (('stdout', 1), ('stderr', 2)): + log = IoLoggerProtocol.build_stream(name, fd) + self.broker.start_receive(log) + self.loggers.append(log) + + # Reopen with line buffering. + sys.stdout = os.fdopen(1, 'w', 1) + + def main(self): + self._setup_master() + try: + try: + self._setup_logging() + self._setup_importer() + 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/mitogen/mitogen/debug.py b/mitogen/mitogen/debug.py new file mode 100644 index 0000000..dbab550 --- /dev/null +++ b/mitogen/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/mitogen/mitogen/doas.py b/mitogen/mitogen/doas.py new file mode 100644 index 0000000..5b212b9 --- /dev/null +++ b/mitogen/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/mitogen/mitogen/docker.py b/mitogen/mitogen/docker.py new file mode 100644 index 0000000..48848c8 --- /dev/null +++ b/mitogen/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/mitogen/mitogen/fakessh.py b/mitogen/mitogen/fakessh.py new file mode 100644 index 0000000..e62cf84 --- /dev/null +++ b/mitogen/mitogen/fakessh.py @@ -0,0 +1,456 @@ +# 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 shutil +import socket +import subprocess +import sys +import tempfile +import threading + +import mitogen.core +import mitogen.master +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) + 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, data) + 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(1, 'w+b', 0), + stdout=os.fdopen(0, '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 = socket.socketpair() + + stream = mitogen.core.Stream(router, context_id) + stream.name = u'fakessh' + 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(context, 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/mitogen/mitogen/fork.py b/mitogen/mitogen/fork.py new file mode 100644 index 0000000..f0c2d7e --- /dev/null +++ b/mitogen/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_block(childfp.fileno()) + + 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/mitogen/mitogen/jail.py b/mitogen/mitogen/jail.py new file mode 100644 index 0000000..4da7eb0 --- /dev/null +++ b/mitogen/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/mitogen/mitogen/kubectl.py b/mitogen/mitogen/kubectl.py new file mode 100644 index 0000000..374ab74 --- /dev/null +++ b/mitogen/mitogen/kubectl.py @@ -0,0 +1,67 @@ +# 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.core +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/mitogen/mitogen/lxc.py b/mitogen/mitogen/lxc.py new file mode 100644 index 0000000..a86ce5f --- /dev/null +++ b/mitogen/mitogen/lxc.py @@ -0,0 +1,74 @@ +# 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 + 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/mitogen/mitogen/lxd.py b/mitogen/mitogen/lxd.py new file mode 100644 index 0000000..675dddc --- /dev/null +++ b/mitogen/mitogen/lxd.py @@ -0,0 +1,76 @@ +# 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 + 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/mitogen/mitogen/master.py b/mitogen/mitogen/master.py new file mode 100644 index 0000000..f9ddf3d --- /dev/null +++ b/mitogen/mitogen/master.py @@ -0,0 +1,1357 @@ +# 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 dis +import errno +import imp +import inspect +import itertools +import logging +import os +import pkgutil +import re +import string +import sys +import threading +import types +import zlib + +try: + import sysconfig +except ImportError: + sysconfig = None + +if not hasattr(pkgutil, 'find_loader'): + # find_loader() was new in >=2.5, but the modern pkgutil.py syntax has + # been kept intentionally 2.3 compatible so we can reuse it. + from mitogen.compat import pkgutil + +import mitogen +import mitogen.core +import mitogen.minify +import mitogen.parent + +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 + +imap = getattr(itertools, 'imap', map) +izip = getattr(itertools, 'izip', zip) + +try: + any +except NameError: + from mitogen.core import any + +try: + next +except NameError: + from mitogen.core import next + + +RLOG = logging.getLogger('mitogen.ctx') + + +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.abspath(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. + """ + if imp.is_builtin(modname) != 0: + return True + + module = sys.modules.get(modname) + if module is None: + return False + + # six installs crap with no __file__ + modpath = os.path.abspath(getattr(module, '__file__', '')) + return is_stdlib_path(modpath) + + +_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): + """ + 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. + + :return: + List of submodule name suffixes. + """ + it = pkgutil.iter_modules([os.path.dirname(path)]) + return [to_text(name) for _, name, _ in it] + + +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): + if not path: + return None + + if path[-4:] in ('.pyc', '.pyo'): + path = path.rstrip('co') + + if path.endswith('.py'): + return path + + if os.path.exists(path) and _looks_like_script(path): + return path + + +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 + + +LOAD_CONST = dis.opname.index('LOAD_CONST') +IMPORT_NAME = dis.opname.index('IMPORT_NAME') + + +def _getarg(nextb, c): + if c >= dis.HAVE_ARGUMENT: + return nextb() | (nextb() << 8) + + +if sys.version_info < (3, 0): + def iter_opcodes(co): + # Yield `(op, oparg)` tuples from the code object `co`. + ordit = imap(ord, co.co_code) + nextb = ordit.next + return ((c, _getarg(nextb, c)) for c in ordit) +elif sys.version_info < (3, 6): + def iter_opcodes(co): + # Yield `(op, oparg)` tuples from the code object `co`. + ordit = iter(co.co_code) + nextb = ordit.__next__ + return ((c, _getarg(nextb, c)) for c in ordit) +else: + def iter_opcodes(co): + # Yield `(op, oparg)` tuples from the code object `co`. + ordit = iter(co.co_code) + nextb = ordit.__next__ + # https://github.com/abarnert/cpython/blob/c095a32f/Python/wordcode.md + return ((c, nextb()) for c in ordit) + + +def scan_code_imports(co): + """ + Given a code object `co`, scan its bytecode yielding any ``IMPORT_NAME`` + and associated prior ``LOAD_CONST`` instructions representing an `Import` + statement or `ImportFrom` statement. + + :return: + Generator producing `(level, modname, namelist)` tuples, where: + + * `level`: -1 for normal import, 0, for absolute import, and >0 for + relative import. + * `modname`: Name of module to import, or from where `namelist` names + are imported. + * `namelist`: for `ImportFrom`, the list of names to be imported from + `modname`. + """ + opit = iter_opcodes(co) + opit, opit2, opit3 = itertools.tee(opit, 3) + + try: + next(opit2) + next(opit3) + next(opit3) + except StopIteration: + return + + if sys.version_info >= (2, 5): + for oparg1, oparg2, (op3, arg3) in izip(opit, opit2, opit3): + if op3 == IMPORT_NAME: + op2, arg2 = oparg2 + op1, arg1 = oparg1 + if op1 == op2 == LOAD_CONST: + yield (co.co_consts[arg1], + co.co_names[arg3], + co.co_consts[arg2] or ()) + else: + # Python 2.4 did not yet have 'level', so stack format differs. + for oparg1, (op2, arg2) in izip(opit, opit2): + if op2 == IMPORT_NAME: + op1, arg1 = oparg1 + if op1 == LOAD_CONST: + yield (-1, co.co_names[arg2], co.co_consts[arg1] or ()) + + +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) + + # See logging.Handler.makeRecord() + record = logging.LogRecord( + name=logger.name, + level=int(level_s), + 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 __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: + # Pre-'import spec' this returned None, in Python3.6 it raises + # ImportError. + loader = pkgutil.find_loader(fullname) + except ImportError: + e = sys.exc_info()[1] + LOG.debug('%r._get_module_via_pkgutil(%r): %s', + self, fullname, e) + return None + + IOLOG.debug('%r._get_module_via_pkgutil(%r) -> %r', + self, fullname, loader) + if not loader: + return + + try: + path = _py_filename(loader.get_filename(fullname)) + source = loader.get_source(fullname) + is_pkg = loader.is_package(fullname) + except (AttributeError, ImportError): + # - Per PEP-302, get_source() and is_package() are optional, + # calling them may throw AttributeError. + # - 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: loading %r using %r failed: %s', + self, fullname, loader, e) + return + + if path is None or source is None: + 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. + """ + module = sys.modules.get(fullname) + if not isinstance(module, types.ModuleType): + LOG.debug('%r: sys.modules[%r] absent or not a regular module', + self, fullname) + return + + LOG.debug('_get_module_via_sys_modules(%r) -> %r', fullname, module) + alleged_name = getattr(module, '__name__', None) + if alleged_name != fullname: + LOG.debug('sys.modules[%r].__name__ is incorrect, assuming ' + 'this is a hacky module alias and ignoring it. ' + 'Got %r, module object: %r', + fullname, alleged_name, module) + return + + path = _py_filename(getattr(module, '__file__', '')) + if not 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 ParentEnumerationMethod(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. + """ + def _find_sane_parent(self, fullname): + """ + Iteratively search :data:`sys.modules` for the least indirect parent of + `fullname` that is loaded and contains 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. + """ + path = None + modpath = [] + while True: + pkgname, _, modname = str_rpartition(to_text(fullname), u'.') + modpath.insert(0, modname) + if not pkgname: + return [], None, modpath + + pkg = sys.modules.get(pkgname) + path = getattr(pkg, '__path__', None) + if pkg and path: + return pkgname.split('.'), path, modpath + + LOG.debug('%r: %r lacks __path__ attribute', self, pkgname) + fullname = pkgname + + 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) + 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 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 ModuleFinder(object): + """ + Given the name of a loaded module, make a best-effort attempt at finding + related modules likely needed by a child context requesting the original + module. + """ + def __init__(self): + #: Import machinery is expensive, keep :py:meth`:get_module_source` + #: results around. + 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(), + ParentEnumerationMethod(), + ] + + 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 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 scan_code_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 sys.modules.get(name) is not None + and not is_stdlib_name(name) + and u'six.moves' not in name # TODO: crap + ) + )) + + 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): + self._log = logging.getLogger('mitogen.responder') + self._router = router + self._finder = ModuleFinder() + self._cache = {} # fullname -> pickled + self.blacklist = [] + self.whitelist = [''] + + #: 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 self.whitelist == ['']: + self.whitelist = ['mitogen'] + self.whitelist.append(fullname) + + def blacklist_prefix(self, fullname): + self.blacklist.append(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] + + if mitogen.core.is_blacklisted_import(self, fullname): + raise ImportError('blacklisted') + + 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) + 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 mitogen.core.is_blacklisted_import(self, 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): + 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 (%.2f KiB) to %s', + 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) + self._send_load_module(stream, fullname) + 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 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=threading.currentThread(), + 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) + 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): + for stream in 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/mitogen/mitogen/minify.py b/mitogen/mitogen/minify.py new file mode 100644 index 0000000..09fdc4e --- /dev/null +++ b/mitogen/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 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/mitogen/mitogen/os_fork.py b/mitogen/mitogen/os_fork.py new file mode 100644 index 0000000..da832c6 --- /dev/null +++ b/mitogen/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 threading +import weakref + +import mitogen.core + + +# 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_cloexec(rsock.fileno()) + mitogen.core.set_cloexec(wsock.fileno()) + mitogen.core.set_block(wsock) # gevent + 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 = threading.currentThread() + 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/mitogen/mitogen/parent.py b/mitogen/mitogen/parent.py new file mode 100644 index 0000000..630e3de --- /dev/null +++ b/mitogen/mitogen/parent.py @@ -0,0 +1,2770 @@ +# 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 codecs +import errno +import fcntl +import getpass +import heapq +import inspect +import logging +import os +import re +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') + +try: + import thread +except ImportError: + import threading as thread + +import mitogen.core +from mitogen.core import b +from mitogen.core import bytes_partition +from mitogen.core import IOLOG + + +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 + + +try: + next +except NameError: + # Python 2.4/2.5 + from mitogen.core import next + + +itervalues = getattr(dict, 'itervalues', dict.values) + +if mitogen.core.PY3: + xrange = range + 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' + +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. + """ + return sum(getattr(termios, name, 0) + for name in names.split()) + + +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. + """ + # BSD: github.com/freebsd/freebsd/blob/master/lib/libc/gen/termios.c#L162 + # Linux: github.com/lattera/glibc/blob/master/termios/cfmakeraw.c#L20 + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = tflags + iflag &= ~flags('IMAXBEL IXOFF INPCK BRKINT PARMRK ' + 'ISTRIP INLCR ICRNL IXON IGNPAR') + iflag &= ~flags('IGNBRK BRKINT PARMRK') + 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): + """ + 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 = socket.socketpair() + for fp in parentfp, childfp: + fp.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, size) + + return parentfp, childfp + + +def create_best_pipe(escalates_privilege=False): + """ + 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. + :returns: + `(parent_rfp, child_wfp, child_rfp, parent_wfp)` + """ + if (not escalates_privilege) or (not SELINUX_ENABLED): + parentfp, childfp = create_socketpair() + return parentfp, childfp, childfp, parentfp + + parent_rfp, child_wfp = mitogen.core.pipe() + try: + child_rfp, parent_wfp = mitogen.core.pipe() + 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 + ) + + stderr = None + stderr_r = None + if merge_stdio: + stderr = child_wfp + elif stderr_pipe: + stderr_r, stderr = mitogen.core.pipe() + mitogen.core.set_cloexec(stderr_r.fileno()) + + 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. + os.close(os.open(os.ttyname(2), os.O_RDWR)) + if hasattr(termios, 'TIOCSCTTY') and not mitogen.core.IS_WSL: + # #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(2, 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) + disable_echo(master_fd) + disable_echo(slave_fd) + mitogen.core.set_block(slave_fd) + 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_block(child_rfp) + mitogen.core.set_block(child_wfp) + 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.readers), len(new.writers), + len(old.readers), len(old.writers)) + + +@mitogen.core.takes_econtext +def upgrade_router(econtext): + if not isinstance(econtext.router, Router): # TODO + econtext.broker.defer(_upgrade_broker, econtext.broker) + 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 versions of OS X, otherwise it is the preferred poller for small FD + counts, as there is no setup/teardown/configuration system call overhead. + """ + SUPPORTED = hasattr(select, 'poll') + _repr = 'PollPoller()' + + def __init__(self): + super(PollPoller, self).__init__() + self._pollobj = select.poll() + + # TODO: no proof we dont need writemask too + _readmask = ( + getattr(select, 'POLLIN', 0) | + getattr(select, 'POLLHUP', 0) + ) + + 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') + _repr = 'KqueuePoller()' + + 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:man2:`epoll` interface. + """ + SUPPORTED = hasattr(select, 'epoll') + _repr = 'EpollPoller()' + + 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) + + _inmask = (getattr(select, 'EPOLLIN', 0) | + getattr(select, 'EPOLLHUP', 0)) + + 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 + + +# 2.4 and 2.5 only had select.select() and select.poll(). +for _klass in mitogen.core.Poller, PollPoller, KqueuePoller, EpollPoller: + if _klass.SUPPORTED: + PREFERRED_POLLER = _klass + +# 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. +# So swap in our favourite poller. +if PollPoller.SUPPORTED: + mitogen.core.Latch.poller_class = PollPoller +else: + mitogen.core.Latch.poller_class = PREFERRED_POLLER + + +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. + # * 'CONTEXT_NAME' and 'PREAMBLE_COMPRESSED_LEN' are substituted with + # their respective values. + # * CONTEXT_NAME must be prefixed with the name of the Python binary in + # order to allow virtualenvs to detect their install prefix. + # * For Darwin, OS X installs a craptacular argv0-introspecting Python + # version switcher as /usr/bin/python. Override attempts to call it + # with an explicit call to python2.7 + # + # 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. + # C: the decompressed core source. + + # Final os.close(2) to avoid --py-debug build from 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 sys.platform == 'darwin' and sys.executable == '/usr/bin/python': + sys.executable += sys.version[:3] + os.environ['ARGV0']=sys.executable + os.execl(sys.executable,sys.executable+'(mitogen:CONTEXT_NAME)') + os.write(1,'MITO000\n'.encode()) + C=_(os.fdopen(0,'rb').read(PREAMBLE_COMPRESSED_LEN),'zip') + fp=os.fdopen(W,'wb',0) + fp.write(C) + fp.close() + fp=os.fdopen(w,'wb',0) + fp.write(C) + fp.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): + source = inspect.getsource(self._first_stage) + source = textwrap.dedent('\n'.join(source.strip().split('\n')[2:])) + source = source.replace(' ', '\t') + source = source.replace('CONTEXT_NAME', self.options.remote_name) + preamble_compressed = self.get_preamble() + source = source.replace('PREAMBLE_COMPRESSED_LEN', + str(len(preamble_compressed))) + compressed = zlib.compress(source.encode(), 9) + encoded = codecs.encode(compressed, 'base64').replace(b('\n'), b('')) + # We can't use bytes.decode() in 3.x since it was restricted to always + # return unicode, so codecs.decode() is used instead. In 3.x + # codecs.decode() requires a bytes object. Since we must be compatible + # with 2.4 (no bytes literal), an extra .encode() either returns the + # same str (2.x) or an equivalent bytes (3.x). + return self.get_python_argv() + [ + '-c', + 'import codecs,os,sys;_=codecs.decode;' + 'exec(_(_("%s".encode(),"base64"),"zip"))' % (encoded.decode(),) + ] + + 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) + return { + 'parent_ids': parent_ids, + 'context_id': self.context.context_id, + 'debug': self.options.debug, + 'profiling': self.options.profiling, + 'unidirectional': self.options.unidirectional, + 'log_level': get_log_level(), + 'whitelist': self._router.get_module_whitelist(), + 'blacklist': self._router.get_module_blacklist(), + '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() + 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() + 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.fileno(), + self.proc.stdout.fileno(), + self.proc.stderr and self.proc.stderr.fileno()) + + 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(xrange(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(xrange(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.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 get_module_blacklist(self): + if mitogen.context_id == 0: + return self.responder.blacklist + return self.importer.master_blacklist + + def get_module_whitelist(self): + if mitogen.context_id == 0: + return self.responder.whitelist + return self.importer.master_whitelist + + 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 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) + + +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: + os.kill(self.proc.pid, 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 + for _ in xrange(count): + delay *= 1.72 + return delay + + 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() + + +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() + + +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, + ) + ) diff --git a/mitogen/mitogen/profiler.py b/mitogen/mitogen/profiler.py new file mode 100644 index 0000000..bbf6086 --- /dev/null +++ b/mitogen/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/mitogen/mitogen/select.py b/mitogen/mitogen/select.py new file mode 100644 index 0000000..2d87574 --- /dev/null +++ b/mitogen/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