def runTest(self): """ try echoing some text and see if it comes back out """ p.send_all(self.shell, "echo hello\n") self.assertEqual(p.recv_some(self.shell), "hello\n") p.send_all(self.shell, "echo hello world\n") self.assertEqual(p.recv_some(self.shell), "hello world\n") p.send_all(self.shell, "exit\n") self.assertEqual("", p.recv_some(self.shell, e=0)) self.assertRaises(Exception, p.recv_some, self.shell)
def runTest(self): """ try echoing some text and see if it comes back out """ p.send_all(self.shell, "echo hello\n") time.sleep(0.1) self.assertEqual(p.recv_some(self.shell), "hello\n") p.send_all(self.shell, "echo hello world\n") time.sleep(0.1) self.assertEqual(p.recv_some(self.shell), "hello world\n") p.send_all(self.shell, "exit\n") time.sleep(0.1) self.assertEqual("", p.recv_some(self.shell, e=0)) self.assertRaises(Exception, p.recv_some, self.shell)
def run_cmd_qa(cmd, qa, no_qa=None, log_ok=True, log_all=False, simple=False, regexp=True, std_qa=None, path=None, maxhits=50, trace=True): """ Run specified interactive command (in a subshell) :param cmd: command to run :param qa: dictionary which maps question to answers :param no_qa: list of patters that are not questions :param log_ok: only run output/exit code for failing commands (exit code non-zero) :param log_all: always log command output and exit code :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code) :param regex: regex used to check the output for errors; if True it will use the default (see parse_log_for_error) :param std_qa: dictionary which maps question regex patterns to answers :param path: path to execute the command is; current working directory is used if unspecified :param maxhits: maximum number of cycles (seconds) without being able to find a known question :param trace: print command being executed as part of trace output """ cwd = os.getcwd() if log_all or (trace and build_option('trace')): # collect output of running command in temporary log file, if desired fd, cmd_log_fn = tempfile.mkstemp(suffix='.log', prefix='easybuild-run_cmd_qa-') os.close(fd) try: cmd_log = open(cmd_log_fn, 'w') except IOError as err: raise EasyBuildError( "Failed to open temporary log file for output of interactive command: %s", err) _log.debug('run_cmd_qa: Output of "%s" will be logged to %s' % (cmd, cmd_log_fn)) else: cmd_log_fn, cmd_log = None, None start_time = datetime.now() if trace: trace_txt = "running interactive command:\n" trace_txt += "\t[started at: %s]\n" % start_time.strftime( '%Y-%m-%d %H:%M:%S') trace_txt += "\t[working dir: %s]\n" % (path or os.getcwd()) trace_txt += "\t[output logged in %s]\n" % cmd_log_fn trace_msg(trace_txt + '\t' + cmd.strip()) # early exit in 'dry run' mode, after printing the command that would be run if build_option('extended_dry_run'): if path is None: path = cwd dry_run_msg(" running interactive command \"%s\"" % cmd, silent=build_option('silent')) dry_run_msg(" (in %s)" % path, silent=build_option('silent')) if simple: return True else: # output, exit code return ('', 0) try: if path: os.chdir(path) _log.debug("run_cmd_qa: running cmd %s (in %s)" % (cmd, os.getcwd())) except OSError as err: _log.warning("Failed to change to %s: %s" % (path, err)) _log.info("running cmd %s in non-existing directory, might fail!" % cmd) # Part 1: 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 def escape_special(string): return re.sub(r"([\+\?\(\)\[\]\*\.\\\$])", r"\\\1", string) split = r'[\s\n]+' regSplit = re.compile(r"" + split) def process_QA(q, a_s): splitq = [escape_special(x) for x in regSplit.split(q)] regQtxt = split.join(splitq) + split.rstrip('+') + "*$" # add optional split at the end for i in [idx for idx, a in enumerate(a_s) if not a.endswith('\n')]: a_s[i] += '\n' regQ = re.compile(r"" + regQtxt) if regQ.search(q): return (a_s, regQ) else: raise EasyBuildError( "runqanda: Question %s converted in %s does not match itself", q, regQtxt) def check_answers_list(answers): """Make sure we have a list of answers (as strings).""" if isinstance(answers, string_type): answers = [answers] elif not isinstance(answers, list): raise EasyBuildError( "Invalid type for answer on %s, no string or list: %s (%s)", question, type(answers), answers) # list is manipulated when answering matching question, so return a copy return answers[:] new_qa = {} _log.debug("new_qa: ") for question, answers in qa.items(): answers = check_answers_list(answers) (answers, regQ) = process_QA(question, answers) new_qa[regQ] = answers _log.debug("new_qa[%s]: %s" % (regQ.pattern, new_qa[regQ])) new_std_qa = {} if std_qa: for question, answers in std_qa.items(): regQ = re.compile(r"" + question + r"[\s\n]*$") answers = check_answers_list(answers) for i in [ idx for idx, a in enumerate(answers) if not a.endswith('\n') ]: answers[i] += '\n' new_std_qa[regQ] = answers _log.debug("new_std_qa[%s]: %s" % (regQ.pattern, new_std_qa[regQ])) new_no_qa = [] if no_qa: # simple statements, can contain wildcards new_no_qa = [re.compile(r"" + x + r"[\s\n]*$") for x in no_qa] _log.debug("New noQandA list is: %s" % [x.pattern for x in new_no_qa]) # Part 2: Run the command and answer questions # - this needs asynchronous stdout # # Log command output if cmd_log: cmd_log.write("# output for interactive command: %s\n\n" % cmd) try: proc = asyncprocess.Popen(cmd, shell=True, stdout=asyncprocess.PIPE, stderr=asyncprocess.STDOUT, stdin=asyncprocess.PIPE, close_fds=True, executable='/bin/bash') except OSError as err: raise EasyBuildError("run_cmd_qa init cmd %s failed:%s", cmd, err) ec = proc.poll() stdout_err = '' old_len_out = -1 hit_count = 0 while ec is None: # need to read from time to time. # - otherwise the stdout/stderr buffer gets filled and it all stops working try: out = get_output_from_process(proc, asynchronous=True) if cmd_log: cmd_log.write(out) stdout_err += out # recv_some used by get_output_from_process for getting asynchronous output may throw exception except (IOError, Exception) as err: _log.debug("run_cmd_qa cmd %s: read failed: %s", cmd, err) out = None hit = False for question, answers in new_qa.items(): res = question.search(stdout_err) if out and res: fa = answers[0] % res.groupdict() # cycle through list of answers last_answer = answers.pop(0) answers.append(last_answer) _log.debug("List of answers for question %s after cycling: %s", question.pattern, answers) _log.debug("run_cmd_qa answer %s question %s out %s", fa, question.pattern, stdout_err[-50:]) asyncprocess.send_all(proc, fa) hit = True break if not hit: for question, answers in new_std_qa.items(): res = question.search(stdout_err) if out and res: fa = answers[0] % res.groupdict() # cycle through list of answers last_answer = answers.pop(0) answers.append(last_answer) _log.debug( "List of answers for question %s after cycling: %s", question.pattern, answers) _log.debug("run_cmd_qa answer %s std question %s out %s", fa, question.pattern, stdout_err[-50:]) asyncprocess.send_all(proc, fa) hit = True break if not hit: if len(stdout_err) > old_len_out: old_len_out = len(stdout_err) else: noqa = False for r in new_no_qa: if r.search(stdout_err): _log.debug("runqanda: noQandA found for out %s", stdout_err[-50:]) noqa = True if not noqa: hit_count += 1 else: hit_count = 0 else: hit_count = 0 if hit_count > maxhits: # explicitly kill the child process before exiting try: os.killpg(proc.pid, signal.SIGKILL) os.kill(proc.pid, signal.SIGKILL) except OSError as err: _log.debug( "run_cmd_qa exception caught when killing child process: %s", err) _log.debug("run_cmd_qa: full stdouterr: %s", stdout_err) raise EasyBuildError( "run_cmd_qa: cmd %s : Max nohits %s reached: end of output %s", cmd, maxhits, stdout_err[-500:]) # the sleep below is required to avoid exiting on unknown 'questions' too early (see above) time.sleep(1) ec = proc.poll() # Process stopped. Read all remaining data try: if proc.stdout: out = get_output_from_process(proc) stdout_err += out if cmd_log: cmd_log.write(out) cmd_log.close() except IOError as err: _log.debug("runqanda cmd %s: remaining data read failed: %s", cmd, err) if trace: trace_msg("interactive command completed: exit %s, ran in %s" % (ec, time_str_since(start_time))) try: os.chdir(cwd) except OSError as err: raise EasyBuildError( "Failed to return to %s after executing command: %s", cwd, err) return parse_cmd_output(cmd, stdout_err, ec, simple, log_all, log_ok, regexp)
tmpOut = recv_some(p) if runLog: runLog.write(tmpOut) stdoutErr += tmpOut # recv_some may throw Exception except (IOError, Exception), err: _log.debug("run_cmd_qa cmd %s: read failed: %s" % (cmd, err)) tmpOut = None hit = False for q, a in newQA.items(): res = q.search(stdoutErr) if tmpOut and res: fa = a % res.groupdict() _log.debug("run_cmd_qa answer %s question %s out %s" % (fa, q.pattern, stdoutErr[-50:])) send_all(p, fa) hit = True break if not hit: for q, a in newstdQA.items(): res = q.search(stdoutErr) if tmpOut and res: fa = a % res.groupdict() _log.debug("run_cmd_qa answer %s standard question %s out %s" % (fa, q.pattern, stdoutErr[-50:])) send_all(p, fa) hit = True break if not hit: if len(stdoutErr) > oldLenOut: oldLenOut = len(stdoutErr) else:
def test_get_output_from_process(self): """Test for get_output_from_process utility function.""" def get_proc(cmd, asynchronous=False): if asynchronous: proc = asyncprocess.Popen(cmd, shell=True, stdout=asyncprocess.PIPE, stderr=asyncprocess.STDOUT, stdin=asyncprocess.PIPE, close_fds=True, executable='/bin/bash') else: proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, close_fds=True, executable='/bin/bash') return proc # get all output at once proc = get_proc("echo hello") out = get_output_from_process(proc) self.assertEqual(out, 'hello\n') # first get 100 bytes, then get the rest all at once proc = get_proc("echo hello") out = get_output_from_process(proc, read_size=100) self.assertEqual(out, 'hello\n') out = get_output_from_process(proc) self.assertEqual(out, '') # get output in small bits, keep trying to get output (which shouldn't fail) proc = get_proc("echo hello") out = get_output_from_process(proc, read_size=1) self.assertEqual(out, 'h') out = get_output_from_process(proc, read_size=3) self.assertEqual(out, 'ell') out = get_output_from_process(proc, read_size=2) self.assertEqual(out, 'o\n') out = get_output_from_process(proc, read_size=1) self.assertEqual(out, '') out = get_output_from_process(proc, read_size=10) self.assertEqual(out, '') out = get_output_from_process(proc) self.assertEqual(out, '') # can also get output asynchronously (read_size is *ignored* in that case) async_cmd = "echo hello; read reply; echo $reply" proc = get_proc(async_cmd, asynchronous=True) out = get_output_from_process(proc, asynchronous=True) self.assertEqual(out, 'hello\n') asyncprocess.send_all(proc, 'test123\n') out = get_output_from_process(proc) self.assertEqual(out, 'test123\n') proc = get_proc(async_cmd, asynchronous=True) out = get_output_from_process(proc, asynchronous=True, read_size=1) # read_size is ignored when getting output asynchronously, we're getting more than 1 byte! self.assertEqual(out, 'hello\n') asyncprocess.send_all(proc, 'test123\n') out = get_output_from_process(proc, read_size=3) self.assertEqual(out, 'tes') out = get_output_from_process(proc, read_size=2) self.assertEqual(out, 't1') out = get_output_from_process(proc) self.assertEqual(out, '23\n')
if runLog: runLog.write(tmpOut) stdoutErr += tmpOut # recv_some may throw Exception except (IOError, Exception), err: _log.debug("run_cmd_qa cmd %s: read failed: %s" % (cmd, err)) tmpOut = None hit = False for q, a in newQA.items(): res = q.search(stdoutErr) if tmpOut and res: fa = a % res.groupdict() _log.debug("run_cmd_qa answer %s question %s out %s" % (fa, q.pattern, stdoutErr[-50:])) send_all(p, fa) hit = True break if not hit: for q, a in newstdQA.items(): res = q.search(stdoutErr) if tmpOut and res: fa = a % res.groupdict() _log.debug( "run_cmd_qa answer %s standard question %s out %s" % (fa, q.pattern, stdoutErr[-50:])) send_all(p, fa) hit = True break if not hit: if len(stdoutErr) > oldLenOut:
def run_cmd_qa(cmd, qa, no_qa=None, log_ok=True, log_all=False, simple=False, regexp=True, std_qa=None, path=None, maxhits=50, trace=True): """ Run specified interactive command (in a subshell) :param cmd: command to run :param qa: dictionary which maps question to answers :param no_qa: list of patters that are not questions :param log_ok: only run output/exit code for failing commands (exit code non-zero) :param log_all: always log command output and exit code :param simple: if True, just return True/False to indicate success, else return a tuple: (output, exit_code) :param regex: regex used to check the output for errors; if True it will use the default (see parse_log_for_error) :param std_qa: dictionary which maps question regex patterns to answers :param path: path to execute the command is; current working directory is used if unspecified :param maxhits: maximum number of cycles (seconds) without being able to find a known question :param trace: print command being executed as part of trace output """ cwd = os.getcwd() if log_all or (trace and build_option('trace')): # collect output of running command in temporary log file, if desired fd, cmd_log_fn = tempfile.mkstemp(suffix='.log', prefix='easybuild-run_cmd_qa-') os.close(fd) try: cmd_log = open(cmd_log_fn, 'w') except IOError as err: raise EasyBuildError("Failed to open temporary log file for output of interactive command: %s", err) _log.debug('run_cmd_qa: Output of "%s" will be logged to %s' % (cmd, cmd_log_fn)) else: cmd_log_fn, cmd_log = None, None start_time = datetime.now() if trace: trace_txt = "running interactive command:\n" trace_txt += "\t[started at: %s]\n" % start_time.strftime('%Y-%m-%d %H:%M:%S') trace_txt += "\t[output logged in %s]\n" % cmd_log_fn trace_msg(trace_txt + '\t' + cmd.strip()) # early exit in 'dry run' mode, after printing the command that would be run if build_option('extended_dry_run'): if path is None: path = cwd dry_run_msg(" running interactive command \"%s\"" % cmd, silent=build_option('silent')) dry_run_msg(" (in %s)" % path, silent=build_option('silent')) if simple: return True else: # output, exit code return ('', 0) try: if path: os.chdir(path) _log.debug("run_cmd_qa: running cmd %s (in %s)" % (cmd, os.getcwd())) except OSError as err: _log.warning("Failed to change to %s: %s" % (path, err)) _log.info("running cmd %s in non-existing directory, might fail!" % cmd) # Part 1: 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 def escape_special(string): return re.sub(r"([\+\?\(\)\[\]\*\.\\\$])", r"\\\1", string) split = '[\s\n]+' regSplit = re.compile(r"" + split) def process_QA(q, a_s): splitq = [escape_special(x) for x in regSplit.split(q)] regQtxt = split.join(splitq) + split.rstrip('+') + "*$" # add optional split at the end for i in [idx for idx, a in enumerate(a_s) if not a.endswith('\n')]: a_s[i] += '\n' regQ = re.compile(r"" + regQtxt) if regQ.search(q): return (a_s, regQ) else: raise EasyBuildError("runqanda: Question %s converted in %s does not match itself", q, regQtxt) def check_answers_list(answers): """Make sure we have a list of answers (as strings).""" if isinstance(answers, basestring): answers = [answers] elif not isinstance(answers, list): raise EasyBuildError("Invalid type for answer on %s, no string or list: %s (%s)", question, type(answers), answers) # list is manipulated when answering matching question, so return a copy return answers[:] new_qa = {} _log.debug("new_qa: ") for question, answers in qa.items(): answers = check_answers_list(answers) (answers, regQ) = process_QA(question, answers) new_qa[regQ] = answers _log.debug("new_qa[%s]: %s" % (regQ.pattern, new_qa[regQ])) new_std_qa = {} if std_qa: for question, answers in std_qa.items(): regQ = re.compile(r"" + question + r"[\s\n]*$") answers = check_answers_list(answers) for i in [idx for idx, a in enumerate(answers) if not a.endswith('\n')]: answers[i] += '\n' new_std_qa[regQ] = answers _log.debug("new_std_qa[%s]: %s" % (regQ.pattern, new_std_qa[regQ])) new_no_qa = [] if no_qa: # simple statements, can contain wildcards new_no_qa = [re.compile(r"" + x + r"[\s\n]*$") for x in no_qa] _log.debug("New noQandA list is: %s" % [x.pattern for x in new_no_qa]) # Part 2: Run the command and answer questions # - this needs asynchronous stdout # # Log command output if cmd_log: cmd_log.write("# output for interactive command: %s\n\n" % cmd) try: p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT, stdin=PIPE, close_fds=True, executable="/bin/bash") except OSError as err: raise EasyBuildError("run_cmd_qa init cmd %s failed:%s", cmd, err) ec = p.poll() stdout_err = '' old_len_out = -1 hit_count = 0 while ec is None: # need to read from time to time. # - otherwise the stdout/stderr buffer gets filled and it all stops working try: out = recv_some(p) if cmd_log: cmd_log.write(out) stdout_err += out # recv_some may throw Exception except (IOError, Exception) as err: _log.debug("run_cmd_qa cmd %s: read failed: %s", cmd, err) out = None hit = False for question, answers in new_qa.items(): res = question.search(stdout_err) if out and res: fa = answers[0] % res.groupdict() # cycle through list of answers last_answer = answers.pop(0) answers.append(last_answer) _log.debug("List of answers for question %s after cycling: %s", question.pattern, answers) _log.debug("run_cmd_qa answer %s question %s out %s", fa, question.pattern, stdout_err[-50:]) send_all(p, fa) hit = True break if not hit: for question, answers in new_std_qa.items(): res = question.search(stdout_err) if out and res: fa = answers[0] % res.groupdict() # cycle through list of answers last_answer = answers.pop(0) answers.append(last_answer) _log.debug("List of answers for question %s after cycling: %s", question.pattern, answers) _log.debug("run_cmd_qa answer %s std question %s out %s", fa, question.pattern, stdout_err[-50:]) send_all(p, fa) hit = True break if not hit: if len(stdout_err) > old_len_out: old_len_out = len(stdout_err) else: noqa = False for r in new_no_qa: if r.search(stdout_err): _log.debug("runqanda: noQandA found for out %s", stdout_err[-50:]) noqa = True if not noqa: hit_count += 1 else: hit_count = 0 else: hit_count = 0 if hit_count > maxhits: # explicitly kill the child process before exiting try: os.killpg(p.pid, signal.SIGKILL) os.kill(p.pid, signal.SIGKILL) except OSError as err: _log.debug("run_cmd_qa exception caught when killing child process: %s", err) _log.debug("run_cmd_qa: full stdouterr: %s", stdout_err) raise EasyBuildError("run_cmd_qa: cmd %s : Max nohits %s reached: end of output %s", cmd, maxhits, stdout_err[-500:]) # the sleep below is required to avoid exiting on unknown 'questions' too early (see above) time.sleep(1) ec = p.poll() # Process stopped. Read all remaining data try: if p.stdout: out = p.stdout.read() stdout_err += out if cmd_log: cmd_log.write(out) cmd_log.close() except IOError as err: _log.debug("runqanda cmd %s: remaining data read failed: %s", cmd, err) if trace: trace_msg("interactive command completed: exit %s, ran in %s" % (ec, time_str_since(start_time))) try: os.chdir(cwd) except OSError as err: raise EasyBuildError("Failed to return to %s after executing command: %s", cwd, err) return parse_cmd_output(cmd, stdout_err, ec, simple, log_all, log_ok, regexp)