Skip to content

Commit 3656979

Browse files
authored
Merge pull request #1307 from moreati/issue1306-investigate
mitogen: Fix non-blocking IO errors in first stage of bootstrap
2 parents 885c6de + 85d6046 commit 3656979

File tree

12 files changed

+149
-49
lines changed

12 files changed

+149
-49
lines changed

.ci/ci_lib.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
'MITOGEN_TEST_IMAGE_TEMPLATE',
4242
'ghcr.io/mitogen-hq/%(distro)s-test:2021',
4343
)
44+
SUDOERS_DEFAULTS_SRC = './tests/image_prep/files/sudoers_defaults'
45+
SUDOERS_DEFAULTS_DEST = '/etc/sudoers.d/mitogen_test_defaults'
4446
TESTS_SSH_PRIVATE_KEY_FILE = os.path.join(GIT_ROOT, 'tests/data/docker/mitogen__has_sudo_pubkey.key')
4547

4648

@@ -58,6 +60,7 @@ def _have_cmd(args):
5860
try:
5961
subprocess.run(
6062
args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
63+
check=True,
6164
)
6265
except OSError as exc:
6366
if exc.errno == errno.ENOENT:

.ci/mitogen_tests.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Run the Mitogen tests.
33

44
import os
5+
import subprocess
56

67
import ci_lib
78

@@ -13,6 +14,14 @@
1314
if not ci_lib.have_docker():
1415
os.environ['SKIP_DOCKER_TESTS'] = '1'
1516

17+
subprocess.check_call(
18+
"umask 0022; sudo cp '%s' '%s'"
19+
% (ci_lib.SUDOERS_DEFAULTS_SRC, ci_lib.SUDOERS_DEFAULTS_DEST),
20+
shell=True,
21+
)
22+
subprocess.check_call(['sudo', 'visudo', '-cf', ci_lib.SUDOERS_DEFAULTS_DEST])
23+
subprocess.check_call(['sudo', '-l'])
24+
1625
interesting = ci_lib.get_interesting_procs()
1726
ci_lib.run('./run_tests -v')
1827
ci_lib.check_stray_processes(interesting)

docs/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ To avail of fixes in an unreleased version, please download a ZIP file
2121
In progress (unreleased)
2222
------------------------
2323

24+
* :gh:issue:`1306` :mod:`ansible_mitogen`: Fix non-blocking IO errors in
25+
first stage of bootstrap
26+
* :gh:issue:`1306` CI: Report sudo version on Ansible targets
27+
* :gh:issue:`1306` CI: Move sudo test users defaults into ``/etc/sudoers.d``
28+
* :gh:issue:`1306` preamble_size: Fix variability of measured command size
29+
* :gh:issue:`1306` tests: Count bytes written in ``stdio_test.StdIOTest``
30+
* :gh:issue:`1306` tests: Check stdio is blocking in sudo contexts
31+
2432

2533
v0.3.27 (2025-08-20)
2634
--------------------

mitogen/parent.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,9 +1440,9 @@ def _first_stage():
14401440
os.environ['ARGV0']=sys.executable
14411441
os.execl(sys.executable,sys.executable+'(mitogen:CONTEXT_NAME)')
14421442
os.write(1,'MITO000\n'.encode())
1443-
fp=os.fdopen(0,'rb')
1444-
C=zlib.decompress(fp.read(PREAMBLE_COMPRESSED_LEN))
1445-
fp.close()
1443+
C=''.encode()
1444+
while PREAMBLE_COMPRESSED_LEN-len(C)and select.select([0],[],[]):C+=os.read(0,PREAMBLE_COMPRESSED_LEN-len(C))
1445+
C=zlib.decompress(C)
14461446
fp=os.fdopen(W,'wb',0)
14471447
fp.write(C)
14481448
fp.close()
@@ -1478,11 +1478,12 @@ def get_boot_command(self):
14781478

