Skip to content

Commit 00a91bd

Browse files
authored
Better timeout handling, exit codes enum update
1 parent 6077362 commit 00a91bd

18 files changed

+198
-94
lines changed

README.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ Context manager is available, connection is closed and lock is released on exit
119119
Subprocess
120120
----------
121121

122-
No initialization required.
123122
Context manager is available, subprocess is killed and lock is released on exit from context.
124123

125124
Base methods
@@ -147,7 +146,7 @@ This methods are almost the same for `SSHCleint` and `Subprocess`, except specif
147146
verbose=False, # type: bool
148147
timeout=1 * 60 * 60, # type: type: typing.Union[int, float, None]
149148
error_info=None, # type: typing.Optional[str]
150-
expected=None, # type: typing.Optional[typing.Iterable[int]]
149+
expected=(0,), # type: typing.Iterable[typing.Union[int, ExitCodes]]
151150
raise_on_err=True, # type: bool
152151
# Keyword only:
153152
exception_class=CalledProcessError, # typing.Type[CalledProcessError]
@@ -163,7 +162,7 @@ This methods are almost the same for `SSHCleint` and `Subprocess`, except specif
163162
error_info=None, # type: typing.Optional[str]
164163
raise_on_err=True, # type: bool
165164
# Keyword only:
166-
expected=None, # typing.Optional[typing.Iterable[typing.Union[int, ExitCodes]]]
165+
expected=(0,), # typing.Iterable[typing.Union[int, ExitCodes]]
167166
exception_class=CalledProcessError, # typing.Type[CalledProcessError]
168167
)
169168

exec_helpers/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Execution helpers for simplified usage of subprocess and ssh."""
1616

17+
import typing
18+
1719
import pkg_resources
1820

1921
from .proc_enums import ExitCodes
@@ -24,6 +26,7 @@
2426
CalledProcessError,
2527
ParallelCallProcessError,
2628
ParallelCallExceptions,
29+
ExecHelperNoKillError,
2730
ExecHelperTimeoutError,
2831
)
2932

@@ -41,6 +44,7 @@
4144
"CalledProcessError",
4245
"ParallelCallExceptions",
4346
"ParallelCallProcessError",
47+
"ExecHelperNoKillError",
4448
"ExecHelperTimeoutError",
4549
"ExecHelper",
4650
"SSHClient",
@@ -51,7 +55,7 @@
5155
"ExitCodes",
5256
"ExecResult",
5357
"async_api",
54-
)
58+
) # type: typing.Tuple[str, ...]
5559

5660
try: # pragma: no cover
5761
__version__ = pkg_resources.get_distribution(__name__).version

exec_helpers/_log_templates.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818

1919
CMD_EXEC = "Executing command:\n{cmd!r}\n"
2020

21+
CMD_KILL_ERROR = (
22+
"Wait for {result.cmd!r} during {timeout!s}s: no return code and no response on SIGTERM + SIGKILL signals!\n"
23+
"\tSTDOUT:\n"
24+
"{result.stdout_brief}\n"
25+
"\tSTDERR:\n"
26+
"{result.stderr_brief}"
27+
)
28+
2129
CMD_WAIT_ERROR = (
2230
"Wait for {result.cmd!r} during {timeout!s}s: no return code!\n"
2331
"\tSTDOUT:\n"

exec_helpers/_ssh_client_base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ def execute_together(
817817
remotes: typing.Iterable["SSHClientBase"],
818818
command: str,
819819
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
820-
expected: typing.Optional[typing.Iterable[int]] = None,
820+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
821821
raise_on_err: bool = True,
822822
*,
823823
exception_class: "typing.Type[exceptions.ParallelCallProcessError]" = exceptions.ParallelCallProcessError,
@@ -832,7 +832,7 @@ def execute_together(
832832
:param timeout: Timeout for command execution.
833833
:type timeout: typing.Union[int, float, None]
834834
:param expected: expected return codes (0 by default)
835-
:type expected: typing.Optional[typing.Iterable[]]
835+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
836836
:param raise_on_err: Raise exception on unexpected return code
837837
:type raise_on_err: bool
838838
:param exception_class: Exception to raise on error. Mandatory subclass of exceptions.ParallelCallProcessError
@@ -847,6 +847,7 @@ def execute_together(
847847
.. versionchanged:: 1.2.0 default timeout 1 hour
848848
.. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd
849849
.. versionchanged:: 3.2.0 Exception class can be substituted
850+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
850851
"""
851852

