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