14791479
# Just enough to decode, decompress, and exec the first stage.
14801480
# Priorities: wider compatibility, faster startup, shorter length.
1481-
# `import os` here, instead of stage 1, to save a few bytes.
14821481
# `sys.path=...` for https://github.com/python/cpython/issues/115911.
1482+
# `import os,select` here (not stage 1) to save a few bytes overall.
14831483
return self.get_python_argv() + [
14841484
'-c',
1485-
'import sys;sys.path=[p for p in sys.path if p];import binascii,os,zlib;'
1485+
'import sys;sys.path=[p for p in sys.path if p];'
1486+
'import binascii,os,select,zlib;'
14861487
'exec(zlib.decompress(binascii.a2b_base64("%s")))' % (encoded.decode(),),
14871488
]
14881489

preamble_size.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
import zlib
1010

11+
import mitogen.core
1112
import mitogen.fakessh
1213
import mitogen.fork
1314
import mitogen.master
@@ -18,14 +19,28 @@
1819
import mitogen.ssh
1920
import mitogen.sudo
2021

22+
23+
class Table(object):
24+
HEADERS = (' ', 'Original', 'Minimized', 'Compressed')
25+
HEAD_FMT = '{:20} {:^15} {:^19} {:^19}'
26+
ROW_FMT = '%-20s %6i %5.1fKiB %5i %4.1fKiB %4.1f%% %5i %4.1fKiB %4.1f%%'
27+
28+
def header(self):
29+
return self.HEAD_FMT.format(*self.HEADERS)
30+
31+
2132
router = mitogen.master.Router()
2233
context = mitogen.parent.Context(router, 0)
23-
options = mitogen.ssh.Options(max_message_size=0, hostname='foo')
34+
options = mitogen.ssh.Options(
35+
hostname='foo',
36+
max_message_size=0,
37+
remote_name='alice@host:1234',
38+
)
2439
conn = mitogen.ssh.Connection(options, router)
2540
conn.context = context
2641

