Skip to content

Commit 61077ef

Browse files
committed
1.1.0
* context managers works with locks * Subprocess: kill on destructor/exit from context manager * Subprocess: remove classmethods
1 parent ce81c14 commit 61077ef

File tree

7 files changed

+98
-46
lines changed

7 files changed

+98
-46
lines changed

README.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,26 @@ Creation from scratch:
9494
username='username', # type: typing.Optional[str]
9595
password='password', # type: typing.Optional[str]
9696
key=None, # type: typing.Optional[paramiko.RSAKey]
97-
keys=None,
97+
keys=None, # type: typing.Optional[typing.Iterable[paramiko.RSAKey]],
98+
key_filename=None, # type: typing.Union[typing.List[str], str, None]
99+
passphrase=None, # type: typing.Optional[str]
98100
)
99101
100102
Key is a main connection key (always tried first) and keys are alternate keys.
103+
Key filename is afilename or list of filenames with keys, which should be loaded.
104+
Passphrase is an alternate password for keys, if it differs from main password.
101105
If main key now correct for username - alternate keys tried, if correct key found - it became main.
102106
If no working key - password is used and None is set as main key.
103107

104108
.. note:: Automatic closing connections during cache record removal supported on CPython implementation only.
105109

110+
Context manager is available, connection is closed and lock is released on exit from context.
111+
106112
Subprocess
107113
----------
108114

109115
No initialization required.
116+
Context manager is available, subprocess is killed and lock is released on exit from context.
110117

111118
Base methods
112119
------------

doc/source/SSHClient.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,14 @@ API: SSHClient and SSHAuth.
7979
8080
Open context manager
8181

82+
.. versionchanged:: 1.1.0 - lock on enter
83+
8284
.. py:method:: __exit__(self, exc_type, exc_val, exc_tb)
8385
8486
Close context manager and disconnect
8587

86-
.. versionchanged:: 1.0 - disconnect enforced on close
88+
.. versionchanged:: 1.0.0 - disconnect enforced on close
89+
.. versionchanged:: 1.1.0 - release lock on exit
8790

8891
.. py:method:: sudo(enforce=None)
8992
@@ -293,7 +296,7 @@ API: SSHClient and SSHAuth.
293296
:param passphrase: passphrase for keys. Need, if differs from password
294297
:type passphrase: ``typing.Optional[str]``
295298

296-
.. versionchanged:: 1.0
299+
.. versionchanged:: 1.0.0
297300
added: key_filename, passphrase arguments
298301

299302
.. py:attribute:: username
@@ -310,7 +313,7 @@ API: SSHClient and SSHAuth.
310313
``typing.Union[typing.List[str], str, None]``
311314
Key filename(s).
312315

313-
.. versionadded:: 1.0
316+
.. versionadded:: 1.0.0
314317

315318
.. py:method:: enter_password(self, tgt)
316319

doc/source/Subprocess.rst

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,23 @@ API: Subprocess
88
99
.. py:class:: Subprocess()
1010
11-
.. py:classmethod:: execute(command, verbose=False, timeout=None, **kwargs)
11+
.. py:attribute:: lock
12+
13+
``threading.RLock``
14+
15+
.. py:method:: __enter__()
16+
17+
Open context manager
18+
19+
.. versionchanged:: 1.1.0 - lock on enter
20+
21+
.. py:method:: __exit__(self, exc_type, exc_val, exc_tb)
22+
23+
Close context manager
24+
25+
.. versionchanged:: 1.1.0 - release lock on exit
26+
27+
.. py:method:: execute(command, verbose=False, timeout=None, **kwargs)
1228
1329
Execute command and wait for return code.
1430

@@ -21,7 +37,9 @@ API: Subprocess
2137
:rtype: ExecResult
2238
:raises: ExecHelperTimeoutError
2339

24-
.. py:classmethod:: check_call(command, verbose=False, timeout=None, error_info=None, expected=None, raise_on_err=True, **kwargs)
40+
.. versionchanged:: 1.1.0 - make method
41+
42+
.. py:method:: check_call(command, verbose=False, timeout=None, error_info=None, expected=None, raise_on_err=True, **kwargs)
2543
2644
Execute command and check for return code.
2745

@@ -40,7 +58,9 @@ API: Subprocess
4058
:rtype: ExecResult
4159
:raises: CalledProcessError
4260

43-
.. py:classmethod:: check_stderr(command, verbose=False, timeout=None, error_info=None, raise_on_err=True, **kwargs)
61+
.. versionchanged:: 1.1.0 - make method
62+
63+
.. py:method:: check_stderr(command, verbose=False, timeout=None, error_info=None, raise_on_err=True, **kwargs)
4464
4565
Execute command expecting return code 0 and empty STDERR.
4666

