mirror of
https://github.com/ansible/ansible
synced 2026-06-19 07:35:52 +00:00
(cherry picked from commit 776f90ae4b)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
bugfixes:
|
||||
- module_utils/basic.py - Fix ``AnsibleModule.run_command()`` to handle ``None`` return from non-blocking pipe reads (https://github.com/ansible/ansible/issues/86920).
|
||||
@@ -2092,7 +2092,13 @@ class AnsibleModule(object):
|
||||
stdout_changed = False
|
||||
for key, event in events:
|
||||
b_chunk = key.fileobj.read(32768)
|
||||
if not b_chunk and b_chunk is not None:
|
||||
if b_chunk is None:
|
||||
# Non-blocking read returned None (no data currently available).
|
||||
# This can happen with certain file-like objects or in edge cases.
|
||||
# Skip this chunk and try again on next select iteration.
|
||||
continue
|
||||
if not b_chunk:
|
||||
# Empty bytes received, EOF reached
|
||||
selector.unregister(key.fileobj)
|
||||
elif key.fileobj == cmd.stdout:
|
||||
stdout += b_chunk
|
||||
|
||||
@@ -255,3 +255,81 @@ def test_run_command_fds(mocker, rc_am):
|
||||
|
||||
assert subprocess_mock.Popen.call_args[1]['pass_fds'] == (101, 42)
|
||||
assert subprocess_mock.Popen.call_args[1]['close_fds'] is True
|
||||
|
||||
|
||||
class TestRunCommandNoneRead:
|
||||
"""
|
||||
Test handling of read() returning None from non-blocking pipes.
|
||||
|
||||
This tests the fix for issue #86920 where read() can return None
|
||||
in certain edge cases with non-blocking I/O, which would cause
|
||||
TypeError when trying to concatenate None to bytes.
|
||||
"""
|
||||
|
||||
class NoneReturningBytesIO(SpecialBytesIO):
|
||||
"""
|
||||
BytesIO that returns None on first read, then actual data.
|
||||
|
||||
This simulates edge cases where non-blocking read() returns None
|
||||
to indicate "no data available right now" rather than empty bytes.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Pop 'data' before calling super().__init__() since BytesIO doesn't accept it
|
||||
self.data = kwargs.pop('data', b'test output')
|
||||
self.read_count = 0
|
||||
super(TestRunCommandNoneRead.NoneReturningBytesIO, self).__init__(*args, **kwargs)
|
||||
|
||||
def read(self, size=-1):
|
||||
self.read_count += 1
|
||||
if self.read_count == 1:
|
||||
# First read returns None (no data available)
|
||||
return None
|
||||
elif self.read_count == 2:
|
||||
# Second read returns actual data
|
||||
return self.data
|
||||
else:
|
||||
# Subsequent reads return empty bytes (EOF)
|
||||
return b''
|
||||
|
||||
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
|
||||
def test_none_from_stdout_read(self, mocker, rc_am):
|
||||
"""Test that None returned from stdout.read() doesn't cause TypeError."""
|
||||
rc_am._subprocess._output = {
|
||||
mocker.sentinel.stdout:
|
||||
self.NoneReturningBytesIO(fh=mocker.sentinel.stdout, data=b'command output'),
|
||||
mocker.sentinel.stderr:
|
||||
SpecialBytesIO(b'', fh=mocker.sentinel.stderr)
|
||||
}
|
||||
(rc, stdout, stderr) = rc_am.run_command('/bin/test')
|
||||
assert rc == 0
|
||||
assert stdout == 'command output'
|
||||
assert stderr == ''
|
||||
|
||||
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
|
||||
def test_none_from_stderr_read(self, mocker, rc_am):
|
||||
"""Test that None returned from stderr.read() doesn't cause TypeError."""
|
||||
rc_am._subprocess._output = {
|
||||
mocker.sentinel.stdout:
|
||||
SpecialBytesIO(b'', fh=mocker.sentinel.stdout),
|
||||
mocker.sentinel.stderr:
|
||||
self.NoneReturningBytesIO(fh=mocker.sentinel.stderr, data=b'error output')
|
||||
}
|
||||
(rc, stdout, stderr) = rc_am.run_command('/bin/test')
|
||||
assert rc == 0
|
||||
assert stdout == ''
|
||||
assert stderr == 'error output'
|
||||
|
||||
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
|
||||
def test_none_from_both_pipes(self, mocker, rc_am):
|
||||
"""Test that None returned from both pipes doesn't cause TypeError."""
|
||||
rc_am._subprocess._output = {
|
||||
mocker.sentinel.stdout:
|
||||
self.NoneReturningBytesIO(fh=mocker.sentinel.stdout, data=b'stdout data'),
|
||||
mocker.sentinel.stderr:
|
||||
self.NoneReturningBytesIO(fh=mocker.sentinel.stderr, data=b'stderr data')
|
||||
}
|
||||
(rc, stdout, stderr) = rc_am.run_command('/bin/test')
|
||||
assert rc == 0
|
||||
assert stdout == 'stdout data'
|
||||
assert stderr == 'stderr data'
|
||||
|
||||
Reference in New Issue
Block a user