2742
print('SSH command size: %s' % (len(' '.join(conn.get_boot_command())),))
28-
print('Bootstrap (mitogen.core) size: %s (%.2fKiB)' % (
43+
print('Preamble (mitogen.core + econtext) size: %s (%.2fKiB)' % (
2944
len(conn.get_preamble()),
3045
len(conn.get_preamble()) / 1024.0,
3146
))
@@ -36,17 +51,10 @@
3651
exit()
3752

3853

39-
print(
40-
' '
41-
' '
42-
' Original '
43-
' '
44-
' Minimized '
45-
' '
46-
' Compressed '
47-
)
48-
54+
table = Table()
55+
print(table.header())
4956
for mod in (
57+
mitogen.core,
5058
mitogen.parent,
5159
mitogen.fork,
5260
mitogen.ssh,
@@ -63,13 +71,7 @@
6371
compressed = zlib.compress(minimized.encode(), 9)
6472
compressed_size = len(compressed)
6573
print(
66-
'%-25s'
67-
' '
68-
'%5i %4.1fKiB'
69-
' '
70-
'%5i %4.1fKiB %.1f%%'
71-
' '
72-
'%5i %4.1fKiB %.1f%%'
74+
table.ROW_FMT
7375
% (
7476
mod.__name__,
7577
original_size,

tests/ansible/setup/report_targets.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,23 @@
1313
- debug: {var: ansible_facts.osversion}
1414
- debug: {var: ansible_facts.python}
1515
- debug: {var: ansible_facts.system}
16+
17+
- name: Check target versions
18+
hosts: localhost:test-targets
19+
check_mode: false
20+
tasks:
21+
- name: Get command versions
22+
command:
23+
cmd: "{{ item.cmd }}"
24+
changed_when: false
25+
check_mode: false
26+
loop:
27+
- cmd: sudo -V
28+
register: command_versions
29+
30+
- name: Show command versions
31+
debug:
32+
msg: |
33+
cmd: {{ item.item.cmd }}
34+
{{ item.stdout }}
35+
loop: "{{ command_versions.results }}"

tests/data/stdio_checks.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,24 @@
33
import sys
44

55

6+
def _shout_stdout_py3(size):
7+
nwritten = sys.stdout.write('A' * size)
8+
return nwritten
9+
10+
11+
def _shout_stdout_py2(size):
12+
shout = 'A' * size
13+
nwritten = 0
14+
while nwritten < size:
15+
nwritten += os.write(sys.stdout.fileno(), shout[-nwritten:])
16+
return nwritten
17+
18+
619
def shout_stdout(size):
7-
sys.stdout.write('A' * size)
8-
return 'success'
20+
if sys.version_info > (3, 0):
21+
return _shout_stdout_py3(size)
22+
else:
23+
return _shout_stdout_py2(size)
924

1025

1126
def file_is_blocking(fobj):

tests/first_stage_test.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import subprocess
22

3+
import mitogen.core
34
import mitogen.parent
45
from mitogen.core import b
56

@@ -21,14 +22,18 @@ def test_valid_syntax(self):
2122
conn.context = mitogen.core.Context(None, 123)
2223
args = conn.get_boot_command()
2324

24-
# Executing the boot command will print "EC0" and expect to read from
25-
# stdin, which will fail because it's pointing at /dev/null, causing
26-
# the forked child to crash with an EOFError and disconnect its write
27-
# pipe. The forked and freshly execed parent will get a 0-byte read
28-
# from the pipe, which is a valid script, and therefore exit indicating
29-
# success.
25+
# The boot command should write an ECO marker to stdout, read the
26+
# preamble from stdin, then execute it.
3027

31-
fp = open("/dev/null", "r")
28+
# This test attaches /dev/zero to stdin to create a specific failure
29+
# 1. Fork child reads PREAMBLE_COMPRESSED_LEN bytes of junk (all `\0`)
30+
# 2. Fork child crashes (trying to decompress the junk data)
31+
# 3. Fork child's file descriptors (write pipes) are closed by the OS
32+
# 4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
33+
# 5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
34+
# 6. Python runs `''` (a valid script) and exits with success
35+
36+
fp = open("/dev/zero", "r")
3237
try:
3338
proc = subprocess.Popen(args,
3439
stdin=fp,
@@ -39,6 +44,9 @@ def test_valid_syntax(self):
3944
self.assertEqual(0, proc.returncode)
4045
self.assertEqual(stdout,
4146
mitogen.parent.BootstrapProtocol.EC0_MARKER+b('\n'))
42-
self.assertIn(b("Error -5 while decompressing data"), stderr)
47+
self.assertIn(
48+
b("Error -3 while decompressing data"), # Unknown compression method
49+
stderr,
50+
)
4351
finally:
4452
fp.close()

tests/image_prep/_user_accounts.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,14 @@
157157
owner: mitogen__has_sudo_pubkey
158158
group: mitogen__group
159159

160-
- name: Configure sudoers defaults
161-
blockinfile:
162-
path: /etc/sudoers
163-
marker: "# {mark} Mitogen test defaults"
164-
block: |
165-
Defaults>mitogen__pw_required targetpw
166-
Defaults>mitogen__require_tty requiretty
167-
Defaults>mitogen__require_tty_pw_required requiretty,targetpw
160+
- name: Configure sudoers
161+
copy:
162+
src: "{{ item.src }}"
163+
dest: "{{ item.dest }}"
164+
mode: ug=r,o=
168165
validate: '/usr/sbin/visudo -cf %s'
166+
with_items:
167+
- {src: sudoers_defaults, dest: /etc/sudoers.d/mitogen_test_defaults}
169168

170169
- name: Configure sudoers users
171170
blockinfile:
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Testing non-blocking stdio during bootstrap
2+
# https://github.com/mitogen-hq/mitogen/issues/1306
3+
Defaults log_output
4+
5+
Defaults>mitogen__pw_required targetpw
6+
Defaults>mitogen__require_tty requiretty
7+
Defaults>mitogen__require_tty_pw_required requiretty,targetpw

0 commit comments

Comments
 (0)