def test_feeder(self): feeder = Feeder() p = capture_stdout([sys.executable, 'echoer.py'], input=feeder, async=True) try: lines = ('hello', 'goodbye') gen = iter(lines) # p.commands may not be set yet (separate thread) while not p.commands or p.commands[0].returncode is None: logger.debug('commands: %s', p.commands) try: data = next(gen) except StopIteration: break feeder.feed(data + '\n') if p.commands: p.commands[0].poll() time.sleep(0.05) # wait for child to return echo finally: # p.commands may not be set yet (separate thread) if p.commands: p.commands[0].terminate() feeder.close() self.assertEqual(p.stdout.text.splitlines(), ['hello hello', 'goodbye goodbye'])
def cli_cmd_sync(cmd, log_obj=None, write_dots=False, on_out=None, on_err=None, cwd=None): """ Runs command line task synchronously :return: """ feeder = Feeder() p = run(cmd, input=feeder, async=True, stdout=Capture(buffer_size=1), stderr=Capture(buffer_size=1), cwd=cwd) out_acc = [] err_acc = [] ret_code = 1 log = None close_log = False # Logging - either filename or logger itself if log_obj is not None: if isinstance(log_obj, types.StringTypes): delete_file_backup(log_obj, chmod=0o600) log = safe_open(log_obj, mode='w', chmod=0o600) close_log = True else: log = log_obj try: while len(p.commands) == 0: time.sleep(0.15) while p.commands[0].returncode is None: out = p.stdout.readline() err = p.stderr.readline() # If output - react on input challenges if out is not None and len(out) > 0: out_acc.append(out) if log is not None: log.write(out) log.flush() if write_dots: sys.stderr.write('.') if on_out is not None: on_out(out, feeder, p) # Collect error if err is not None and len(err) > 0: err_acc.append(err) if log is not None: log.write(err) log.flush() if write_dots: sys.stderr.write('.') if on_err is not None: on_err(err, feeder, p) p.commands[0].poll() time.sleep(0.01) ret_code = p.commands[0].returncode # Collect output to accumulator rest_out = p.stdout.readlines() if rest_out is not None and len(rest_out) > 0: for out in rest_out: out_acc.append(out) if log is not None: log.write(out) log.flush() if on_out is not None: on_out(out, feeder, p) # Collect error to accumulator rest_err = p.stderr.readlines() if rest_err is not None and len(rest_err) > 0: for err in rest_err: err_acc.append(err) if log is not None: log.write(err) log.flush() if on_err is not None: on_err(err, feeder, p) return ret_code, out_acc, err_acc finally: feeder.close() if close_log: log.close()
def cli_cmd_sync(cmd, log_obj=None, write_dots=False, on_out=None, on_err=None, cwd=None, shell=True, readlines=True, env=None, **kwargs): """ Runs command line task synchronously :return: return code, out_acc, err_acc """ from sarge import run, Capture, Feeder import time import sys feeder = Feeder() p = run(cmd, input=feeder, async=True, stdout=Capture(timeout=7, buffer_size=1), stderr=Capture(timeout=7, buffer_size=1), cwd=cwd, shell=shell, env=env, **kwargs) out_acc = [] err_acc = [] ret_code = 1 log = None close_log = False # Logging - either filename or logger itself if log_obj is not None: if isinstance(log_obj, basestring): delete_file_backup(log_obj, chmod=0o600) log = safe_open(log_obj, mode='w', chmod=0o600) close_log = True else: log = log_obj try: while len(p.commands) == 0: time.sleep(0.15) while p.commands[0].returncode is None: out, err = None, None if readlines: out = p.stdout.readline() err = p.stderr.readline() else: out = p.stdout.read(1) err = p.stdout.read(1) # If output - react on input challenges if out is not None and len(out) > 0: out_acc.append(out) if log is not None: log.write(out) log.flush() if write_dots: sys.stderr.write('.') if on_out is not None: on_out(out, feeder, p) # Collect error if err is not None and len(err) > 0: err_acc.append(err) if log is not None: log.write(err) log.flush() if write_dots: sys.stderr.write('.') if on_err is not None: on_err(err, feeder, p) p.commands[0].poll() time.sleep(0.01) ret_code = p.commands[0].returncode logger.debug('Command terminated with code: %s' % ret_code) # Collect output to accumulator rest_out = p.stdout.readlines() if rest_out is not None and len(rest_out) > 0: for out in rest_out: out_acc.append(out) if log is not None: log.write(out) log.flush() if on_out is not None: on_out(out, feeder, p) # Collect error to accumulator rest_err = p.stderr.readlines() if rest_err is not None and len(rest_err) > 0: for err in rest_err: err_acc.append(err) if log is not None: log.write(err) log.flush() if on_err is not None: on_err(err, feeder, p) return ret_code, out_acc, err_acc finally: feeder.close() if close_log: log.close()
def run_internal(self): def preexec_function(): os.setpgrp() cmd = self.cmd if self.shell: args_str = (" ".join(self.args) if isinstance( self.args, (list, tuple)) else self.args) if isinstance(cmd, (list, tuple)): cmd = " ".join(cmd) if args_str and len(args_str) > 0: cmd += " " + args_str else: if self.args and not isinstance(self.args, (list, tuple)): raise ValueError("!Shell requires array of args") if self.args: cmd += self.args self.using_stdout_cap = self.stdout is None self.using_stderr_cap = self.stderr is None self.feeder = Feeder() logger.debug("Starting command %s in %s" % (cmd, self.cwd)) run_args = {} if self.preexec_setgrp: run_args['preexec_fn'] = preexec_function p = run(cmd, input=self.feeder, async_=True, stdout=self.stdout or Capture(timeout=0.1, buffer_size=1), stderr=self.stderr or Capture(timeout=0.1, buffer_size=1), cwd=self.cwd, env=self.env, shell=self.shell, **run_args) self.time_start = time.time() self.proc = p self.ret_code = 1 self.out_acc, self.err_acc = [], [] out_cur, err_cur = [""], [""] def process_line(line, is_err=False): dst = self.err_acc if is_err else self.out_acc dst.append(line) if self.log_out_during: if self.no_log_just_write: dv = sys.stderr if is_err else sys.stdout dv.write(line + "\n") dv.flush() else: logger.debug("Out: %s" % line.strip()) if self.on_output: self.on_output(self, line, is_err) def add_output(buffers, is_err=False, finish=False): buffers = [ x.decode("utf8") for x in buffers if x is not None and x != "" ] lines = [""] if not buffers and not finish: return dst_cur = err_cur if is_err else out_cur for x in buffers: clines = [v.strip("\r") for v in x.split("\n")] lines[-1] += clines[0] lines.extend(clines[1:]) nlines = len(lines) dst_cur[0] += lines[0] if nlines > 1: process_line(dst_cur[0], is_err) dst_cur[0] = "" for line in lines[1:-1]: process_line(line, is_err) if not finish and nlines > 1: dst_cur[0] = lines[-1] or "" if finish: cline = dst_cur[0] if nlines == 1 else lines[-1] if cline: process_line(cline, is_err) try: while len(p.commands) == 0: time.sleep(0.15) logger.debug("Program started, progs: %s" % len(p.commands)) if p.commands[0] is None: self.is_running = False self.was_running = True logger.error("Program could not be started") return self.is_running = True self.on_change() out = None err = None while p.commands[0] and p.commands[0].returncode is None: if self.using_stdout_cap: out = p.stdout.read(-1, False) add_output([out], is_err=False) if self.using_stderr_cap: err = p.stderr.read(-1, False) add_output([err], is_err=True) if self.on_tick: self.on_tick(self) p.commands[0].poll() if self.terminating and p.commands[0].returncode is None: logger.debug("Terminating by sigint %s" % p.commands[0]) sarge_sigint(p.commands[0], signal.SIGTERM) sarge_sigint(p.commands[0], signal.SIGINT) logger.debug("Sigint sent") logger.debug("Process closed") # If there is data, consume it right away. if (self.using_stdout_cap and out) or (self.using_stderr_cap and err): continue time.sleep(0.15) logger.debug("Runner while ended") p.wait() self.ret_code = p.commands[0].returncode if p.commands[0] else -1 if self.using_stdout_cap: try_fnc(lambda: p.stdout.close()) add_output(self.drain_stream(p.stdout, True), finish=True) if self.using_stderr_cap: try_fnc(lambda: p.stderr.close()) add_output(self.drain_stream(p.stderr, True), is_err=True, finish=True) self.was_running = True self.is_running = False self.on_change() logger.debug("Program ended with code: %s" % self.ret_code) logger.debug("Command: %s" % cmd) if self.log_out_after: logger.debug("Std out: %s" % "\n".join(self.out_acc)) logger.debug("Error out: %s" % "\n".join(self.err_acc)) except Exception as e: self.is_running = False logger.error("Exception in async runner: %s" % (e, )) finally: self.was_running = True self.time_elapsed = time.time() - self.time_start try_fnc(lambda: self.feeder.close()) try_fnc(lambda: self.proc.close()) if self.on_finished: self.on_finished(self)
def run_internal(self): def preexec_function(): os.setpgrp() def preexec_setsid(): logger.debug("setsid called") os.setsid() cmd = self.cmd if self.shell: args_str = (" ".join(self.args) if isinstance( self.args, (list, tuple)) else self.args) if isinstance(cmd, (list, tuple)): cmd = " ".join(cmd) if args_str and len(args_str) > 0: cmd += " " + args_str else: if self.args and not isinstance(self.args, (list, tuple)): raise ValueError("!Shell requires array of args") if self.args: cmd += self.args self.using_stdout_cap = self.stdout is None self.using_stderr_cap = self.stderr is None self.feeder = Feeder() logger.debug("Starting command %s in %s" % (cmd, self.cwd)) run_args = {} if self.create_new_group: if self.is_win: self.win_create_process_group = True else: self.preexec_setsid = True if self.preexec_setgrp: run_args['preexec_fn'] = preexec_function if self.preexec_setsid: run_args['preexec_fn'] = preexec_setsid # https://stackoverflow.com/questions/44124338/trying-to-implement-signal-ctrl-c-event-in-python3-6 if self.win_create_process_group: run_args['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP p = run(cmd, input=self.feeder, async_=True, stdout=self.stdout or Capture(timeout=0.1, buffer_size=1), stderr=self.stderr or Capture(timeout=0.1, buffer_size=1), cwd=self.cwd, env=self.env, shell=self.shell, **run_args) self.p = p self.time_start = time.time() self.proc = p self.ret_code = 1 self.out_acc, self.err_acc = [], [] out_cur, err_cur = [""], [""] def process_line(line, is_err=False): dst = self.err_acc if is_err else self.out_acc dst.append(line) if self.log_out_during: if self.no_log_just_write: dv = sys.stderr if is_err else sys.stdout dv.write(line + "\n") dv.flush() else: logger.debug("Out: %s" % line.strip()) if self.on_output: self.on_output(self, line, is_err) def add_output(buffers, is_err=False, finish=False): buffers = [ x.decode("utf8") for x in buffers if x is not None and x != "" ] lines = [""] if not buffers and not finish: return dst_cur = err_cur if is_err else out_cur for x in buffers: clines = [v.strip("\r") for v in x.split("\n")] lines[-1] += clines[0] lines.extend(clines[1:]) nlines = len(lines) dst_cur[0] += lines[0] if nlines > 1: process_line(dst_cur[0], is_err) dst_cur[0] = "" for line in lines[1:-1]: process_line(line, is_err) if not finish and nlines > 1: dst_cur[0] = lines[-1] or "" if finish: cline = dst_cur[0] if nlines == 1 else lines[-1] if cline: process_line(cline, is_err) try: while len(p.commands) == 0: time.sleep(0.15) logger.debug("Program started, progs: %s, pid: %s" % (len(p.commands), self.get_pid())) if p.commands[0] is None: self.is_running = False self.was_running = True logger.error("Program could not be started") return self.is_running = True self.on_change() out = None err = None while p.commands[0] and p.commands[0].returncode is None: if self.using_stdout_cap: out = p.stdout.read(-1, False) add_output([out], is_err=False) if self.using_stderr_cap: err = p.stderr.read(-1, False) add_output([err], is_err=True) if self.on_tick: self.on_tick(self) p.commands[0].poll() if self.terminating and p.commands[0].returncode is None: self.send_term_signals() logger.debug("Process closed") # If there is data, consume it right away. if (self.using_stdout_cap and out) or (self.using_stderr_cap and err): continue time.sleep(0.15) try_fnc(lambda: p.commands[0].poll()) self.ret_code = p.commands[0].returncode if p.commands[0] else -1 logger.debug("Runner while-loop ended, retcode: %s" % (p.commands[0].returncode, )) if self.do_not_block_runner_thread_on_termination: logger.debug( "Not blocking runner thread on termination. Finishing, some output may be lost" ) self.was_running = True self.is_running = False return if self.force_runner_thread_termination: self.was_running = True self.is_running = False return logger.debug("Waiting for process to complete") p.wait() self.ret_code = p.commands[0].returncode if p.commands[0] else -1 if self.do_drain_streams and self.using_stdout_cap: logger.debug("Draining stdout stream") try_fnc(lambda: p.stdout.close()) add_output(self.drain_stream(p.stdout, True), finish=True) if self.do_drain_streams and self.using_stderr_cap: logger.debug("Draining stderr stream") try_fnc(lambda: p.stderr.close()) add_output(self.drain_stream(p.stderr, True), is_err=True, finish=True) self.was_running = True self.is_running = False self.on_change() logger.debug("Program ended with code: %s" % self.ret_code) logger.debug("Command: %s" % cmd) if self.log_out_after: logger.debug("Std out: %s" % "\n".join(self.out_acc)) logger.debug("Error out: %s" % "\n".join(self.err_acc)) except Exception as e: self.is_running = False logger.error("Exception in async runner: %s" % (e, )) finally: self.was_running = True self.time_elapsed = time.time() - self.time_start rtt_utils.try_fnc(lambda: self.feeder.close()) if not self.do_not_block_runner_thread_on_termination: rtt_utils.try_fnc(lambda: self.proc.close()) if self.on_finished: self.on_finished(self)
class AsyncRunner: def __init__(self, cmd, args=None, stdout=None, stderr=None, cwd=None, shell=True, env=None): self.cmd = cmd self.args = args self.on_finished = None self.on_output = None self.on_tick = None self.no_log_just_write = False self.log_out_during = True self.log_out_after = True self.stdout = stdout self.stderr = stderr self.cwd = cwd self.shell = shell self.env = env self.create_new_group = None self.preexec_setgrp = False self.preexec_setsid = False self.win_create_process_group = False self.using_stdout_cap = True self.using_stderr_cap = True self.do_drain_streams = True self.do_not_block_runner_thread_on_termination = False self.force_runner_thread_termination = False self.try_terminate_children_for_shell = False self.ret_code = None self.out_acc = [] self.err_acc = [] self.time_start = None self.time_elapsed = None self.feeder = None self.proc = None self.is_running = False self.was_running = False self.terminating = False self.thread = None self.p = None self.terminate_timeout = 0.5 self.signal_timeout = 0.5 self.terminate_ctrlc_timeout = 0.5 self.is_win = sys.platform.startswith('win') def run(self): try: self.run_internal() except Exception as e: self.is_running = False logger.error("Unexpected exception in runner: %s" % (e, ), exc_info=e) finally: self.was_running = True logger.debug("Runner thread finished") if self.force_runner_thread_termination: raise SystemError("Terminate runner") def __del__(self): self.deinit() def deinit(self): rtt_utils.try_fnc(lambda: self.feeder.close()) if not self.proc: return if self.do_not_block_runner_thread_on_termination or self.force_runner_thread_termination: return if self.using_stdout_cap: rtt_utils.try_fnc(lambda: self.proc.stdout.close()) if self.using_stderr_cap: rtt_utils.try_fnc(lambda: self.proc.stderr.close()) rtt_utils.try_fnc(lambda: self.proc.close()) def drain_stream(self, s, block=False, timeout=0.15): ret = [] while True: rs = s.read(-1, block, timeout) if not rs: break ret.append(rs) return ret def run_internal(self): def preexec_function(): os.setpgrp() def preexec_setsid(): logger.debug("setsid called") os.setsid() cmd = self.cmd if self.shell: args_str = (" ".join(self.args) if isinstance( self.args, (list, tuple)) else self.args) if isinstance(cmd, (list, tuple)): cmd = " ".join(cmd) if args_str and len(args_str) > 0: cmd += " " + args_str else: if self.args and not isinstance(self.args, (list, tuple)): raise ValueError("!Shell requires array of args") if self.args: cmd += self.args self.using_stdout_cap = self.stdout is None self.using_stderr_cap = self.stderr is None self.feeder = Feeder() logger.debug("Starting command %s in %s" % (cmd, self.cwd)) run_args = {} if self.create_new_group: if self.is_win: self.win_create_process_group = True else: self.preexec_setsid = True if self.preexec_setgrp: run_args['preexec_fn'] = preexec_function if self.preexec_setsid: run_args['preexec_fn'] = preexec_setsid # https://stackoverflow.com/questions/44124338/trying-to-implement-signal-ctrl-c-event-in-python3-6 if self.win_create_process_group: run_args['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP p = run(cmd, input=self.feeder, async_=True, stdout=self.stdout or Capture(timeout=0.1, buffer_size=1), stderr=self.stderr or Capture(timeout=0.1, buffer_size=1), cwd=self.cwd, env=self.env, shell=self.shell, **run_args) self.p = p self.time_start = time.time() self.proc = p self.ret_code = 1 self.out_acc, self.err_acc = [], [] out_cur, err_cur = [""], [""] def process_line(line, is_err=False): dst = self.err_acc if is_err else self.out_acc dst.append(line) if self.log_out_during: if self.no_log_just_write: dv = sys.stderr if is_err else sys.stdout dv.write(line + "\n") dv.flush() else: logger.debug("Out: %s" % line.strip()) if self.on_output: self.on_output(self, line, is_err) def add_output(buffers, is_err=False, finish=False): buffers = [ x.decode("utf8") for x in buffers if x is not None and x != "" ] lines = [""] if not buffers and not finish: return dst_cur = err_cur if is_err else out_cur for x in buffers: clines = [v.strip("\r") for v in x.split("\n")] lines[-1] += clines[0] lines.extend(clines[1:]) nlines = len(lines) dst_cur[0] += lines[0] if nlines > 1: process_line(dst_cur[0], is_err) dst_cur[0] = "" for line in lines[1:-1]: process_line(line, is_err) if not finish and nlines > 1: dst_cur[0] = lines[-1] or "" if finish: cline = dst_cur[0] if nlines == 1 else lines[-1] if cline: process_line(cline, is_err) try: while len(p.commands) == 0: time.sleep(0.15) logger.debug("Program started, progs: %s, pid: %s" % (len(p.commands), self.get_pid())) if p.commands[0] is None: self.is_running = False self.was_running = True logger.error("Program could not be started") return self.is_running = True self.on_change() out = None err = None while p.commands[0] and p.commands[0].returncode is None: if self.using_stdout_cap: out = p.stdout.read(-1, False) add_output([out], is_err=False) if self.using_stderr_cap: err = p.stderr.read(-1, False) add_output([err], is_err=True) if self.on_tick: self.on_tick(self) p.commands[0].poll() if self.terminating and p.commands[0].returncode is None: self.send_term_signals() logger.debug("Process closed") # If there is data, consume it right away. if (self.using_stdout_cap and out) or (self.using_stderr_cap and err): continue time.sleep(0.15) try_fnc(lambda: p.commands[0].poll()) self.ret_code = p.commands[0].returncode if p.commands[0] else -1 logger.debug("Runner while-loop ended, retcode: %s" % (p.commands[0].returncode, )) if self.do_not_block_runner_thread_on_termination: logger.debug( "Not blocking runner thread on termination. Finishing, some output may be lost" ) self.was_running = True self.is_running = False return if self.force_runner_thread_termination: self.was_running = True self.is_running = False return logger.debug("Waiting for process to complete") p.wait() self.ret_code = p.commands[0].returncode if p.commands[0] else -1 if self.do_drain_streams and self.using_stdout_cap: logger.debug("Draining stdout stream") try_fnc(lambda: p.stdout.close()) add_output(self.drain_stream(p.stdout, True), finish=True) if self.do_drain_streams and self.using_stderr_cap: logger.debug("Draining stderr stream") try_fnc(lambda: p.stderr.close()) add_output(self.drain_stream(p.stderr, True), is_err=True, finish=True) self.was_running = True self.is_running = False self.on_change() logger.debug("Program ended with code: %s" % self.ret_code) logger.debug("Command: %s" % cmd) if self.log_out_after: logger.debug("Std out: %s" % "\n".join(self.out_acc)) logger.debug("Error out: %s" % "\n".join(self.err_acc)) except Exception as e: self.is_running = False logger.error("Exception in async runner: %s" % (e, )) finally: self.was_running = True self.time_elapsed = time.time() - self.time_start rtt_utils.try_fnc(lambda: self.feeder.close()) if not self.do_not_block_runner_thread_on_termination: rtt_utils.try_fnc(lambda: self.proc.close()) if self.on_finished: self.on_finished(self) def test_is_running(self): try_fnc(lambda: self.p.commands[0].poll()) return self.is_running and self.p.commands[ 0] and self.p.commands[0].returncode is None def sleep_if_running(self, tm): stime = time.time() while time.time() - stime < tm: if not self.test_is_running(): return False time.sleep(0.2) return True def send_term_signals(self): p = self.p pid = self.get_pid() logger.debug("Terminating by sigint %s, PID: %s" % (p.commands[0], pid)) test_is_running = self.test_is_running sleep_if_running = self.sleep_if_running if not test_is_running(): return # PGid works only on POSIX if self.preexec_setsid and pid is not None: pgid = os.getpgid(pid) logger.debug("Terminating process group %s for process %s" % (pgid, pid)) logger.debug("Sending pg SIGINT") try_fnc(lambda: os.killpg(pgid, signal.SIGINT)) sleep_if_running(self.terminate_ctrlc_timeout) if not test_is_running(): return logger.debug("Sending pg SIGTERM") try_fnc(lambda: os.killpg(pgid, signal.SIGTERM)) sleep_if_running(self.terminate_timeout) if not test_is_running(): return logger.debug("Sending pg SIGKILL") try_fnc(lambda: os.killpg(pgid, signal.SIGKILL)) sleep_if_running(self.signal_timeout) if not test_is_running(): return if self.is_win: cmd = "tasklist /fi \"pid eq %s\"" % pid logger.debug("Retrieving process info on the process %s" % (pid, )) subprocess.run(cmd, shell=True) time.sleep(self.signal_timeout) if self.is_win: if self.shell and self.try_terminate_children_for_shell: logger.debug( "Experimental: sending CTRL+C to children. " "May cause interruption of all processes running in the console" ) self._win_terminate_children(pid) # Windows - process has to be process leader, otherwise this sends signal to everyone # Thus do this only if win && is process group leader if self.win_create_process_group: logger.debug("Trying to invoke CTRL+C (win) in process group") try_fnc(lambda: os.kill(pid, signal.CTRL_C_EVENT)) try_fnc(lambda: p.commands[0].process.send_signal( signal.CTRL_C_EVENT)) sleep_if_running(self.terminate_ctrlc_timeout) cmd = "Taskkill /PID %s /F /T" % pid logger.debug("Closing process with taskkill: %s" % (cmd, )) subprocess.run(cmd, shell=True) time.sleep(self.terminate_timeout) logger.debug("Sending win SIGTERM") try_fnc(lambda: os.kill(pid, signal.SIGTERM)) sleep_if_running(self.terminate_timeout) logger.debug("Sending win SIGKILL") try_fnc(lambda: os.kill(pid, signal.SIGKILL)) sleep_if_running(self.terminate_timeout) try_fnc(lambda: p.commands[0].terminate()) time.sleep(self.signal_timeout) try_fnc(lambda: p.commands[0].kill()) time.sleep(self.signal_timeout) return # Posix process termination logger.debug("Sending SIGINT") try_fnc(lambda: sarge_sigint(p.commands[0], signal.SIGINT)) sleep_if_running(self.terminate_ctrlc_timeout) logger.debug("Sending SIGTERM") try_fnc(lambda: p.commands[0].terminate()) sleep_if_running(self.terminate_timeout) if not test_is_running(): return logger.debug("Sending SIGHUP") try_fnc(lambda: sarge_sigint(p.commands[0], signal.SIGHUP)) sleep_if_running(self.signal_timeout) if not test_is_running(): return logger.debug("Sending SIGTERM") try_fnc(lambda: sarge_sigint(p.commands[0], signal.SIGTERM)) sleep_if_running(self.signal_timeout) if not test_is_running(): return logger.debug("Sending SIGKILL") try_fnc(lambda: p.commands[0].kill()) try_fnc(lambda: sarge_sigint(p.commands[0], signal.SIGKILL)) def _win_get_children(self, pid): cmd = "wmic process where (ParentProcessId=%s) get ProcessId" % pid logger.debug("Obtaining child processes for %s: %s" % ( pid, cmd, )) r = subprocess.run(cmd, shell=True, check=True, text=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) lines = [x.strip() for x in r.stdout.splitlines()[1:] if x.strip()] return [z for z in [try_fnc(lambda: int(y)) for y in lines] if z] def _win_terminate_children(self, pid): """ Tries to send CTRL+C signal to the child process - useful when command is executed with shell=True. On Windows, CTRL+C signal is not transmitted to the child process from cmd.exe (apparently). This solution does not work with `create_new_group`, from some reason, sending CTRL+C event does not work to new sessions - or at least we did not observe CTRL+C event in our java process. On the other hand - calling CTRL+C to children process in this method also causes interrupt event in the main python code (caller of the shutdown()). From this reason all sleeps has to be guarded with KeyboardInterrupt checking. This indicates we cannot just send CTRL+C to a child process selectively but that it is broadcasted to the whole session. -> kills all other running tasks by sending them CTRL+C Useful explanation: - send_signal(CTRL_C_EVENT) does not work because CTRL_C_EVENT is only for os.kill. [REF1] - os.kill(CTRL_C_EVENT) sends the signal to all processes running in the current cmd window [REF2] - Popen(..., creationflags=CREATE_NEW_PROCESS_GROUP) does not work because CTRL_C_EVENT is ignored for process groups [REF2]. This is a bug in the python documentation [REF3]. [REF1]: http://docs.python.org/library/signal.html#signal.CTRL_C_EVENT [REF2]: http://msdn.microsoft.com/en-us/library/windows/desktop/ms683155%28v=vs.85%29.aspx [REF3]: http://docs.python.org/library/subprocess.html#subprocess.Popen.send_signal Proposed workaround: - Let your program run in a different cmd window with the Windows shell command start. - Add a CTRL-C request wrapper between your control application and the application which should get the CTRL-C signal. The wrapper will run in the same cmd window as the application which should get the CTRL-C signal. - The wrapper will shutdown itself and the program which should get the CTRL-C signal by sending all processes in the cmd window the CTRL_C_EVENT. - The control program should be able to request the wrapper to send the CTRL-C signal. This might be implemented trough IPC means, e.g. sockets. Overall, it is quite pain to make implement graceful shutdown of child processes by sending CTRL+C signal on Windows. We explored several combinations of settings, none of which enabled sending targeted CTRL+C signal. We resorted to calling taskkill with killing all child processes (/T). Without this we were not able to terminate child process (when shell=True) is used, wrappers were hanging on sarge process closing, processes stayed after python finished, console was uninterruptable and so on. src: - https://stackoverflow.com/questions/7085604/sending-c-to-python-subprocess-objects-on-windows - https://stackoverflow.com/questions/44124338/trying-to-implement-signal-ctrl-c-event-in-python3-6/44128151 """ try: children = self._win_get_children(pid) logger.debug("Children processes of %s: %s" % (pid, children)) if len(children) == 0: return for cpid in children: logger.debug( "Trying to invoke CTRL+C (win) for %s (parent %s)" % (cpid, pid)) try: try_fnc(lambda: os.kill(cpid, signal.CTRL_C_EVENT)) time.sleep(0.1) except KeyboardInterrupt: logger.debug("Keyboard interrupt _win_terminate_children") self.sleep_if_running(self.terminate_ctrlc_timeout) for cpid in children: logger.debug("Sending win SIGTERM for %s (parent %s)" % (cpid, pid)) try_fnc(lambda: os.kill(cpid, signal.SIGTERM)) self.sleep_if_running(self.terminate_timeout) except Exception as e: logger.debug("Child process termination failed: %s" % (e, )) def on_change(self): pass def get_pid(self): try: return self.p.commands[0].process.pid except: return None def wait(self, timeout=None, require_ok=False): tstart = time.time() while self.is_running: if timeout is not None and time.time() - tstart > timeout: raise Exception("Timeout") try: time.sleep(0.1) except KeyboardInterrupt: logger.debug("Keyboard interrupt wait()") if require_ok and self.ret_code != 0: raise Exception("Return code is not zero: %s" % self.ret_code) def shutdown(self, timeout=None): if not self.is_running: return try: self.terminating = True time.sleep(1) except KeyboardInterrupt: logger.debug("Shutdown keyboard interrupt") # Terminating with sigint logger.debug("Waiting for program to terminate...") tstart = time.time() while self.is_running: if timeout is not None and time.time() - tstart > timeout: raise Exception("Timeout") try: time.sleep(0.1) except KeyboardInterrupt: logger.debug("Shutdown Keyboard interrupt loop") logger.debug("Program terminated") self.deinit() def start(self, wait_running=True, timeout=None): install_sarge_filter() self.thread = threading.Thread(target=self.run, args=()) self.thread.setDaemon(False) self.terminating = False self.is_running = False self.thread.start() if not wait_running: self.is_running = True return tstart = time.time() while not self.is_running and not self.was_running: if timeout is not None and time.time() - tstart > timeout: raise Exception("Timeout") time.sleep(0.1) return self
import re from sarge import Capture, Feeder, run f = Feeder() c = Capture(buffer_size=1) p = run('python login_test.py', async_=True, stdout=c, input=f) c.expect('Username:'******'input username') f.feed('user\n') c.expect('Password:'******'input password') f.feed('pass\n') VERIFICATION_CODE_REGEX = re.compile(rb'Input verification code \((\d{4})\): ') match = c.expect(VERIFICATION_CODE_REGEX) print('input verification code', match.group(1)) f.feed(match.group(1) + b'\n') c.expect('>>>', timeout=5) f.feed('print(1 + 1)\n') f.feed('exit()\n') p.wait() print('final output:\n', b''.join(c.readlines()).decode('utf-8'))