852853
@threaded.threadpooled
@@ -869,7 +870,6 @@ def get_result(remote: "SSHClientBase") -> exec_result.ExecResult:
869870
async_result.interface.close()
870871
return res
871872

872-
expected = expected or [proc_enums.ExitCodes.EX_OK]
873873
expected = proc_enums.exit_codes_to_enums(expected)
874874

875875
futures = {remote: get_result(remote) for remote in set(remotes)} # Use distinct remotes

exec_helpers/api.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def check_call(
281281
verbose: bool = False,
282282
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
283283
error_info: typing.Optional[str] = None,
284-
expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None,
284+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
285285
raise_on_err: bool = True,
286286
*,
287287
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
@@ -298,7 +298,7 @@ def check_call(
298298
:param error_info: Text for error details, if fail happens
299299
:type error_info: typing.Optional[str]
300300
:param expected: expected return codes (0 by default)
301-
:type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]]
301+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
302302
:param raise_on_err: Raise exception on unexpected return code
303303
:type raise_on_err: bool
304304
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
@@ -312,6 +312,7 @@ def check_call(
312312
313313
.. versionchanged:: 1.2.0 default timeout 1 hour
314314
.. versionchanged:: 3.2.0 Exception class can be substituted
315+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
315316
"""
316317
expected_codes = proc_enums.exit_codes_to_enums(expected)
317318
ret = self.execute(command, verbose, timeout, **kwargs)
@@ -335,7 +336,7 @@ def check_stderr(
335336
error_info: typing.Optional[str] = None,
336337
raise_on_err: bool = True,
337338
*,
338-
expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None,
339+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
339340
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
340341
**kwargs: typing.Any
341342
) -> exec_result.ExecResult:
@@ -352,7 +353,7 @@ def check_stderr(
352353
:param raise_on_err: Raise exception on unexpected return code
353354
:type raise_on_err: bool
354355
:param expected: expected return codes (0 by default)
355-
:type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]]
356+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
356357
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
357358
:type exception_class: typing.Type[exceptions.CalledProcessError]
358359
:param kwargs: additional parameters for call.
@@ -364,6 +365,7 @@ def check_stderr(
364365
365366
.. versionchanged:: 1.2.0 default timeout 1 hour
366367
.. versionchanged:: 3.2.0 Exception class can be substituted
368+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
367369
"""
368370
ret = self.check_call(
369371
command,

exec_helpers/async_api/api.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ async def check_call( # type: ignore
202202
verbose: bool = False,
203203
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
204204
error_info: typing.Optional[str] = None,
205-
expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None,
205+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
206206
raise_on_err: bool = True,
207207
*,
208208
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
@@ -219,7 +219,7 @@ async def check_call( # type: ignore
219219
:param error_info: Text for error details, if fail happens
220220
:type error_info: typing.Optional[str]
221221
:param expected: expected return codes (0 by default)
222-
:type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]]
222+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
223223
:param raise_on_err: Raise exception on unexpected return code
224224
:type raise_on_err: bool
225225
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
@@ -230,6 +230,8 @@ async def check_call( # type: ignore
230230
:rtype: ExecResult
231231
:raises ExecHelperTimeoutError: Timeout exceeded
232232
:raises CalledProcessError: Unexpected exit code
233+
234+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
233235
"""
234236
expected_codes = proc_enums.exit_codes_to_enums(expected)
235237
ret = await self.execute(command, verbose, timeout, **kwargs)
@@ -253,7 +255,7 @@ async def check_stderr( # type: ignore
253255
error_info: typing.Optional[str] = None,
254256
raise_on_err: bool = True,
255257
*,
256-
expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None,
258+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
257259
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
258260
**kwargs: typing.Any
259261
) -> exec_result.ExecResult:
@@ -270,7 +272,7 @@ async def check_stderr( # type: ignore
270272
:param raise_on_err: Raise exception on unexpected return code
271273
:type raise_on_err: bool
272274
:param expected: expected return codes (0 by default)
273-
:type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]]
275+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
274276
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
275277
:type exception_class: typing.Type[exceptions.CalledProcessError]
276278
:param kwargs: additional parameters for call.
@@ -279,6 +281,8 @@ async def check_stderr( # type: ignore
279281
:rtype: ExecResult
280282
:raises ExecHelperTimeoutError: Timeout exceeded
281283
:raises CalledProcessError: Unexpected exit code or stderr presents
284+
285+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
282286
"""
283287
ret = await self.check_call(
284288
command,

exec_helpers/async_api/subprocess_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ async def poll_stderr() -> None:
141141
_subprocess_helpers.kill_proc_tree(async_result.interface.pid)
142142
exit_code = await asyncio.wait_for(async_result.interface.wait(), timeout=0.001)
143143
if exit_code is None:
144-
async_result.interface.kill() # kill -9
144+
raise exceptions.ExecHelperNoKillError(result=result, timeout=timeout)
145145
finally:
146146
stdout_task.cancel()
147147
stderr_task.cancel()

exec_helpers/exceptions.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
__all__ = (
2626
"ExecHelperError",
27+
"ExecHelperNoKillError",
2728
"ExecHelperTimeoutError",
2829
"ExecCalledProcessError",
2930
"CalledProcessError",
@@ -50,25 +51,22 @@ class ExecCalledProcessError(ExecHelperError):
5051
__slots__ = ()
5152

5253

53-
class ExecHelperTimeoutError(ExecCalledProcessError):
54-
"""Execution timeout.
55-
56-
.. versionchanged:: 1.3.0 provide full result and timeout inside.
57-
.. versionchanged:: 1.3.0 subclass ExecCalledProcessError
58-
"""
54+
class ExecHelperTimeutProcessError(ExecCalledProcessError):
55+
"""Timeout based errors."""
5956

6057
__slots__ = ("result", "timeout")
6158

62-
def __init__(self, result: "exec_result.ExecResult", timeout: typing.Union[int, float]) -> None:
59+
def __init__(self, message: str, *, result: "exec_result.ExecResult", timeout: typing.Union[int, float]) -> None:
6360
"""Exception for error on process calls.
6461
62+
:param message: exception message
63+
:type message: str
6564
:param result: execution result
6665
:type result: exec_result.ExecResult
6766
:param timeout: timeout for command
6867
:type timeout: typing.Union[int, float]
6968
"""
70-
message = _log_templates.CMD_WAIT_ERROR.format(result=result, timeout=timeout)
71-
super(ExecHelperTimeoutError, self).__init__(message)
69+
super(ExecHelperTimeutProcessError, self).__init__(message)
7270
self.result = result
7371
self.timeout = timeout
7472

@@ -88,6 +86,47 @@ def stderr(self) -> str:
8886
return self.result.stderr_str
8987

9088

89+
class ExecHelperNoKillError(ExecHelperTimeutProcessError):
90+
"""Impossible to kill process.
91+
92+
.. versionadded:: 3.4.0
93+
"""
94+
95+
__slots__ = ()
96+
97+
def __init__(self, result: "exec_result.ExecResult", timeout: typing.Union[int, float]) -> None:
98+
"""Exception for error on process calls.
99+
100+
:param result: execution result
101+
:type result: exec_result.ExecResult
102+
:param timeout: timeout for command
103+
:type timeout: typing.Union[int, float]
104+
"""
105+
message = _log_templates.CMD_KILL_ERROR.format(result=result, timeout=timeout)
106+
super(ExecHelperNoKillError, self).__init__(message, result=result, timeout=timeout)
107+
108+
109+
class ExecHelperTimeoutError(ExecHelperTimeutProcessError):
110+
"""Execution timeout.
111+
112+
.. versionchanged:: 1.3.0 provide full result and timeout inside.
113+
.. versionchanged:: 1.3.0 subclass ExecCalledProcessError
114+
"""
115+
116+
__slots__ = ()
117+
118+
def __init__(self, result: "exec_result.ExecResult", timeout: typing.Union[int, float]) -> None:
119+
"""Exception for error on process calls.
120+
121+
:param result: execution result
122+
:type result: exec_result.ExecResult
123+
:param timeout: timeout for command
124+
:type timeout: typing.Union[int, float]
125+
"""
126+
message = _log_templates.CMD_WAIT_ERROR.format(result=result, timeout=timeout)
127+
super(ExecHelperTimeoutError, self).__init__(message, result=result, timeout=timeout)
128+
129+
91130
class CalledProcessError(ExecCalledProcessError):
92131
"""Exception for error on process calls."""
93132

@@ -96,19 +135,19 @@ class CalledProcessError(ExecCalledProcessError):
96135
def __init__(
97136
self,
98137
result: "exec_result.ExecResult",
99-
expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None,
138+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
100139
) -> None:
101140
"""Exception for error on process calls.
102141
103142
:param result: execution result
104143
:type result: exec_result.ExecResult
105144
:param expected: expected return codes
106-
:type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]]
145+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
107146
108147
.. versionchanged:: 1.1.1 - provide full result
148+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
109149
"""
110150
self.result = result
111-
expected = expected or [proc_enums.ExitCodes.EX_OK]
112151
self.expected = proc_enums.exit_codes_to_enums(expected)
113152
message = (
114153
"Command {result.cmd!r} returned exit code {result.exit_code} "
@@ -150,7 +189,7 @@ def __init__(
150189
command: str,
151190
errors: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"],
152191
results: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"],
153-
expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] = None,
192+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
154193
*,
155194
_message: typing.Optional[str] = None
156195
) -> None:
@@ -163,11 +202,12 @@ def __init__(
163202
:param results: all results
164203
:type results: typing.Dict[typing.Tuple[str, int], ExecResult]
165204
:param expected: expected return codes
166-
:type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
205+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
167206
:param _message: message override
168207
:type _message: typing.Optional[str]
208+
209+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
169210
"""
170-
expected = expected or [proc_enums.ExitCodes.EX_OK]
171211
prep_expected = proc_enums.exit_codes_to_enums(expected)
172212
message = _message or (
173213
"Command {cmd!r} "
@@ -201,7 +241,7 @@ def __init__(
201241
exceptions: typing.Dict[typing.Tuple[str, int], Exception],
202242
errors: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"],
203243
results: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"],
204-
expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] = None,
244+
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
205245
*,
206246
_message: typing.Optional[str] = None
207247
) -> None:
@@ -216,11 +256,12 @@ def __init__(
216256
:param results: all results
217257
:type results: typing.Dict[typing.Tuple[str, int], ExecResult]
218258
:param expected: expected return codes
219-
:type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
259+
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
220260
:param _message: message override
221261
:type _message: typing.Optional[str]
262+
263+
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
222264
"""
223-
expected = expected or [proc_enums.ExitCodes.EX_OK]
224265
prep_expected = proc_enums.exit_codes_to_enums(expected)
225266
message = _message or (
226267
"Command {cmd!r} "

0 commit comments

Comments
 (0)