Skip to content
c3e9d6c Mar 3, 2016
@stdweird @boegel @JensTimmerman @itkovian @piojo
844 lines (683 sloc) 30.3 KB
#
# Copyright 2009-2016 Ghent University
#
# This file is part of vsc-base,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
# with support of Ghent University (http://ugent.be/hpc),
# the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
# the Flemish Research Foundation (FWO) (http://www.fwo.be/en)
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
#
# https://github.com/hpcugent/vsc-base
#
# vsc-base is free software: you can redistribute it and/or modify
# it under the terms of the GNU Library General Public License as
# published by the Free Software Foundation, either version 2 of
# the License, or (at your option) any later version.
#
# vsc-base is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public License
# along with vsc-base. If not, see <http://www.gnu.org/licenses/>.
#
"""
Python module to execute a command
Historical overview of existing equivalent code
- EasyBuild filetools module
- C{run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True, log_output=False, path=None)}
- C{run_cmd_qa(cmd, qa, no_qa=None, log_ok=True, log_all=False, simple=False, regexp=True, std_qa=None, path=None)}
- Executes a command cmd
- looks for questions and tries to answer based on qa dictionary
- returns exitcode and stdout+stderr (mixed)
- no input though stdin
- if C{log_ok} or C{log_all} are set -> will C{log.error} if non-zero exit-code
- if C{simple} is C{True} -> instead of returning a tuple (output, ec) it will just return C{True} or C{False} signifying succes
- C{regexp} -> Regex used to check the output for errors. If C{True} will use default (see C{parselogForError})
- if log_output is True -> all output of command will be logged to a tempfile
- path is the path run_cmd should chdir to before doing anything
- Q&A: support reading stdout asynchronous and replying to a question through stdin
- Manage C{managecommands} module C{Command} class
- C{run} method
- python-package-vsc-utils run module Command class
- C{run} method
- C{mympirun} (old)
- C{runrun(self, cmd, returnout=False, flush=False, realcmd=False)}:
- C{runrunnormal(self, cmd, returnout=False, flush=False)}
- C{runrunfile(self, cmd, returnout=False, flush=False)}
- C{hanything} commands/command module
- C{run} method
- fake pty support
@author: Stijn De Weirdt (Ghent University)
"""
import errno
import logging
import os
import pty
import re
import signal
import sys
import time
from vsc.utils.fancylogger import getLogger, getAllExistingLoggers
PROCESS_MODULE_ASYNCPROCESS_PATH = 'vsc.utils.asyncprocess'
PROCESS_MODULE_SUBPROCESS_PATH = 'subprocess'
RUNRUN_TIMEOUT_OUTPUT = ''
RUNRUN_TIMEOUT_EXITCODE = 123
RUNRUN_QA_MAX_MISS_EXITCODE = 124
BASH = '/bin/bash'
SHELL = BASH
class DummyFunction(object):
def __getattr__(self, name):
def dummy(*args, **kwargs):
pass
return dummy
class Run(object):
"""Base class for static run method"""
INIT_INPUT_CLOSE = True
USE_SHELL = True
SHELL = SHELL # set the shell via the module constant
@classmethod
def run(cls, cmd, **kwargs):
"""static method
return (exitcode,output)
"""
r = cls(cmd, **kwargs)
return r._run()
def __init__(self, cmd=None, **kwargs):
"""
Handle initiliastion
@param cmd: command to run
@param input: set "simple" input
@param startpath: directory to change to before executing command
@param disable_log: use fake logger (won't log anything)
@param use_shell: use the subshell
@param shell: change the shell
"""
self.input = kwargs.pop('input', None)
self.startpath = kwargs.pop('startpath', None)
self.use_shell = kwargs.pop('use_shell', self.USE_SHELL)
self.shell = kwargs.pop('shell', self.SHELL)
if kwargs.pop('disable_log', None):
self.log = DummyFunction() # No logging
if not hasattr(self, 'log'):
self.log = getLogger(self._get_log_name())
self.cmd = cmd # actual command
self._cwd_before_startpath = None
self._process_module = None
self._process = None
self.readsize = 1024 # number of bytes to read blocking
self._shellcmd = None
self._popen_named_args = None
self._process_exitcode = None
self._process_output = None
self._post_exitcode_log_failure = self.log.error
super(Run, self).__init__(**kwargs)
def _get_log_name(self):
"""Set the log name"""
return self.__class__.__name__
def _prep_module(self, modulepath=None, extendfromlist=None):
# these will provide the required Popen, PIPE and STDOUT
if modulepath is None:
modulepath = PROCESS_MODULE_SUBPROCESS_PATH
fromlist = ['Popen', 'PIPE', 'STDOUT']
if extendfromlist is not None:
fromlist.extend(extendfromlist)
self._process_modulepath = modulepath
self._process_module = __import__(self._process_modulepath, globals(), locals(), fromlist)
def _run(self):
"""actual method
Structure
- pre
- convert command to shell command - DONE
- chdir before start - DONE
- start C{Popen} - DONE
- support async and subprocess - DONE
- support for
- filehandle
- PIPE - DONE
- pty - DONE
- main
- should capture exitcode and output
- features
- separate stdout and stderr ?
- simple single run
- no timeout/waiting - DONE
- flush to
- stdout
- logger - DONE
- both stdout and logger
- process intermediate output
- qa
- input
- qa
- from file ?
- text - DONE
- post
- parse with regexp
- raise/log error on match
- return
- return output
- log output
- write to file
- return in string - DONE
- on C{ec > 0}
- error - DONE
- raiseException
- simple
- just return True/False
"""
self._run_pre()
self._wait_for_process()
return self._run_post()
def _run_pre(self):
"""Non-blocking start"""
if self._process_module is None:
self._prep_module()
if self.startpath is not None:
self._start_in_path()
if self._shellcmd is None:
self._make_shell_command()
if self._popen_named_args is None:
self._make_popen_named_args()
self._init_process()
self._init_input()
def _run_post(self):
self._cleanup_process()
self._post_exitcode()
self._post_output()
if self.startpath is not None:
self._return_to_previous_start_in_path()
return self._run_return()
def _start_in_path(self):
"""Change path before the run"""
if self.startpath is None:
self.log.debug("_start_in_path: no startpath set")
return
if os.path.exists(self.startpath):
if os.path.isdir(self.startpath):
try:
self._cwd_before_startpath = os.getcwd() # store it some one can return to it
os.chdir(self.startpath)
except:
self.raiseException("_start_in_path: failed to change path from %s to startpath %s" %
(self._cwd_before_startpath, self.startpath))
else:
self.log.raiseExcpetion("_start_in_path: provided startpath %s exists but is no directory" %
self.startpath)
else:
self.raiseException("_start_in_path: startpath %s does not exist" % self.startpath)
def _return_to_previous_start_in_path(self):
"""Change to original path before the change to startpath"""
if self._cwd_before_startpath is None:
self.log.warning("_return_to_previous_start_in_path: previous cwd is empty. Not trying anything")
return
if os.path.exists(self._cwd_before_startpath):
if os.path.isdir(self._cwd_before_startpath):
try:
currentpath = os.getcwd()
if not currentpath == self.startpath:
self.log.warning(("_return_to_previous_start_in_path: current diretory %s does not match "
"startpath %s") % (currentpath, self.startpath))
os.chdir(self._cwd_before_startpath)
except:
self.raiseException(("_return_to_previous_start_in_path: failed to change path from current %s "
"to previous path %s") % (currentpath, self._cwd_before_startpath))
else:
self.log.raiseExcpetion(("_return_to_previous_start_in_path: provided previous cwd path %s exists "
"but is no directory") % self._cwd_before_startpath)
else:
self.raiseException("_return_to_previous_start_in_path: previous cwd path %s does not exist" %
self._cwd_before_startpath)
def _make_popen_named_args(self, others=None):
"""Create the named args for Popen"""
self._popen_named_args = {
'stdout': self._process_module.PIPE,
'stderr': self._process_module.STDOUT,
'stdin': self._process_module.PIPE,
'close_fds': True,
'shell': self.use_shell,
'executable': self.shell,
}
if others is not None:
self._popen_named_args.update(others)
self.log.debug("_popen_named_args %s" % self._popen_named_args)
def _make_shell_command(self):
"""Convert cmd into shell command"""
if self.cmd is None:
self.log.raiseExcpetion("_make_shell_command: no cmd set.")
if isinstance(self.cmd, basestring):
self._shellcmd = self.cmd
elif isinstance(self.cmd, (list, tuple,)):
self._shellcmd = " ".join(self.cmd)
else:
self.log.raiseException("Failed to convert cmd %s (type %s) into shell command" % (self.cmd, type(self.cmd)))
def _init_process(self):
"""Initialise the self._process"""
try:
self._process = self._process_module.Popen(self._shellcmd, **self._popen_named_args)
except OSError:
self.log.raiseException("_init_process: init Popen shellcmd %s failed: %s" % (self._shellcmd))
def _init_input(self):
"""Handle input, if any in a simple way"""
if self.input is not None: # allow empty string (whatever it may mean)
try:
self._process.stdin.write(self.input)
except:
self.log.raiseException("_init_input: Failed write input %s to process" % self.input)
if self.INIT_INPUT_CLOSE:
self._process.stdin.close()
self.log.debug("_init_input: process stdin closed")
else:
self.log.debug("_init_input: process stdin NOT closed")
def _wait_for_process(self):
"""The main loop
This one has most simple loop
"""
try:
self._process_exitcode = self._process.wait()
self._process_output = self._read_process(-1) # -1 is read all
except:
self.log.raiseException("_wait_for_process: problem during wait exitcode %s output %s" %
(self._process_exitcode, self._process_output))
def _cleanup_process(self):
"""Cleanup any leftovers from the process"""
pass
def _read_process(self, readsize=None):
"""Read from process, return out"""
if readsize is None:
readsize = self.readsize
if readsize is None:
readsize = -1 # read all
self.log.debug("_read_process: going to read with readsize %s" % readsize)
out = self._process.stdout.read(readsize)
return out
def _post_exitcode(self):
"""Postprocess the exitcode in self._process_exitcode"""
if not self._process_exitcode == 0:
self._post_exitcode_log_failure("_post_exitcode: problem occured with cmd %s: output %s" %
(self.cmd, self._process_output))
else:
self.log.debug("_post_exitcode: success cmd %s: output %s" % (self.cmd, self._process_output))
def _post_output(self):
"""Postprocess the output in self._process_output"""
pass
def _run_return(self):
"""What to return"""
return self._process_exitcode, self._process_output
def _killtasks(self, tasks=None, sig=signal.SIGKILL, kill_pgid=False):
"""
Kill all tasks
@param: tasks list of processids
@param: sig, signal to use to kill
@apram: kill_pgid, send kill to group
"""
if tasks is None:
self.log.error("killtasks no tasks passed")
elif isinstance(tasks, basestring):
try:
tasks = [int(tasks)]
except:
self.log.error("killtasks failed to convert tasks string %s to int" % tasks)
for pid in tasks:
pgid = os.getpgid(pid)
try:
os.kill(int(pid), sig)
if kill_pgid:
os.killpg(pgid, sig)
self.log.debug("Killed %s with signal %s" % (pid, sig))
except OSError, err:
# ERSCH is no such process, so no issue
if not err.errno == errno.ESRCH:
self.log.error("Failed to kill %s: %s" % (pid, err))
except Exception, err:
self.log.error("Failed to kill %s: %s" % (pid, err))
def stop_tasks(self):
"""Cleanup current run"""
self._killtasks(tasks=[self._process.pid])
try:
os.waitpid(-1, os.WNOHANG)
except:
pass
class RunNoWorries(Run):
"""When the exitcode is >0, log.debug instead of log.error"""
def __init__(self, cmd, **kwargs):
super(RunNoWorries, self).__init__(cmd, **kwargs)
self._post_exitcode_log_failure = self.log.debug
class RunLoopException(Exception):
def __init__(self, code, output):
self.code = code
self.output = output
def __str__(self):
return "%s code %s output %s" % (self.__class__.__name__, self.code, self.output)
class RunLoop(Run):
"""Main process is a while loop which reads the output in blocks
need to read from time to time.
otherwise the stdout/stderr buffer gets filled and it all stops working
"""
LOOP_TIMEOUT_INIT = 0.1
LOOP_TIMEOUT_MAIN = 1
def __init__(self, cmd, **kwargs):
super(RunLoop, self).__init__(cmd, **kwargs)
self._loop_count = None
self._loop_continue = None # intial state, change this to break out the loop
def _wait_for_process(self):
"""Loop through the process in timesteps
collected output is run through _loop_process_output
"""
# these are initialised outside the function (cannot be forgotten, but can be overwritten)
self._loop_count = 0 # internal counter
self._loop_continue = True
self._process_output = ''
# further initialisation
self._loop_initialise()
time.sleep(self.LOOP_TIMEOUT_INIT)
ec = self._process.poll()
try:
while self._loop_continue and ec < 0:
output = self._read_process()
self._process_output += output
# process after updating the self._process_ vars
self._loop_process_output(output)
if len(output) == 0:
time.sleep(self.LOOP_TIMEOUT_MAIN)
ec = self._process.poll()
self._loop_count += 1
self.log.debug("_wait_for_process: loop stopped after %s iterations (ec %s loop_continue %s)" %
(self._loop_count, ec, self._loop_continue))
# read remaining data (all of it)
output = self._read_process(-1)
self._process_output += output
self._process_exitcode = ec
# process after updating the self._process_ vars
self._loop_process_output_final(output)
except RunLoopException, err:
self.log.debug('RunLoopException %s' % err)
self._process_output = err.output
self._process_exitcode = err.code
def _loop_initialise(self):
"""Initialisation before the loop starts"""
pass
def _loop_process_output(self, output):
"""Process the output that is read in blocks
simplest form: do nothing
"""
pass
def _loop_process_output_final(self, output):
"""Process the remaining output that is read
simplest form: do the same as _loop_process_output
"""
self._loop_process_output(output)
class RunLoopLog(RunLoop):
LOOP_LOG_LEVEL = logging.INFO
def _wait_for_process(self):
# initialise the info logger
self.log.info("Going to run cmd %s" % self._shellcmd)
super(RunLoopLog, self)._wait_for_process()
def _loop_process_output(self, output):
"""Process the output that is read in blocks
send it to the logger. The logger need to be stream-like
"""
self.log.streamLog(self.LOOP_LOG_LEVEL, output)
super(RunLoopLog, self)._loop_process_output(output)
class RunLoopStdout(RunLoop):
def _loop_process_output(self, output):
"""Process the output that is read in blocks
send it to the stdout
"""
sys.stdout.write(output)
sys.stdout.flush()
super(RunLoopStdout, self)._loop_process_output(output)
class RunAsync(Run):
"""Async process class"""
def _prep_module(self, modulepath=None, extendfromlist=None):
# these will provide the required Popen, PIPE and STDOUT
if modulepath is None:
modulepath = PROCESS_MODULE_ASYNCPROCESS_PATH
if extendfromlist is None:
extendfromlist = ['send_all', 'recv_some']
super(RunAsync, self)._prep_module(modulepath=modulepath, extendfromlist=extendfromlist)
def _read_process(self, readsize=None):
"""Read from async process, return out"""
if readsize is None:
readsize = self.readsize
if self._process.stdout is None:
# Nothing yet/anymore
return ''
try:
if readsize is not None and readsize < 0:
# read all blocking (it's not why we should use async
out = self._process.stdout.read()
else:
# non-blocking read (readsize is a maximum to return !
out = self._process_module.recv_some(self._process, maxread=readsize)
return out
except (IOError, Exception):
# recv_some may throw Exception
self.log.exception("_read_process: read failed")
return ''
class RunFile(Run):
"""Popen to filehandle"""
def __init__(self, cmd, **kwargs):
self.filename = kwargs.pop('filename', None)
self.filehandle = None
super(RunFile, self).__init__(cmd, **kwargs)
def _make_popen_named_args(self, others=None):
if others is None:
if os.path.exists(self.filename):
if os.path.isfile(self.filename):
self.log.warning("_make_popen_named_args: going to overwrite existing file %s" % self.filename)
elif os.path.isdir(self.filename):
self.raiseException(("_make_popen_named_args: writing to filename %s impossible. Path exists and "
"is a directory.") % self.filename)
else:
self.raiseException("_make_popen_named_args: path exists and is not a file or directory %s" %
self.filename)
else:
dirname = os.path.dirname(self.filename)
if dirname and not os.path.isdir(dirname):
try:
os.makedirs(dirname)
except:
self.log.raiseException(("_make_popen_named_args: dirname %s for file %s does not exists. "
"Creating it failed.") % (dirname, self.filename))
try:
self.filehandle = open(self.filename, 'w')
except:
self.log.raiseException("_make_popen_named_args: failed to open filehandle for file %s" % self.filename)
others = {
'stdout': self.filehandle,
}
super(RunFile, self)._make_popen_named_args(others=others)
def _cleanup_process(self):
"""Close the filehandle"""
try:
self.filehandle.close()
except:
self.log.raiseException("_cleanup_process: failed to close filehandle for filename %s" % self.filename)
def _read_process(self, readsize=None):
"""Meaningless for filehandle"""
return ''
class RunPty(Run):
"""Pty support (eg for screen sessions)"""
def _read_process(self, readsize=None):
"""This does not work for pty"""
return ''
def _make_popen_named_args(self, others=None):
if others is None:
(master, slave) = pty.openpty()
others = {
'stdin': slave,
'stdout': slave,
'stderr': slave
}
super(RunPty, self)._make_popen_named_args(others=others)
class RunTimeout(RunLoop, RunAsync):
"""Question/Answer processing"""
def __init__(self, cmd, **kwargs):
self.timeout = float(kwargs.pop('timeout', None))
self.start = time.time()
super(RunTimeout, self).__init__(cmd, **kwargs)
def _loop_process_output(self, output):
""""""
time_passed = time.time() - self.start
if self.timeout is not None and time_passed > self.timeout:
self.log.debug("Time passed %s > timeout %s." % (time_passed, self.timeout))
self.stop_tasks()
# go out of loop
raise RunLoopException(RUNRUN_TIMEOUT_EXITCODE, RUNRUN_TIMEOUT_OUTPUT)
super(RunTimeout, self)._loop_process_output(output)
class RunQA(RunLoop, RunAsync):
"""Question/Answer processing"""
LOOP_MAX_MISS_COUNT = 20
INIT_INPUT_CLOSE = False
CYCLE_ANSWERS = True
def __init__(self, cmd, **kwargs):
"""
Add question and answer style running
@param qa: dict with exact questions and answers
@param qa_reg: dict with (named) regex-questions and answers (answers can contain named string templates)
@param no_qa: list of regex that can block the output, but is not seen as a question.
Regular expressions are compiled, just pass the (raw) text.
"""
qa = kwargs.pop('qa', {})
qa_reg = kwargs.pop('qa_reg', {})
no_qa = kwargs.pop('no_qa', [])
self._loop_miss_count = None # maximum number of misses
self._loop_previous_ouput_length = None # track length of output through loop
super(RunQA, self).__init__(cmd, **kwargs)
self.qa, self.qa_reg, self.no_qa = self._parse_qa(qa, qa_reg, no_qa)
def _parse_qa(self, qa, qa_reg, no_qa):
"""
process the QandA dictionary
- given initial set of Q and A (in dict), return dict of reg. exp. and A
- make regular expression that matches the string with
- replace whitespace
- replace newline
- qa_reg: question is compiled as is, and whitespace+ending is added
- provided answers can be either strings or lists of strings (which will be used iteratively)
"""
def escape_special(string):
specials = '.*+?(){}[]|\$^'
return re.sub(r"([%s])" % ''.join(['\%s' % x for x in specials]), r"\\\1", string)
SPLIT = '[\s\n]+'
REG_SPLIT = re.compile(r"" + SPLIT)
def process_answers(answers):
"""Construct list of newline-terminated answers (as strings)."""
if isinstance(answers, basestring):
answers = [answers]
elif isinstance(answers, list):
# list is manipulated when answering matching question, so take a copy
answers = answers[:]
else:
msg_tmpl = "Invalid type for answer, not a string or list: %s (%s)"
self.log.raiseException(msg_tmpl % (type(answers), answers), exception=TypeError)
# add optional split at the end
for i in [idx for idx, a in enumerate(answers) if not a.endswith('\n')]:
answers[i] += '\n'
return answers
def process_question(question):
"""Convert string question to regex."""
split_q = [escape_special(x) for x in REG_SPLIT.split(question)]
reg_q_txt = SPLIT.join(split_q) + SPLIT.rstrip('+') + "*$"
reg_q = re.compile(r"" + reg_q_txt)
if reg_q.search(question):
return reg_q
else:
# this is just a sanity check on the created regex, can this actually occur?
msg_tmpl = "_parse_qa process_question: question %s converted in %s does not match itself"
self.log.raiseException(msg_tmpl % (question.pattern, reg_q_txt), exception=ValueError)
new_qa = {}
self.log.debug("new_qa: ")
for question, answers in qa.items():
reg_q = process_question(question)
new_qa[reg_q] = process_answers(answers)
self.log.debug("new_qa[%s]: %s" % (reg_q.pattern.__repr__(), answers))
new_qa_reg = {}
self.log.debug("new_qa_reg: ")
for question, answers in qa_reg.items():
reg_q = re.compile(r"" + question + r"[\s\n]*$")
new_qa_reg[reg_q] = process_answers(answers)
self.log.debug("new_qa_reg[%s]: %s" % (reg_q.pattern.__repr__(), answers))
# simple statements, can contain wildcards
new_no_qa = [re.compile(r"" + x + r"[\s\n]*$") for x in no_qa]
self.log.debug("new_no_qa: %s" % [x.pattern.__repr__() for x in new_no_qa])
return new_qa, new_qa_reg, new_no_qa
def _loop_initialise(self):
"""Initialisation before the loop starts"""
self._loop_miss_count = 0
self._loop_previous_ouput_length = 0
def _loop_process_output(self, output):
"""Process the output that is read in blocks
check the output passed to questions available
"""
hit = False
self.log.debug('output %s all_output %s' % (output, self._process_output))
# qa first and then qa_reg
nr_qa = len(self.qa)
for idx, (question, answers) in enumerate(self.qa.items() + self.qa_reg.items()):
res = question.search(self._process_output)
if output and res:
answer = answers[0] % res.groupdict()
if len(answers) > 1:
prev_answer = answers.pop(0)
if self.CYCLE_ANSWERS:
answers.append(prev_answer)
self.log.debug("New answers list for question %s: %s" % (question.pattern, answers))
self.log.debug("_loop_process_output: answer %s question %s (std: %s) out %s" %
(answer, question.pattern, idx >= nr_qa, self._process_output[-50:]))
self._process_module.send_all(self._process, answer)
hit = True
break
if not hit:
curoutlen = len(self._process_output)
if curoutlen > self._loop_previous_ouput_length:
# still progress in output, just continue (but don't reset miss counter either)
self._loop_previous_ouput_length = curoutlen
else:
noqa = False
for r in self.no_qa:
if r.search(self._process_output):
self.log.debug("_loop_process_output: no_qa found for out %s" % self._process_output[-50:])
noqa = True
if not noqa:
self._loop_miss_count += 1
else:
self._loop_miss_count = 0 # rreset miss counter on hit
if self._loop_miss_count > self.LOOP_MAX_MISS_COUNT:
self.log.debug("loop_process_output: max misses LOOP_MAX_MISS_COUNT %s reached. End of output: %s" %
(self.LOOP_MAX_MISS_COUNT, self._process_output[-500:]))
self.stop_tasks()
# go out of loop
raise RunLoopException(RUNRUN_QA_MAX_MISS_EXITCODE, self._process_output)
super(RunQA, self)._loop_process_output(output)
class RunAsyncLoop(RunLoop, RunAsync):
"""Async read in loop"""
pass
class RunAsyncLoopLog(RunLoopLog, RunAsync):
"""Async read, log to logger"""
pass
class RunQALog(RunLoopLog, RunQA):
"""Async loop QA with LoopLog"""
pass
class RunQAStdout(RunLoopStdout, RunQA):
"""Async loop QA with LoopLogStdout"""
pass
class RunAsyncLoopStdout(RunLoopStdout, RunAsync):
"""Async read, flush to stdout"""
pass
# convenient names
# eg: from vsc.utils.run import trivial
run_simple = Run.run
run_simple_noworries = RunNoWorries.run
run_async = RunAsync.run
run_asyncloop = RunAsyncLoop.run
run_timeout = RunTimeout.run
run_to_file = RunFile.run
run_async_to_stdout = RunAsyncLoopStdout.run
run_qa = RunQA.run
run_qalog = RunQALog.run
run_qastdout = RunQAStdout.run
if __name__ == "__main__":
run_simple('echo ok')
Something went wrong with that request. Please try again.