3 """Helper methods concerned with executing subprocesses."""
12 from typing import List, Optional
14 logger = logging.getLogger(__file__)
17 def cmd_showing_output(
20 """Kick off a child process. Capture and print all output that it
21 produces on stdout and stderr. Wait for the subprocess to exit
22 and return the exit value as the return code of this function.
25 line_enders = set([b'\n', b'\r'])
26 sel = selectors.DefaultSelector()
27 with subprocess.Popen(
31 stdout=subprocess.PIPE,
32 stderr=subprocess.PIPE,
33 universal_newlines=False,
35 sel.register(p.stdout, selectors.EVENT_READ) # type: ignore
36 sel.register(p.stderr, selectors.EVENT_READ) # type: ignore
39 for key, _ in sel.select():
40 char = key.fileobj.read(1) # type: ignore
42 sel.unregister(key.fileobj)
43 if len(sel.get_map()) == 0:
48 if key.fileobj is p.stdout:
49 # sys.stdout.buffer.write(char)
50 os.write(sys.stdout.fileno(), char)
51 if char in line_enders:
54 # sys.stderr.buffer.write(char)
55 os.write(sys.stderr.fileno(), char)
56 if char in line_enders:
62 def cmd_with_timeout(command: str, timeout_seconds: Optional[float]) -> int:
63 """Run a command but do not let it run for more than timeout seconds.
64 Doesn't capture or rebroadcast command output. Function returns
65 the exit value of the command or raises a TimeoutExpired exception
66 if the deadline is exceeded.
68 >>> cmd_with_timeout('/bin/echo foo', 10.0)
71 >>> cmd_with_timeout('/bin/sleep 2', 0.1)
72 Traceback (most recent call last):
74 subprocess.TimeoutExpired: Command '['/bin/bash', '-c', '/bin/sleep 2']' timed out after 0.1 seconds
77 return subprocess.check_call(["/bin/bash", "-c", command], timeout=timeout_seconds)
80 def cmd(command: str, timeout_seconds: Optional[float] = None) -> str:
81 """Run a command and capture its output to stdout (only) in a string.
82 Return that string as this function's output. Raises
83 subprocess.CalledProcessError or TimeoutExpired on error.
85 >>> cmd('/bin/echo foo')[:-1]
88 >>> cmd('/bin/sleep 2', 0.1)
89 Traceback (most recent call last):
91 subprocess.TimeoutExpired: Command '/bin/sleep 2' timed out after 0.1 seconds
99 timeout=timeout_seconds,
101 return ret.decode("utf-8")
104 def run_silently(command: str, timeout_seconds: Optional[float] = None) -> None:
105 """Run a command silently but raise subprocess.CalledProcessError if
108 >>> run_silently("/usr/bin/true")
110 >>> run_silently("/usr/bin/false")
111 Traceback (most recent call last):
113 subprocess.CalledProcessError: Command '/usr/bin/false' returned non-zero exit status 1.
119 stderr=subprocess.DEVNULL,
120 stdout=subprocess.DEVNULL,
121 capture_output=False,
123 timeout=timeout_seconds,
127 def cmd_in_background(command: str, *, silent: bool = False) -> subprocess.Popen:
128 args = shlex.split(command)
130 subproc = subprocess.Popen(
132 stdin=subprocess.DEVNULL,
133 stdout=subprocess.DEVNULL,
134 stderr=subprocess.DEVNULL,
137 subproc = subprocess.Popen(args, stdin=subprocess.DEVNULL)
139 def kill_subproc() -> None:
141 if subproc.poll() is None:
142 logger.info('At exit handler: killing %s (%s)', subproc, command)
144 subproc.wait(timeout=10.0)
145 except BaseException as be:
148 atexit.register(kill_subproc)
152 def cmd_list(command: List[str]) -> str:
153 """Run a command with args encapsulated in a list and return the
154 output text as a string. Raises subprocess.CalledProcessError.
156 ret = subprocess.run(command, capture_output=True, check=True).stdout
157 return ret.decode("utf-8")
160 if __name__ == '__main__':