7 | 7 |
import sys
|
8 | 8 |
import time
|
9 | 9 |
import timeit
|
|
10 |
import warnings
|
10 | 11 |
from multiprocessing import Pipe
|
11 | 12 |
from threading import Thread
|
12 | 13 |
|
|
16 | 17 |
#
|
17 | 18 |
# - runs an external process and waits for it to finish
|
18 | 19 |
# - does not deadlock, no matter the process stdout/stderr output behaviour
|
19 | |
# - returns the exit code, stdout, stderr (separately), and the total process
|
20 | |
# runtime as a dictionary
|
|
20 |
# - returns the exit code, stdout, stderr (separately) as a
|
|
21 |
# subprocess.CompletedProcess object
|
21 | 22 |
# - process can run in a custom environment, either as a modification of
|
22 | 23 |
# the current environment or in a new environment from scratch
|
23 | 24 |
# - stdin can be fed to the process, the returned dictionary contains
|
|
35 | 36 |
#
|
36 | 37 |
# Returns:
|
37 | 38 |
#
|
38 | |
# {'command': ['/bin/ls', '/some/path/containing spaces'],
|
39 | |
# 'exitcode': 2,
|
40 | |
# 'runtime': 0.12990689277648926,
|
41 | |
# 'stderr': '/bin/ls: cannot access /some/path/containing spaces: No such file or directory\n',
|
42 | |
# 'stdout': '',
|
43 | |
# 'time_end': '2017-11-12 19:54:49 GMT',
|
44 | |
# 'time_start': '2017-11-12 19:54:49 GMT',
|
45 | |
# 'timeout': False}
|
46 | |
#
|
|
39 |
# ReturnObject(
|
|
40 |
# args=('/bin/ls', '/some/path/containing spaces'),
|
|
41 |
# returncode=2,
|
|
42 |
# stdout=b'',
|
|
43 |
# stderr=b'/bin/ls: cannot access /some/path/containing spaces: No such file or directory\n'
|
|
44 |
# )
|
|
45 |
#
|
|
46 |
# which also offers (albeit deprecated)
|
|
47 |
#
|
|
48 |
# result.runtime == 0.12990689277648926
|
|
49 |
# result.time_end == '2017-11-12 19:54:49 GMT'
|
|
50 |
# result.time_start == '2017-11-12 19:54:49 GMT'
|
|
51 |
# result.timeout == False
|
47 | 52 |
|
48 | 53 |
__author__ = """Markus Gerstel"""
|
49 | 54 |
__email__ = "scientificsoftware@diamond.ac.uk"
|
|
179 | 184 |
if not self.has_finished():
|
180 | 185 |
if self._debug:
|
181 | 186 |
logger.debug(
|
182 | |
"NBSR join after %f seconds, underrun not resolved"
|
183 | |
% (timeit.default_timer() - underrun_debug_timer)
|
|
187 |
"NBSR join after %f seconds, underrun not resolved",
|
|
188 |
timeit.default_timer() - underrun_debug_timer,
|
184 | 189 |
)
|
185 | 190 |
raise Exception("thread did not terminate")
|
186 | 191 |
if self._debug:
|
187 | 192 |
logger.debug(
|
188 | |
"NBSR underrun resolved after %f seconds"
|
189 | |
% (timeit.default_timer() - underrun_debug_timer)
|
|
193 |
"NBSR underrun resolved after %f seconds",
|
|
194 |
timeit.default_timer() - underrun_debug_timer,
|
190 | 195 |
)
|
191 | 196 |
if self._closed:
|
192 | 197 |
raise Exception("streamreader double-closed")
|
|
231 | 236 |
raise
|
232 | 237 |
self._buffer_pos += len(block)
|
233 | 238 |
if debug:
|
234 | |
logger.debug("wrote %d bytes to stream" % len(block))
|
|
239 |
logger.debug("wrote %d bytes to stream", len(block))
|
235 | 240 |
self._stream.close()
|
236 | 241 |
self._terminated = True
|
237 | 242 |
if notify:
|
|
300 | 305 |
return command
|
301 | 306 |
|
302 | 307 |
|
303 | |
class ReturnObject(dict, subprocess.CompletedProcess):
|
|
308 |
class ReturnObject(subprocess.CompletedProcess):
|
304 | 309 |
"""
|
305 | 310 |
A subprocess.CompletedProcess-like object containing the executed
|
306 | 311 |
command, stdout and stderr (both as bytestrings), and the exitcode.
|
|
310 | 315 |
exited with a non-zero exit code.
|
311 | 316 |
"""
|
312 | 317 |
|
313 | |
def __init__(self, *arg, **kw):
|
314 | |
super().__init__(*arg, **kw)
|
315 | |
self.args = self["command"]
|
316 | |
self.returncode = self["exitcode"]
|
317 | |
self.stdout = self["stdout"]
|
318 | |
self.stderr = self["stderr"]
|
|
318 |
def __init__(self, exitcode=None, command=None, stdout=None, stderr=None, **kw):
|
|
319 |
super().__init__(
|
|
320 |
args=command, returncode=exitcode, stdout=stdout, stderr=stderr
|
|
321 |
)
|
|
322 |
self._extras = {
|
|
323 |
"timeout": kw.get("timeout"),
|
|
324 |
"runtime": kw.get("runtime"),
|
|
325 |
"time_start": kw.get("time_start"),
|
|
326 |
"time_end": kw.get("time_end"),
|
|
327 |
}
|
|
328 |
|
|
329 |
def __getitem__(self, key):
|
|
330 |
warnings.warn(
|
|
331 |
"dictionary access to a procrunner return object is deprecated",
|
|
332 |
DeprecationWarning,
|
|
333 |
stacklevel=2,
|
|
334 |
)
|
|
335 |
if key in self._extras:
|
|
336 |
return self._extras[key]
|
|
337 |
if not hasattr(self, key):
|
|
338 |
raise KeyError("Unknown attribute {key}".format(key=key))
|
|
339 |
return getattr(self, key)
|
319 | 340 |
|
320 | 341 |
def __eq__(self, other):
|
321 | 342 |
"""Override equality operator to account for added fields"""
|
|
327 | 348 |
"""This object is not immutable, so mark it as unhashable"""
|
328 | 349 |
return None
|
329 | 350 |
|
330 | |
def __ne__(self, other):
|
331 | |
"""Overrides the default implementation (unnecessary in Python 3)"""
|
332 | |
return not self.__eq__(other)
|
|
351 |
@property
|
|
352 |
def cmd(self):
|
|
353 |
warnings.warn(
|
|
354 |
"procrunner return object .cmd is deprecated, use .args",
|
|
355 |
DeprecationWarning,
|
|
356 |
stacklevel=2,
|
|
357 |
)
|
|
358 |
return self.args
|
|
359 |
|
|
360 |
@property
|
|
361 |
def command(self):
|
|
362 |
warnings.warn(
|
|
363 |
"procrunner return object .command is deprecated, use .args",
|
|
364 |
DeprecationWarning,
|
|
365 |
stacklevel=2,
|
|
366 |
)
|
|
367 |
return self.args
|
|
368 |
|
|
369 |
@property
|
|
370 |
def exitcode(self):
|
|
371 |
warnings.warn(
|
|
372 |
"procrunner return object .exitcode is deprecated, use .returncode",
|
|
373 |
DeprecationWarning,
|
|
374 |
stacklevel=2,
|
|
375 |
)
|
|
376 |
return self.returncode
|
|
377 |
|
|
378 |
@property
|
|
379 |
def timeout(self):
|
|
380 |
warnings.warn(
|
|
381 |
"procrunner return object .timeout is deprecated",
|
|
382 |
DeprecationWarning,
|
|
383 |
stacklevel=2,
|
|
384 |
)
|
|
385 |
return self._extras["timeout"]
|
|
386 |
|
|
387 |
@property
|
|
388 |
def runtime(self):
|
|
389 |
warnings.warn(
|
|
390 |
"procrunner return object .runtime is deprecated",
|
|
391 |
DeprecationWarning,
|
|
392 |
stacklevel=2,
|
|
393 |
)
|
|
394 |
return self._extras["runtime"]
|
|
395 |
|
|
396 |
@property
|
|
397 |
def time_start(self):
|
|
398 |
warnings.warn(
|
|
399 |
"procrunner return object .time_start is deprecated",
|
|
400 |
DeprecationWarning,
|
|
401 |
stacklevel=2,
|
|
402 |
)
|
|
403 |
return self._extras["time_start"]
|
|
404 |
|
|
405 |
@property
|
|
406 |
def time_end(self):
|
|
407 |
warnings.warn(
|
|
408 |
"procrunner return object .time_end is deprecated",
|
|
409 |
DeprecationWarning,
|
|
410 |
stacklevel=2,
|
|
411 |
)
|
|
412 |
return self._extras["time_end"]
|
|
413 |
|
|
414 |
def update(self, dictionary):
|
|
415 |
self._extras.update(dictionary)
|
333 | 416 |
|
334 | 417 |
|
335 | 418 |
def run(
|
|
447 | 530 |
(timeout is None) or (timeit.default_timer() < max_time)
|
448 | 531 |
):
|
449 | 532 |
if debug and timeout is not None:
|
450 | |
logger.debug("still running (T%.2fs)" % (timeit.default_timer() - max_time))
|
|
533 |
logger.debug("still running (T%.2fs)", timeit.default_timer() - max_time)
|
451 | 534 |
|
452 | 535 |
# wait for some time or until a stream is closed
|
453 | 536 |
try:
|
|
481 | 564 |
# timeout condition
|
482 | 565 |
timeout_encountered = True
|
483 | 566 |
if debug:
|
484 | |
logger.debug("timeout (T%.2fs)" % (timeit.default_timer() - max_time))
|
|
567 |
logger.debug("timeout (T%.2fs)", timeit.default_timer() - max_time)
|
485 | 568 |
|
486 | 569 |
# send terminate signal and wait some time for buffers to be read
|
487 | 570 |
p.terminate()
|
|
507 | 590 |
runtime = timeit.default_timer() - start_time
|
508 | 591 |
if timeout is not None:
|
509 | 592 |
logger.debug(
|
510 | |
"Process ended after %.1f seconds with exit code %d (T%.2fs)"
|
511 | |
% (runtime, p.returncode, timeit.default_timer() - max_time)
|
|
593 |
"Process ended after %.1f seconds with exit code %d (T%.2fs)",
|
|
594 |
runtime,
|
|
595 |
p.returncode,
|
|
596 |
timeit.default_timer() - max_time,
|
512 | 597 |
)
|
513 | 598 |
else:
|
514 | 599 |
logger.debug(
|
515 | |
"Process ended after %.1f seconds with exit code %d"
|
516 | |
% (runtime, p.returncode)
|
|
600 |
"Process ended after %.1f seconds with exit code %d", runtime, p.returncode
|
517 | 601 |
)
|
518 | 602 |
|
519 | 603 |
stdout = stdout.get_output()
|
|
521 | 605 |
time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
|
522 | 606 |
|
523 | 607 |
result = ReturnObject(
|
524 | |
{
|
525 | |
"exitcode": p.returncode,
|
526 | |
"command": command,
|
527 | |
"stdout": stdout,
|
528 | |
"stderr": stderr,
|
529 | |
"timeout": timeout_encountered,
|
530 | |
"runtime": runtime,
|
531 | |
"time_start": time_start,
|
532 | |
"time_end": time_end,
|
533 | |
}
|
|
608 |
exitcode=p.returncode,
|
|
609 |
command=command,
|
|
610 |
stdout=stdout,
|
|
611 |
stderr=stderr,
|
|
612 |
timeout=timeout_encountered,
|
|
613 |
runtime=runtime,
|
|
614 |
time_start=time_start,
|
|
615 |
time_end=time_end,
|
534 | 616 |
)
|
535 | 617 |
if stdin is not None:
|
536 | 618 |
result.update(
|