diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac00d84b..18411b35 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,23 +25,17 @@ jobs: fail-fast: false matrix: include: - - name: Ans_27_210 - tox_env: py27-mode_ansible-ansible2.10 - - name: Ans_27_4 - tox_env: py27-mode_ansible-ansible4 + - tox_env: py27-m_ans-ans2.10 + - tox_env: py27-m_ans-ans4 - - name: Ans_36_210 + - tox_env: py36-m_ans-ans2.10 python_version: '3.6' - tox_env: py36-mode_ansible-ansible2.10 - - name: Ans_36_4 + - tox_env: py36-m_ans-ans4 python_version: '3.6' - tox_env: py36-mode_ansible-ansible4 - - name: Mito_27 - tox_env: py27-mode_mitogen - - name: Mito_36 + - tox_env: py27-m_mtg + - tox_env: py36-m_mtg python_version: '3.6' - tox_env: py36-mode_mitogen steps: - uses: actions/checkout@v4 @@ -98,7 +92,7 @@ jobs: run: | set -o errexit -o nounset -o pipefail - # Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12) + # Tox environment name (e.g. py312-m_mtg) -> Python executable name (e.g. python3.12) PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "${{ matrix.tox_env }}"))') if [[ -z $PYTHON ]]; then @@ -123,7 +117,7 @@ jobs: run: | set -o errexit -o nounset -o pipefail - # Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12) + # Tox environment name (e.g. py312-m_mtg) -> Python executable name (e.g. python3.12) PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "${{ matrix.tox_env }}"))') if [[ -z $PYTHON ]]; then @@ -147,50 +141,36 @@ jobs: fail-fast: false matrix: include: - - name: Ans_311_210 + - tox_env: py311-m_ans-ans2.10 python_version: '3.11' - tox_env: py311-mode_ansible-ansible2.10 - - name: Ans_311_3 + - tox_env: py311-m_ans-ans3 python_version: '3.11' - tox_env: py311-mode_ansible-ansible3 - - name: Ans_311_4 + - tox_env: py311-m_ans-ans4 python_version: '3.11' - tox_env: py311-mode_ansible-ansible4 - - name: Ans_311_5 + - tox_env: py311-m_ans-ans5 python_version: '3.11' - tox_env: py311-mode_ansible-ansible5 - - name: Ans_313_6 + - tox_env: py313-m_ans-ans6 python_version: '3.13' - tox_env: py313-mode_ansible-ansible6 - - name: Ans_313_7 + - tox_env: py313-m_ans-ans7 python_version: '3.13' - tox_env: py313-mode_ansible-ansible7 - - name: Ans_313_8 + - tox_env: py313-m_ans-ans8 python_version: '3.13' - tox_env: py313-mode_ansible-ansible8 - - name: Ans_313_9 + - tox_env: py313-m_ans-ans9 python_version: '3.13' - tox_env: py313-mode_ansible-ansible9 - - name: Ans_313_10 + - tox_env: py313-m_ans-ans10 python_version: '3.13' - tox_env: py313-mode_ansible-ansible10 - - name: Ans_313_11 + - tox_env: py313-m_ans-ans11 python_version: '3.13' - tox_env: py313-mode_ansible-ansible11 - - name: Ans_313_12 + - tox_env: py313-m_ans-ans12 python_version: '3.13' - tox_env: py313-mode_ansible-ansible12 - - name: Van_313_11 + - tox_env: py313-m_ans-ans11-s_lin python_version: '3.13' - tox_env: py313-mode_ansible-ansible11-strategy_linear - - name: Van_313_12 + - tox_env: py313-m_ans-ans12-s_lin python_version: '3.13' - tox_env: py313-mode_ansible-ansible12-strategy_linear - - name: Mito_313 + - tox_env: py313-m_mtg python_version: '3.13' - tox_env: py313-mode_mitogen steps: - uses: actions/checkout@v4 @@ -234,7 +214,7 @@ jobs: run: | set -o errexit -o nounset -o pipefail - # Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12) + # Tox environment name (e.g. py312-m_mtg) -> Python executable name (e.g. python3.12) PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "${{ matrix.tox_env }}"))') if [[ -z $PYTHON ]]; then @@ -250,7 +230,7 @@ jobs: run: | set -o errexit -o nounset -o pipefail - # Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12) + # Tox environment name (e.g. py312-m_mtg) -> Python executable name (e.g. python3.12) PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "${{ matrix.tox_env }}"))') if [[ -z $PYTHON ]]; then @@ -270,22 +250,14 @@ jobs: fail-fast: false matrix: include: - - name: Mito_313 - tox_env: py313-mode_mitogen - - - name: Loc_313_11 + - tox_env: py313-m_lcl-ans11 sshpass_version: "1.10" - tox_env: py313-mode_localhost-ansible11 - - - name: Van_313_11 + - tox_env: py313-m_lcl-ans11-s_lin sshpass_version: "1.10" - tox_env: py313-mode_localhost-ansible11-strategy_linear - - - name: Loc_313_12 - tox_env: py313-mode_localhost-ansible12 + - tox_env: py313-m_lcl-ans12 + - tox_env: py313-m_lcl-ans12-s_lin - - name: Van_313_12 - tox_env: py313-mode_localhost-ansible12-strategy_linear + - tox_env: py313-m_mtg steps: - uses: actions/checkout@v4 @@ -325,7 +297,7 @@ jobs: run: | set -o errexit -o nounset -o pipefail - # Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12) + # Tox environment name (e.g. py312-m_mtg) -> Python executable name (e.g. python3.12) PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "${{ matrix.tox_env }}"))') if [[ -z $PYTHON ]]; then @@ -341,7 +313,7 @@ jobs: run: | set -o errexit -o nounset -o pipefail - # Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12) + # Tox environment name (e.g. py312-m_mtg) -> Python executable name (e.g. python3.12) PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "${{ matrix.tox_env }}"))') if [[ -z $PYTHON ]]; then diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 03856f4d..5b9ae70b 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -409,6 +409,7 @@ def _connect_mitogen_doas(spec): #: generating ContextService keyword arguments matching a connection #: specification. CONNECTION_METHOD = { + # Ansible connection plugins 'buildah': _connect_buildah, 'docker': _connect_docker, 'kubectl': _connect_kubectl, @@ -421,9 +422,14 @@ CONNECTION_METHOD = { 'setns': _connect_setns, 'ssh': _connect_ssh, 'smart': _connect_ssh, # issue #548. + + # Ansible become plugins + 'community.general.doas': _connect_doas, 'su': _connect_su, 'sudo': _connect_sudo, 'doas': _connect_doas, + + # Mitogen specific methods 'mitogen_su': _connect_mitogen_su, 'mitogen_sudo': _connect_mitogen_sudo, 'mitogen_doas': _connect_mitogen_doas, diff --git a/ansible_mitogen/process.py b/ansible_mitogen/process.py index 7ec70f2a..897ef4f0 100644 --- a/ansible_mitogen/process.py +++ b/ansible_mitogen/process.py @@ -426,7 +426,7 @@ class ClassicWorkerModel(WorkerModel): common_setup(_init_logging=_init_logging) - self.parent_sock, self.child_sock = socket.socketpair() + self.parent_sock, self.child_sock = mitogen.core.socketpair() mitogen.core.set_cloexec(self.parent_sock.fileno()) mitogen.core.set_cloexec(self.child_sock.fileno()) diff --git a/docs/changelog.rst b/docs/changelog.rst index 61b29bd4..8ce993d5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,15 @@ To avail of fixes in an unreleased version, please download a ZIP file `directly from GitHub `_. +v0.3.26 (2025-08-04) +-------------------- + +* :gh:issue:`1318` CI: Abbreviate Github Actions job names +* :gh:issue:`1309` :mod:`ansible_mitogen`: Fix ``become_method: doas`` +* :gh:issue:`712` :mod:`mitogen`: Fix :exc:`BlockingIOError` & ``EAGAIN`` + errors in subprocesses that write to stdio + + v0.3.25 (2025-07-29) -------------------- diff --git a/mitogen/__init__.py b/mitogen/__init__.py index 7133b66c..55212205 100644 --- a/mitogen/__init__.py +++ b/mitogen/__init__.py @@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup. #: Library version as a tuple. -__version__ = (0, 3, 25) +__version__ = (0, 3, 26) #: This is :data:`False` in slave contexts. Previously it was used to prevent diff --git a/mitogen/core.py b/mitogen/core.py index 5be36a95..a548c72f 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -87,6 +87,17 @@ import warnings import weakref import zlib +if sys.version_info > (3,5): + from os import get_blocking, set_blocking +else: + def get_blocking(fd): + return not fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK + + def set_blocking(fd, blocking): + fl = fcntl.fcntl(fd, fcntl.F_GETFL) + if blocking: fcntl.fcntl(fd, fcntl.F_SETFL, fl & ~os.O_NONBLOCK) + else: fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) + try: # Python >= 3.4, PEP 451 ModuleSpec API import importlib.machinery @@ -559,26 +570,6 @@ def set_cloexec(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`, @@ -720,7 +711,7 @@ def import_module(modname): return __import__(modname, None, None, ['']) -def pipe(): +def pipe(blocking=None): """ Create a UNIX pipe pair using :func:`os.pipe`, wrapping the returned descriptors in Python file objects in order to manage their lifetime and @@ -728,12 +719,22 @@ def pipe(): not been closed explicitly. """ rfd, wfd = os.pipe() + for fd in rfd, wfd: + if blocking is not None: set_blocking(fd, blocking) # noqa: E701 return ( os.fdopen(rfd, 'rb', 0), os.fdopen(wfd, 'wb', 0) ) +def socketpair(blocking=None): + fp1, fp2 = socket.socketpair() + for fp in fp1, fp2: + fd = fp.fileno() + if blocking is not None: set_blocking(fd, blocking) # noqa: E701 + return fp1, fp2 + + def iter_split(buf, delim, func): """ Invoke `func(s)` for each `delim`-delimited chunk in the potentially large @@ -1879,8 +1880,7 @@ class Stream(object): """ 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. + :class:`Side` will call :func:`set_cloexec` on them. The same file object may be used for both sides. The default :meth:`on_disconnect` is handles the possibility that only one @@ -2155,14 +2155,11 @@ class Side(object): :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): + def __init__(self, stream, fp, cloexec=True, keep_alive=True): #: The :class:`Stream` for which this is a read or write side. self.stream = stream # File or socket object responsible for the lifetime of its underlying @@ -2180,8 +2177,6 @@ class Side(object): self._fork_refs[id(self)] = self if cloexec: set_cloexec(self.fd) - if not blocking: - set_nonblock(self.fd) def __repr__(self): return '' % ( @@ -2785,7 +2780,7 @@ class Latch(object): try: return self._cls_idle_socketpairs.pop() # pop() must be atomic except IndexError: - rsock, wsock = socket.socketpair() + rsock, wsock = socketpair() rsock.setblocking(False) set_cloexec(rsock.fileno()) set_cloexec(wsock.fileno()) @@ -2958,7 +2953,8 @@ class Waker(Protocol): @classmethod def build_stream(cls, broker): stream = super(Waker, cls).build_stream(broker) - stream.accept(*pipe()) + rfp, wfp = pipe(blocking=False) + stream.accept(rfp, wfp) return stream def __init__(self, broker): @@ -3056,7 +3052,8 @@ class IoLoggerProtocol(DelimitedProtocol): prevent break :meth:`on_shutdown` from calling :meth:`shutdown() ` on it. """ - rsock, wsock = socket.socketpair() + # Leave wsock & dest_fd blocking, so the subprocess will have sane stdio + rsock, wsock = socketpair() os.dup2(wsock.fileno(), dest_fd) stream = super(IoLoggerProtocol, cls).build_stream(name) stream.name = name @@ -4038,6 +4035,9 @@ class ExternalContext(object): local_id=self.config['context_id'], parent_ids=self.config['parent_ids'] ) + for f in in_fp, out_fp: + fd = f.fileno() + set_blocking(fd, False) self.stream.accept(in_fp, out_fp) self.stream.name = 'parent' self.stream.receive_side.keep_alive = False diff --git a/mitogen/fakessh.py b/mitogen/fakessh.py index a2a8da15..23599903 100644 --- a/mitogen/fakessh.py +++ b/mitogen/fakessh.py @@ -179,6 +179,9 @@ class Process(object): self.control_handle = router.add_handler(self._on_control) self.stdin_handle = router.add_handler(self._on_stdin) self.pump = IoPump.build_stream(router.broker) + for fp in stdin, stdout: + fd = fp.fileno() + mitogen.core.set_blocking(fd, False) self.pump.accept(stdin, stdout) self.stdin = None self.control = None @@ -419,10 +422,11 @@ def run(dest, router, args, deadline=None, econtext=None): fakessh = mitogen.parent.Context(router, context_id) fakessh.name = u'fakessh.%d' % (context_id,) - sock1, sock2 = socket.socketpair() + sock1, sock2 = mitogen.core.socketpair() stream = mitogen.core.Stream(router, context_id) stream.name = u'fakessh' + mitogen.core.set_blocking(sock1.fileno(), False) stream.accept(sock1, sock1) router.register(fakessh, stream) diff --git a/mitogen/fork.py b/mitogen/fork.py index f0c2d7e7..d77ed6f2 100644 --- a/mitogen/fork.py +++ b/mitogen/fork.py @@ -211,7 +211,7 @@ class Connection(mitogen.parent.Connection): on_fork() if self.options.on_fork: self.options.on_fork() - mitogen.core.set_block(childfp.fileno()) + mitogen.core.set_blocking(childfp.fileno(), True) childfp.send(b('MITO002\n')) diff --git a/mitogen/os_fork.py b/mitogen/os_fork.py index 9c649d07..f85534bc 100644 --- a/mitogen/os_fork.py +++ b/mitogen/os_fork.py @@ -38,6 +38,7 @@ import sys import weakref import mitogen.core +import mitogen.parent # List of weakrefs. On Python 2.4, mitogen.core registers its Broker on this @@ -131,9 +132,9 @@ class Corker(object): `obj` to be written to by one of its threads. """ rsock, wsock = mitogen.parent.create_socketpair(size=4096) + mitogen.core.set_blocking(wsock.fileno(), True) # gevent mitogen.core.set_cloexec(rsock.fileno()) mitogen.core.set_cloexec(wsock.fileno()) - mitogen.core.set_block(wsock) # gevent self._rsocks.append(rsock) obj.defer(self._do_cork, s, wsock) diff --git a/mitogen/parent.py b/mitogen/parent.py index 7c56c734..7ac14fb1 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -265,7 +265,7 @@ def disable_echo(fd): termios.tcsetattr(fd, flags, new) -def create_socketpair(size=None): +def create_socketpair(size=None, blocking=None): """ Create a :func:`socket.socketpair` for use as a child's UNIX stdio channels. As socketpairs are bidirectional, they are economical on file @@ -276,14 +276,14 @@ def create_socketpair(size=None): if size is None: size = mitogen.core.CHUNK_SIZE - parentfp, childfp = socket.socketpair() + parentfp, childfp = mitogen.core.socketpair(blocking) for fp in parentfp, childfp: fp.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, size) return parentfp, childfp -def create_best_pipe(escalates_privilege=False): +def create_best_pipe(escalates_privilege=False, blocking=None): """ By default we prefer to communicate with children over a UNIX socket, as a single file descriptor can represent bidirectional communication, and a @@ -301,16 +301,19 @@ def create_best_pipe(escalates_privilege=False): :param bool escalates_privilege: If :data:`True`, the target program may escalate privileges, causing SELinux to disconnect AF_UNIX sockets, so avoid those. + :param None|bool blocking: + If :data:`False` or :data:`True`, set non-blocking or blocking mode. + If :data:`None` (default), use default. :returns: `(parent_rfp, child_wfp, child_rfp, parent_wfp)` """ if (not escalates_privilege) or (not SELINUX_ENABLED): - parentfp, childfp = create_socketpair() + parentfp, childfp = create_socketpair(blocking=blocking) return parentfp, childfp, childfp, parentfp - parent_rfp, child_wfp = mitogen.core.pipe() + parent_rfp, child_wfp = mitogen.core.pipe(blocking) try: - child_rfp, parent_wfp = mitogen.core.pipe() + child_rfp, parent_wfp = mitogen.core.pipe(blocking) return parent_rfp, child_wfp, child_rfp, parent_wfp except: parent_rfp.close() @@ -481,7 +484,7 @@ def openpty(): if not IS_SOLARIS: disable_echo(master_fd) disable_echo(slave_fd) - mitogen.core.set_block(slave_fd) + mitogen.core.set_blocking(slave_fd, True) return master_fp, slave_fp @@ -547,8 +550,8 @@ def hybrid_tty_create_child(args, escalates_privilege=False): escalates_privilege=escalates_privilege, ) try: - mitogen.core.set_block(child_rfp) - mitogen.core.set_block(child_wfp) + mitogen.core.set_blocking(child_rfp.fileno(), True) + mitogen.core.set_blocking(child_wfp.fileno(), True) proc = popen( args=args, stdin=child_rfp, @@ -1643,6 +1646,9 @@ class Connection(object): stream = self.stream_factory() stream.conn = self stream.name = self.options.name or self._get_name() + for fp in self.proc.stdout, self.proc.stdin: + fd = fp.fileno() + mitogen.core.set_blocking(fd, False) stream.accept(self.proc.stdout, self.proc.stdin) mitogen.core.listen(stream, 'disconnect', self.on_stdio_disconnect) @@ -1653,6 +1659,8 @@ class Connection(object): stream = self.stderr_stream_factory() stream.conn = self stream.name = self.options.name or self._get_name() + fd = self.proc.stderr.fileno() + mitogen.core.set_blocking(fd, False) stream.accept(self.proc.stderr, self.proc.stderr) mitogen.core.listen(stream, 'disconnect', self.on_stderr_disconnect) @@ -2555,9 +2563,8 @@ class Reaper(object): relatively conservative retries. """ delay = 0.05 - for _ in xrange(count): - delay *= 1.72 - return delay + factor = 1.72 + return delay * factor ** count def _on_broker_shutdown(self): """ diff --git a/mitogen/unix.py b/mitogen/unix.py index b241a403..84eedc4b 100644 --- a/mitogen/unix.py +++ b/mitogen/unix.py @@ -111,6 +111,7 @@ class Listener(mitogen.core.Protocol): sock.listen(backlog) stream = super(Listener, cls).build_stream(router, path) + mitogen.core.set_blocking(sock.fileno(), False) stream.accept(sock, sock) router.broker.start_receive(stream) return stream @@ -169,6 +170,7 @@ class Listener(mitogen.core.Protocol): auth_id=mitogen.context_id, ) stream.name = u'unix_client.%d' % (pid,) + mitogen.core.set_blocking(sock.fileno(), False) stream.accept(sock, sock) LOG.debug('listener: accepted connection from PID %d: %s', pid, stream.name) diff --git a/tests/ansible/integration/become/all.yml b/tests/ansible/integration/become/all.yml index 1b507e16..2f22acd1 100644 --- a/tests/ansible/integration/become/all.yml +++ b/tests/ansible/integration/become/all.yml @@ -1,4 +1,5 @@ +- import_playbook: doas.yml - import_playbook: su_password.yml - import_playbook: sudo_flags_failure.yml - import_playbook: sudo_nonexistent.yml diff --git a/tests/ansible/integration/become/doas.yml b/tests/ansible/integration/become/doas.yml new file mode 100644 index 00000000..31858168 --- /dev/null +++ b/tests/ansible/integration/become/doas.yml @@ -0,0 +1,91 @@ +- name: integration/become/doas.yml - unqualified + hosts: test-targets:&linux_containers + gather_facts: false + become_method: doas # noqa: schema[playbook] + vars: + ansible_become_password: has_sudo_nopw_password + tasks: + # Vanilla Ansible doas requires pipelining=false + # https://github.com/ansible-collections/community.general/issues/9977 + - include_tasks: ../_mitogen_only.yml + + - name: Test doas -> default target user + become: true + command: whoami + changed_when: false + check_mode: false + register: doas_default_user + + - assert: + that: + - doas_default_user.stdout == 'root' + fail_msg: + doas_default_user={{ doas_default_user }} + + - name: Test doas -> mitogen__user1 + become: true + become_user: mitogen__user1 + command: whoami + changed_when: false + check_mode: false + register: doas_mitogen__user1 + when: + - become_unpriv_available + + - assert: + that: + - doas_mitogen__user1.stdout == 'mitogen__user1' + fail_msg: + doas_mitogen__user1={{ doas_mitogen__user1 }} + when: + - become_unpriv_available + tags: + - doas + - issue_1309 + - mitogen_only + +- name: integration/become/doas.yml - FQCN + hosts: test-targets:&linux_containers + gather_facts: false + become_method: community.general.doas + vars: + ansible_become_password: has_sudo_nopw_password + tasks: + # Vanilla Ansible doas requires pipelining=false + # https://github.com/ansible-collections/community.general/issues/9977 + - include_tasks: ../_mitogen_only.yml + + - name: Test community.general.doas -> default target user + become: true + command: whoami + changed_when: false + check_mode: false + register: fq_doas_default_user + + - assert: + that: + - fq_doas_default_user.stdout == 'root' + fail_msg: + fq_doas_default_user={{ fq_doas_default_user }} + + - name: Test community.general.doas -> mitogen__user1 + become: true + become_user: mitogen__user1 + command: whoami + changed_when: false + check_mode: false + register: fq_doas_mitogen__user1 + when: + - become_unpriv_available + + - assert: + that: + - fq_doas_mitogen__user1.stdout == 'mitogen__user1' + fail_msg: + fq_doas_mitogen__user1={{ fq_doas_mitogen__user1 }} + when: + - become_unpriv_available + tags: + - doas + - issue_1309 + - mitogen_only diff --git a/tests/blocking_test.py b/tests/blocking_test.py new file mode 100644 index 00000000..ca9b791a --- /dev/null +++ b/tests/blocking_test.py @@ -0,0 +1,32 @@ +import os +import tempfile + +import mitogen.core + +import testlib + +class BlockingIOTest(testlib.TestCase): + def setUp(self): + super(BlockingIOTest, self).setUp() + self.fp = tempfile.TemporaryFile() + self.fd = self.fp.fileno() + + def tearDown(self): + self.fp.close() + super(BlockingIOTest, self).tearDown() + + def test_get_blocking(self): + if hasattr(os, 'get_blocking'): + self.assertEqual( + os.get_blocking(self.fd), mitogen.core.get_blocking(self.fd), + ) + self.assertTrue(mitogen.core.get_blocking(self.fd) is True) + + def test_set_blocking(self): + mitogen.core.set_blocking(self.fd, False) + if hasattr(os, 'get_blocking'): + self.assertEqual( + os.get_blocking(self.fd), mitogen.core.get_blocking(self.fd), + ) + self.assertTrue(mitogen.core.get_blocking(self.fd) is False) + diff --git a/tests/create_child_test.py b/tests/create_child_test.py index 57b04b3f..bbe59229 100644 --- a/tests/create_child_test.py +++ b/tests/create_child_test.py @@ -190,7 +190,7 @@ class TtyCreateChildTest(testlib.TestCase): proc = self.func([ 'bash', '-c', 'exec 2>%s; echo hi > /dev/tty' % (tf.name,) ]) - mitogen.core.set_block(proc.stdin.fileno()) + mitogen.core.set_blocking(proc.stdin.fileno(), True) # read(3) below due to https://bugs.python.org/issue37696 self.assertEqual(mitogen.core.b('hi\n'), proc.stdin.read(3)) waited_pid, status = os.waitpid(proc.pid, 0) diff --git a/tests/data/stdio_checks.py b/tests/data/stdio_checks.py new file mode 100644 index 00000000..5bfe8063 --- /dev/null +++ b/tests/data/stdio_checks.py @@ -0,0 +1,16 @@ +import fcntl +import os +import sys + + +def shout_stdout(size): + sys.stdout.write('A' * size) + return 'success' + + +def file_is_blocking(fobj): + return not (fcntl.fcntl(fobj.fileno(), fcntl.F_GETFL) & os.O_NONBLOCK) + + +def stdio_is_blocking(): + return [file_is_blocking(f) for f in [sys.stdin, sys.stdout, sys.stderr]] diff --git a/tests/pipe_test.py b/tests/pipe_test.py new file mode 100644 index 00000000..4dfb49de --- /dev/null +++ b/tests/pipe_test.py @@ -0,0 +1,35 @@ +import os + +import mitogen.core + +import testlib + +class PipeTest(testlib.TestCase): + def test_pipe_blocking_unspecified(self): + "Test that unspecified blocking arg (None) behaves same as os.pipe()" + os_rfd, os_wfd = os.pipe() + mi_rfp, mi_wfp = mitogen.core.pipe() + + self.assertEqual(mitogen.core.get_blocking(os_rfd), + mitogen.core.get_blocking(mi_rfp.fileno())) + self.assertEqual(mitogen.core.get_blocking(os_wfd), + mitogen.core.get_blocking(mi_wfp.fileno())) + mi_rfp.close() + mi_wfp.close() + os.close(os_rfd) + os.close(os_wfd) + + def test_pipe_blocking_true(self): + mi_rfp, mi_wfp = mitogen.core.pipe(blocking=True) + self.assertTrue(mitogen.core.get_blocking(mi_rfp.fileno())) + self.assertTrue(mitogen.core.get_blocking(mi_wfp.fileno())) + mi_rfp.close() + mi_wfp.close() + + def test_pipe_blocking_false(self): + mi_rfp, mi_wfp = mitogen.core.pipe(blocking=False) + self.assertFalse(mitogen.core.get_blocking(mi_rfp.fileno())) + self.assertFalse(mitogen.core.get_blocking(mi_wfp.fileno())) + mi_rfp.close() + mi_wfp.close() + diff --git a/tests/poller_test.py b/tests/poller_test.py index 0abc836d..01e4a561 100644 --- a/tests/poller_test.py +++ b/tests/poller_test.py @@ -28,15 +28,13 @@ class SockMixin(object): # buffers on both sides (bidirectional IO), making it easier to test # combinations of readability/writeability on the one side of a single # file object. - self.l1_sock, self.r1_sock = socket.socketpair() + self.l1_sock, self.r1_sock = mitogen.core.socketpair(blocking=False) self.l1 = self.l1_sock.fileno() self.r1 = self.r1_sock.fileno() - self.l2_sock, self.r2_sock = socket.socketpair() + self.l2_sock, self.r2_sock = mitogen.core.socketpair(blocking=False) self.l2 = self.l2_sock.fileno() self.r2 = self.r2_sock.fileno() - for fp in self.l1, self.r1, self.l2, self.r2: - mitogen.core.set_nonblock(fp) def fill(self, fd): """Make `fd` unwriteable.""" diff --git a/tests/socketpair_test.py b/tests/socketpair_test.py new file mode 100644 index 00000000..c60ca6c1 --- /dev/null +++ b/tests/socketpair_test.py @@ -0,0 +1,35 @@ +import socket + +import mitogen.core + +import testlib + +class SocketPairTest(testlib.TestCase): + def test_socketpair_blocking_unspecified(self): + "Test that unspecified blocking arg (None) batches socket.socketpair()" + sk_fp1, sk_fp2 = socket.socketpair() + mi_fp1, mi_fp2 = mitogen.core.socketpair() + + self.assertEqual(mitogen.core.get_blocking(sk_fp1.fileno()), + mitogen.core.get_blocking(mi_fp1.fileno())) + self.assertEqual(mitogen.core.get_blocking(sk_fp2.fileno()), + mitogen.core.get_blocking(mi_fp2.fileno())) + mi_fp1.close() + mi_fp2.close() + sk_fp1.close() + sk_fp2.close() + + def test_socketpair_blocking_true(self): + mi_fp1, mi_fp2 = mitogen.core.socketpair(blocking=True) + self.assertTrue(mitogen.core.get_blocking(mi_fp1.fileno())) + self.assertTrue(mitogen.core.get_blocking(mi_fp2.fileno())) + mi_fp1.close() + mi_fp2.close() + + def test_socketpair_blocking_false(self): + mi_fp1, mi_fp2 = mitogen.core.socketpair(blocking=False) + self.assertFalse(mitogen.core.get_blocking(mi_fp1.fileno())) + self.assertFalse(mitogen.core.get_blocking(mi_fp2.fileno())) + mi_fp1.close() + mi_fp2.close() + diff --git a/tests/stdio_test.py b/tests/stdio_test.py new file mode 100644 index 00000000..da8edd8e --- /dev/null +++ b/tests/stdio_test.py @@ -0,0 +1,28 @@ +import testlib + +import stdio_checks + + +class StdIOTest(testlib.RouterMixin, testlib.TestCase): + """ + Test that stdin, stdout, and stderr conform to common expectations, + such as blocking IO. + """ + def test_can_write_stdout_1_mib(self): + """ + Writing to stdout should not raise EAGAIN. Regression test for + https://github.com/mitogen-hq/mitogen/issues/712. + """ + size = 1 * 2**20 + context = self.router.local() + result = context.call(stdio_checks.shout_stdout, size) + self.assertEqual('success', result) + + def test_stdio_is_blocking(self): + context = self.router.local() + stdin_blocking, stdout_blocking, stderr_blocking = context.call( + stdio_checks.stdio_is_blocking, + ) + self.assertTrue(stdin_blocking) + self.assertTrue(stdout_blocking) + self.assertTrue(stderr_blocking) diff --git a/tox.ini b/tox.ini index 569d9936..9fa61dc6 100644 --- a/tox.ini +++ b/tox.ini @@ -55,10 +55,10 @@ [tox] envlist = init, - py{27,36}-mode_ansible-ansible{2.10,3,4}, - py{311}-mode_ansible-ansible{2.10,3,4,5}, - py{313}-mode_ansible-ansible{6,7,8,9,10,11,12}, - py{27,36,313}-mode_mitogen, + py{27,36}-m_ans-ans{2.10,3,4} + py{311}-m_ans-ans{2.10,3-5} + py{313}-m_ans-ans{6-12} + py{27,36,313}-m_mtg report, [testenv] @@ -76,28 +76,28 @@ basepython = py313: python3.13 deps = -r{toxinidir}/tests/requirements.txt - mode_ansible: -r{toxinidir}/tests/ansible/requirements.txt - ansible2.10: ansible~=2.10.0 - ansible3: ansible~=3.0 - ansible4: ansible~=4.0 - ansible5: ansible~=5.0 + m_ans: -r{toxinidir}/tests/ansible/requirements.txt + ans2.10: ansible~=2.10.0 + ans3: ansible~=3.0 + ans4: ansible~=4.0 + ans5: ansible~=5.0 # From Ansible 6 PyPI distributions include a wheel - ansible6: ansible~=6.0 - ansible7: ansible~=7.0 - ansible8: ansible~=8.0 - ansible9: ansible~=9.0 - ansible10: ansible~=10.0 - ansible11: ansible~=11.0 - ansible12: ansible>=12.0.0b2 + ans6: ansible~=6.0 + ans7: ansible~=7.0 + ans8: ansible~=8.0 + ans9: ansible~=9.0 + ans10: ansible~=10.0 + ans11: ansible~=11.0 + ans12: ansible>=12.0.0b2 install_command = python -m pip --no-python-version-warning --disable-pip-version-check install {opts} {packages} commands_pre = mode_debops_common: {toxinidir}/.ci/debops_common_install.py commands = - mode_ansible: {toxinidir}/.ci/ansible_tests.py + m_ans: {toxinidir}/.ci/ansible_tests.py mode_debops_common: {toxinidir}/.ci/debops_common_tests.py - mode_localhost: {toxinidir}/.ci/localhost_ansible_tests.py - mode_mitogen: {toxinidir}/.ci/mitogen_tests.py + m_lcl: {toxinidir}/.ci/localhost_ansible_tests.py + m_mtg: {toxinidir}/.ci/mitogen_tests.py passenv = ANSIBLE_* HOME @@ -111,20 +111,18 @@ setenv = PIP_CONSTRAINT={toxinidir}/tests/constraints.txt # Superceded in Ansible >= 6 (ansible-core >= 2.13) by result_format=yaml # Deprecated in Ansible 12 (ansible-core 2.19) - ansible{2.10,3-5}: DEFAULT_STDOUT_CALLBACK=yaml + ans{2.10,3,4,5}: ANSIBLE_STDOUT_CALLBACK=yaml # Print warning on the first occurence at each module:linenno in Mitogen. Available Python 2.7, 3.2+. PYTHONWARNINGS=default:::ansible_mitogen,default:::mitogen # Ansible 6 - 8 (ansible-core 2.13 - 2.15) require Python 2.7 or >= 3.5 on targets - ansible6: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 - ansible7: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 - ansible8: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 + ans{6,7,8}: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 # Ansible 9 (ansible-core 2.16) requires Python 2.7 or >= 3.6 on targets - ansible9: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1804 ubuntu2004 + ans9: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1804 ubuntu2004 # Ansible 10 (ansible-core 2.17) requires Python >= 3.7 on targets - ansible10: MITOGEN_TEST_DISTRO_SPECS=debian10-py3 debian11-py3 ubuntu2004-py3 + ans10: MITOGEN_TEST_DISTRO_SPECS=debian10-py3 debian11-py3 ubuntu2004-py3 # Ansible 11 (ansible-core 2.18) requires Python >= 3.8 on targets - ansible11: MITOGEN_TEST_DISTRO_SPECS=debian11-py3 ubuntu2004-py3 - ansible12: MITOGEN_TEST_DISTRO_SPECS=debian11-py3 ubuntu2004-py3 + ans11: MITOGEN_TEST_DISTRO_SPECS=debian11-py3 ubuntu2004-py3 + ans12: MITOGEN_TEST_DISTRO_SPECS=debian11-py3 ubuntu2004-py3 distros_centos: MITOGEN_TEST_DISTRO_SPECS=centos6 centos7 centos8 distros_centos5: MITOGEN_TEST_DISTRO_SPECS=centos5 distros_centos6: MITOGEN_TEST_DISTRO_SPECS=centos6 @@ -138,14 +136,14 @@ setenv = distros_ubuntu1604: MITOGEN_TEST_DISTRO_SPECS=ubuntu1604 distros_ubuntu1804: MITOGEN_TEST_DISTRO_SPECS=ubuntu1804 distros_ubuntu2004: MITOGEN_TEST_DISTRO_SPECS=ubuntu2004 - mode_ansible: MODE=ansible - mode_ansible: ANSIBLE_SKIP_TAGS=resource_intensive - mode_ansible: ANSIBLE_CALLBACK_WHITELIST=profile_tasks - mode_ansible: ANSIBLE_CALLBACKS_ENABLED=profile_tasks + m_ans: MODE=ansible + m_ans: ANSIBLE_SKIP_TAGS=resource_intensive + m_ans: ANSIBLE_CALLBACK_WHITELIST=profile_tasks + m_ans: ANSIBLE_CALLBACKS_ENABLED=profile_tasks mode_debops_common: MODE=debops_common - mode_localhost: ANSIBLE_SKIP_TAGS=issue_776,resource_intensive - mode_mitogen: MODE=mitogen - strategy_linear: ANSIBLE_STRATEGY=linear + m_lcl: ANSIBLE_SKIP_TAGS=issue_776,resource_intensive + m_mtg: MODE=mitogen + s_lin: ANSIBLE_STRATEGY=linear allowlist_externals = # Added: Tox 3.18: Tox 4.0+ *_install.py