@@ -58,3 +78,5 @@ API: Subprocess
5878
:raises: CalledProcessError
5979

6080
.. note:: expected return codes can be overridden via kwargs.
81+
82+
.. versionchanged:: 1.1.0 - make method

exec_helpers/__init__.py

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

48-
__version__ = '1.0.0'
48+
__version__ = '1.1.0'
4949
__author__ = "Alexey Stepanov"
5050
__author_email__ = 'penguinolog@gmail.com'
5151
__url__ = 'https://github.com/penguinolog/exec-helpers'

exec_helpers/_ssh_client_base.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def __init__(
103103
:param passphrase: passphrase for keys. Need, if differs from password
104104
:type passphrase: typing.Optional[str]
105105
106-
.. versionchanged:: 1.0
106+
.. versionchanged:: 1.0.0
107107
added: key_filename, passphrase arguments
108108
"""
109109
self.__username = username
@@ -152,7 +152,7 @@ def key_filename(
152152
): # type: () -> typing.Union[typing.List[str], str, None]
153153
"""Key filename(s).
154154
155-
.. versionadded:: 1.0
155+
.. versionadded:: 1.0.0
156156
"""
157157
return copy.deepcopy(self.__key_filename)
158158

@@ -706,15 +706,21 @@ def __del__(self):
706706
self.__sftp = None
707707

708708
def __enter__(self):
709-
"""Get context manager."""
709+
"""Get context manager.
710+
711+
.. versionchanged:: 1.1.0 - lock on enter
712+
"""
713+
self.lock.acquire()
710714
return self
711715

712716
def __exit__(self, exc_type, exc_val, exc_tb):
713717
"""Exit context manager.
714718
715-
.. versionchanged:: 1.0 - disconnect enforced on close
719+
.. versionchanged:: 1.0.0 - disconnect enforced on close
720+
.. versionchanged:: 1.1.0 - release lock on exit
716721
"""
717722
self.close()
723+
self.lock.release()
718724

719725
def reconnect(self):
720726
"""Reconnect SSH session."""

exec_helpers/subprocess_runner.py

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,45 @@ def _py2_str(src):
7979
class Subprocess(BaseSingleton):
8080
"""Subprocess helper with timeouts and lock-free FIFO."""
8181

82-
__lock = threading.RLock()
83-
84-
__slots__ = ()
82+
__slots__ = (
83+
'__lock',
84+
'__process',
85+
)
8586

8687
def __init__(self):
8788
"""Subprocess helper with timeouts and lock-free FIFO.
8889
8990
For excluding race-conditions we allow to run 1 command simultaneously
9091
"""
91-
pass
92+
self.__lock = threading.RLock()
93+
self.__process = None
94+
95+
@property
96+
def lock(self): # type: () -> threading.RLock
97+
"""Lock.
98+
99+
:rtype: threading.RLock
100+
"""
101+
return self.__lock
92102

93103
def __enter__(self):
94104
"""Context manager usage."""
105+
self.lock.acquire()
95106
return self
96107

97108
def __exit__(self, exc_type, exc_val, exc_tb):
98109
"""Context manager usage."""
99-
pass
110+
if self.__process:
111+
self.__process.kill()
112+
self.lock.release()
113+
114+
def __del__(self):
115+
"""Destructor. Kill running subprocess, if it running."""
116+
if self.__process:
117+
self.__process.kill()
100118

101-
@classmethod
102119
def __exec_command(
103-
cls,
120+
self,
104121
command, # type: str
105122
cwd=None, # type: typing.Optional[str]
106123
env=None, # type: typing.Optional[typing.Dict[str, typing.Any]]
@@ -146,43 +163,41 @@ def poll_streams(
146163

147164
@threaded.threaded(started=True, daemon=True)
148165
def poll_pipes(
149-
proc, # type: subprocess.Popen
150166
result, # type: exec_result.ExecResult
151167
stop # type: threading.Event
152168
):
153169
"""Polling task for FIFO buffers.
154170
155-
:type proc: subprocess.Popen
156171
:type result: exec_result.ExecResult
157172
:type stop: threading.Event
158173
"""
159174
while not stop.isSet():
160175
time.sleep(0.1)
161176
poll_streams(
162177
result=result,
163-
stdout=proc.stdout,
164-
stderr=proc.stderr,
178+
stdout=self.__process.stdout,
179+
stderr=self.__process.stderr,
165180
)
166181

167-
proc.poll()
182+
self.__process.poll()
168183

169-
if proc.returncode is not None:
184+
if self.__process.returncode is not None:
170185
result.read_stdout(
171-
src=proc.stdout,
186+
src=self.__process.stdout,
172187
log=logger,
173188
verbose=verbose
174189
)
175190
result.read_stderr(
176-
src=proc.stderr,
191+
src=self.__process.stderr,
177192
log=logger,
178193
verbose=verbose
179194
)
180-
result.exit_code = proc.returncode
195+
result.exit_code = self.__process.returncode
181196

182197
stop.set()
183198

184199
# 1 Command per run
185-
with cls.__lock:
200+
with self.lock:
186201
result = exec_result.ExecResult(cmd=command)
187202
stop_event = threading.Event()
188203
message = _log_templates.CMD_EXEC.format(cmd=command.rstrip())
@@ -191,18 +206,18 @@ def poll_pipes(
191206
else:
192207
logger.debug(message)
193208
# Run
194-
process = subprocess.Popen(
209+
self.__process = subprocess.Popen(
195210
args=[command],
196211
stdin=subprocess.PIPE,
197212
stdout=subprocess.PIPE,
198213
stderr=subprocess.PIPE,
199214
shell=True, cwd=cwd, env=env,
200-
universal_newlines=False)
215+
universal_newlines=False,
216+
)
201217

202218
# Poll output
203219
# pylint: disable=assignment-from-no-return
204220
poll_thread = poll_pipes(
205-
process,
206221
result,
207222
stop_event
208223
) # type: threading.Thread
@@ -213,17 +228,19 @@ def poll_pipes(
213228
# Process closed?
214229
if stop_event.isSet():
215230
stop_event.clear()
231+
self.__process = None
216232
return result
217233
# Kill not ended process and wait for close
218234
try:
219-
process.kill() # kill -9
235+
self.__process.kill() # kill -9
220236
stop_event.wait(5)
221237
poll_thread.join(5)
222238
except OSError:
223239
# Nothing to kill
224240
logger.warning(
225241
u"{!s} has been completed just after timeout: "
226242
"please validate timeout.".format(command))
243+
self.__process = None
227244

228245
wait_err_msg = _log_templates.CMD_WAIT_ERROR.format(
229246
cmd=command.rstrip(),
@@ -239,9 +256,8 @@ def poll_pipes(
239256
wait_err_msg + output_brief_msg
240257
)
241258

242-
@classmethod
243259
def execute(
244-
cls,
260+
self,
245261
command, # type: str
246262
verbose=False, # type: bool
247263
timeout=None, # type: typing.Optional[int]
@@ -257,8 +273,8 @@ def execute(
257273
:rtype: ExecResult
258274
:raises: ExecHelperTimeoutError
259275
"""
260-
result = cls.__exec_command(command=command, timeout=timeout,
261-
verbose=verbose, **kwargs)
276+
result = self.__exec_command(command=command, timeout=timeout,
277+
verbose=verbose, **kwargs)
262278
message = _log_templates.CMD_RESULT.format(
263279
cmd=command, code=result.exit_code)
264280
logger.log(
@@ -268,9 +284,8 @@ def execute(
268284

269285
return result
270286

271-
@classmethod
272287
def check_call(
273-
cls,
288+
self,
274289
command, # type: str
275290
verbose=False, # type: bool
276291
timeout=None, # type: typing.Optional[int]
@@ -293,7 +308,7 @@ def check_call(
293308
:raises: DevopsCalledProcessError
294309
"""
295310
expected = proc_enums.exit_codes_to_enums(expected)
296-
ret = cls.execute(command, verbose, timeout, **kwargs)
311+
ret = self.execute(command, verbose, timeout, **kwargs)
297312
if ret['exit_code'] not in expected:
298313
message = (
299314
_log_templates.CMD_UNEXPECTED_EXIT_CODE.format(
@@ -312,9 +327,8 @@ def check_call(
312327
stderr=ret['stderr_brief'])
313328
return ret
314329

315-
@classmethod
316330
def check_stderr(
317-
cls,
331+
self,
318332
command, # type: str
319333
verbose=False, # type: bool
320334
timeout=None, # type: typing.Optional[int]
@@ -334,7 +348,7 @@ def check_stderr(
334348
:rtype: ExecResult
335349
:raises: DevopsCalledProcessError
336350
"""
337-
ret = cls.check_call(
351+
ret = self.check_call(
338352
command, verbose, timeout=timeout,
339353
error_info=error_info, raise_on_err=raise_on_err, **kwargs)
340354
if ret['stderr']:

0 commit comments

Comments
 (0)