Skip to content

Commit f80f0cb

Browse files
authored
Use subprocess.Popen().wait(timeout=... for timeouts. Related #64 (#65)
Timeout can be float
1 parent 9dfc2bc commit f80f0cb

File tree

7 files changed

+31
-23
lines changed

7 files changed

+31
-23
lines changed

exec_helpers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
'ExecResult',
4747
)
4848

49-
__version__ = '2.0.0'
49+
__version__ = '2.0.1'
5050
__author__ = "Alexey Stepanov"
5151
__author_email__ = 'penguinolog@gmail.com'
5252
__maintainers__ = {

exec_helpers/_ssh_client_base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ def _exec_command(
654654
:type interface: paramiko.channel.Channel
655655
:type stdout: typing.Optional[paramiko.ChannelFile]
656656
:type stderr: typing.Optional[paramiko.ChannelFile]
657-
:type timeout: typing.Union[int, None]
657+
:type timeout: typing.Union[int, float, None]
658658
:type verbose: bool
659659
:param log_mask_re: regex lookup rule to mask command for logger.
660660
all MATCHED groups will be replaced by '<*masked*>'
@@ -744,7 +744,7 @@ def execute_through_host(
744744
auth: typing.Optional[ssh_auth.SSHAuth] = None,
745745
target_port: int = 22,
746746
verbose: bool = False,
747-
timeout: typing.Union[int, None] = constants.DEFAULT_TIMEOUT,
747+
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
748748
get_pty: bool = False,
749749
**kwargs: typing.Dict
750750
) -> exec_result.ExecResult:
@@ -761,7 +761,7 @@ def execute_through_host(
761761
:param verbose: Produce log.info records for command call and output
762762
:type verbose: bool
763763
:param timeout: Timeout for command execution.
764-
:type timeout: typing.Union[int, None]
764+
:type timeout: typing.Union[int, float, None]
765765
:param get_pty: open PTY on target machine
766766
:type get_pty: bool
767767
:rtype: ExecResult
@@ -822,7 +822,7 @@ def execute_together(
822822
cls,
823823
remotes: typing.Iterable['SSHClientBase'],
824824
command: str,
825-
timeout: typing.Union[int, None] = constants.DEFAULT_TIMEOUT,
825+
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
826826
expected: typing.Optional[typing.Iterable[int]] = None,
827827
raise_on_err: bool = True,
828828
**kwargs: typing.Dict
@@ -834,7 +834,7 @@ def execute_together(
834834
:param command: Command for execution
835835
:type command: str
836836
:param timeout: Timeout for command execution.
837-
:type timeout: typing.Union[int, None]
837+
:type timeout: typing.Union[int, float, None]
838838
:param expected: expected return codes (0 by default)
839839
:type expected: typing.Optional[typing.Iterable[]]
840840
:param raise_on_err: Raise exception on unexpected return code

exec_helpers/api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def execute(
197197
self,
198198
command: str,
199199
verbose: bool = False,
200-
timeout: typing.Union[int, None] = constants.DEFAULT_TIMEOUT,
200+
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
201201
**kwargs: typing.Dict
202202
) -> exec_result.ExecResult:
203203
"""Execute command and wait for return code.
@@ -247,7 +247,7 @@ def check_call(
247247
self,
248248
command: str,
249249
verbose: bool = False,
250-
timeout: typing.Union[int, None] = constants.DEFAULT_TIMEOUT,
250+
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
251251
error_info: typing.Optional[str] = None,
252252
expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None,
253253
raise_on_err: bool = True,
@@ -297,7 +297,7 @@ def check_stderr(
297297
self,
298298
command: str,
299299
verbose: bool = False,
300-
timeout: typing.Union[int, None] = constants.DEFAULT_TIMEOUT,
300+
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
301301
error_info: typing.Optional[str] = None,
302302
raise_on_err: bool = True,
303303
**kwargs: typing.Dict

exec_helpers/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from exec_helpers import proc_enums
2020
from exec_helpers import _log_templates
2121

22-
if typing.TYPE_CHECKING:
22+
if typing.TYPE_CHECKING: # pragma: no cover
2323
from exec_helpers import exec_result # noqa: F401 # pylint: disable=cyclic-import
2424

2525
__all__ = (

exec_helpers/exec_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ def exit_code(self, new_val: typing.Union[int, proc_enums.ExitCodes]) -> None:
329329
if self.timestamp:
330330
raise RuntimeError('Exit code is already received.')
331331
if not isinstance(new_val, int):
332-
raise TypeError('Exit code is strictly int')
332+
raise TypeError('Exit code is strictly int, received: {code!r}'.format(code=new_val))
333333
with self.lock:
334334
self.__exit_code = proc_enums.exit_code_to_enum(new_val)
335335
if self.__exit_code != proc_enums.ExitCodes.EX_INVALID:

exec_helpers/subprocess_runner.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def _exec_command(
104104
:param stderr: STDERR pipe or file-like object
105105
:type stderr: typing.Any
106106
:param timeout: Timeout for command execution
107-
:type timeout: typing.Union[int, None]
107+
:type timeout: typing.Union[int, float, None]
108108
:param verbose: produce verbose log record on command call
109109
:type verbose: bool
110110
:param log_mask_re: regex lookup rule to mask command for logger.
@@ -143,8 +143,12 @@ def poll_stderr() -> None:
143143
stderr_future = poll_stderr() # type: concurrent.futures.Future
144144
# pylint: enable=assignment-from-no-return
145145

146-
concurrent.futures.wait([stdout_future, stderr_future], timeout=timeout) # Wait real timeout here
147-
exit_code = interface.poll() # Update exit code
146+
try:
147+
exit_code = interface.wait(timeout=timeout) # Wait real timeout here, it's python 3 only feature
148+
except subprocess.TimeoutExpired:
149+
exit_code = interface.poll() # Update exit code
150+
151+
concurrent.futures.wait([stdout_future, stderr_future], timeout=1) # Minimal timeout to complete polling
148152

149153
# Process closed?
150154
if exit_code is not None:
@@ -183,7 +187,7 @@ def execute_async( # pylint: disable=signature-differs
183187
**kwargs: typing.Dict
184188
) -> typing.Tuple[subprocess.Popen, None, None, None]:
185189
"""Overload: with stdin."""
186-
pass
190+
pass # pragma: no cover
187191

188192
@typing.overload # noqa: F811
189193
def execute_async(
@@ -197,7 +201,7 @@ def execute_async(
197201
**kwargs: typing.Dict
198202
) -> typing.Tuple[subprocess.Popen, None, typing.IO, typing.IO]:
199203
"""Overload: no stdin."""
200-
pass
204+
pass # pragma: no cover
201205

202206
# pylint: enable=unused-argument
203207
def execute_async( # type: ignore # noqa: F811

test/test_subprocess_runner.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
stdout_list = [b' \n', b'2\n', b'3\n', b' \n']
3737
stderr_list = [b' \n', b'0\n', b'1\n', b' \n']
3838
print_stdin = 'read line; echo "$line"'
39+
default_timeout = 60 * 60 # 1 hour
3940

4041

4142
class FakeFileStream(object):
@@ -88,6 +89,7 @@ def prepare_close(
8889
else:
8990
popen_obj.configure_mock(stderr=None)
9091
popen_obj.attach_mock(mock.Mock(return_value=ec), 'poll')
92+
popen_obj.attach_mock(mock.Mock(return_value=ec), 'wait')
9193
popen_obj.configure_mock(returncode=ec)
9294

9395
popen.return_value = popen_obj
@@ -140,7 +142,7 @@ def test_001_call(
140142
mock.call.log(level=logging.DEBUG, msg=self.gen_cmd_result_log_message(result)),
141143
)
142144
self.assertIn(
143-
mock.call.poll(), popen_obj.mock_calls
145+
mock.call.wait(timeout=default_timeout), popen_obj.mock_calls
144146
)
145147

146148
def test_002_call_verbose(
@@ -198,6 +200,7 @@ def test_004_execute_timeout_fail(
198200
):
199201
popen_obj, exp_result = self.prepare_close(popen)
200202
popen_obj.poll.return_value = None
203+
popen_obj.wait.return_value = None
201204

202205
runner = exec_helpers.Subprocess()
203206

@@ -263,7 +266,7 @@ def test_005_execute_no_stdout(
263266
msg=self.gen_cmd_result_log_message(result)),
264267
])
265268
self.assertIn(
266-
mock.call.poll(), popen_obj.mock_calls
269+
mock.call.wait(timeout=default_timeout), popen_obj.mock_calls
267270
)
268271

269272
def test_006_execute_no_stderr(
@@ -305,7 +308,7 @@ def test_006_execute_no_stderr(
305308
msg=self.gen_cmd_result_log_message(result)),
306309
])
307310
self.assertIn(
308-
mock.call.poll(), popen_obj.mock_calls
311+
mock.call.wait(timeout=default_timeout), popen_obj.mock_calls
309312
)
310313

311314
def test_007_execute_no_stdout_stderr(
@@ -345,7 +348,7 @@ def test_007_execute_no_stdout_stderr(
345348
msg=self.gen_cmd_result_log_message(result)),
346349
])
347350
self.assertIn(
348-
mock.call.poll(), popen_obj.mock_calls
351+
mock.call.wait(timeout=default_timeout), popen_obj.mock_calls
349352
)
350353

351354
def test_008_execute_mask_global(
@@ -394,7 +397,7 @@ def test_008_execute_mask_global(
394397
)
395398

396399
self.assertIn(
397-
mock.call.poll(), popen_obj.mock_calls
400+
mock.call.wait(timeout=default_timeout), popen_obj.mock_calls
398401
)
399402

400403
def test_009_execute_mask_local(
@@ -439,7 +442,7 @@ def test_009_execute_mask_local(
439442
mock.call.log(level=logging.DEBUG, msg=self.gen_cmd_result_log_message(result)),
440443
)
441444
self.assertIn(
442-
mock.call.poll(), popen_obj.mock_calls
445+
mock.call.wait(timeout=default_timeout), popen_obj.mock_calls
443446
)
444447

445448
def test_004_check_stdin_str(
@@ -767,8 +770,9 @@ def test_013_execute_timeout_done(
767770

768771
):
769772
popen_obj, exp_result = self.prepare_close(popen, ec=exec_helpers.ExitCodes.EX_INVALID)
770-
popen_obj.poll.side_effect = [None, exec_helpers.ExitCodes.EX_INVALID]
773+
popen_obj.poll.return_value = exec_helpers.ExitCodes.EX_INVALID
771774
popen_obj.attach_mock(mock.Mock(side_effect=OSError), 'kill')
775+
popen_obj.wait.return_value = None
772776

773777
runner = exec_helpers.Subprocess()
774778

0 commit comments

Comments
 (0)