]>
glassweightruler.freedombox.rocks Git - waydroid.git/blob - tools/helpers/run_core.py
1 # Copyright 2021 Oliver Smith
2 # SPDX-License-Identifier: GPL-3.0-or-later
11 import tools
.helpers
.run
13 """ For a detailed description of all output modes, read the description of
14 core() at the bottom. All other functions in this file get (indirectly)
18 def sanity_checks(output
="log", output_return
=False, check
=None):
20 Raise an exception if the parameters passed to core() don't make sense
21 (all parameters are described in core() below).
23 vals
= ["log", "stdout", "interactive", "tui", "background", "pipe"]
24 if output
not in vals
:
25 raise RuntimeError("Invalid output value: " + str(output
))
27 # Prevent setting the check parameter with output="background".
28 # The exit code won't be checked when running in background, so it would
29 # always by check=False. But we prevent it from getting set to check=False
30 # as well, so it does not look like you could change it to check=True.
31 if check
is not None and output
== "background":
32 raise RuntimeError("Can't use check with output: background")
34 if output_return
and output
in ["tui", "background"]:
35 raise RuntimeError("Can't use output_return with output: " + output
)
38 def background(args
, cmd
, working_dir
=None):
39 """ Run a subprocess in background and redirect its output to the log. """
40 ret
= subprocess
.Popen(cmd
, stdout
=args
.logfd
, stderr
=args
.logfd
,
42 logging
.debug("New background process: pid={}, output=background".format(ret
.pid
))
46 def pipe(args
, cmd
, working_dir
=None):
47 """ Run a subprocess in background and redirect its output to a pipe. """
48 ret
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=args
.logfd
,
50 logging
.verbose("New background process: pid={}, output=pipe".format(ret
.pid
))
54 def pipe_read(args
, process
, output_to_stdout
=False, output_return
=False,
55 output_return_buffer
=False):
57 Read all available output from a subprocess and copy it to the log and
58 optionally stdout and a buffer variable. This is only meant to be called by
59 foreground_pipe() below.
61 :param process: subprocess.Popen instance
62 :param output_to_stdout: copy all output to waydroid's stdout
63 :param output_return: when set to True, output_return_buffer will be
65 :param output_return_buffer: list of bytes that gets extended with the
66 current output in case output_return is True.
69 # Copy available output
70 out
= process
.stdout
.readline()
72 args
.logfd
.buffer.write(out
)
74 sys
.stdout
.buffer.write(out
)
76 output_return_buffer
.append(out
)
79 # No more output (flush buffers)
86 def kill_process_tree(args
, pid
, ppids
, sudo
):
88 Recursively kill a pid and its child processes
90 :param pid: process id that will be killed
91 :param ppids: list of process id and parent process id tuples (pid, ppid)
92 :param sudo: use sudo to kill the process
95 tools
.helpers
.run
.root(args
, ["kill", "-9", str(pid
)],
98 tools
.helpers
.run
.user(args
, ["kill", "-9", str(pid
)],
101 for (child_pid
, child_ppid
) in ppids
:
102 if child_ppid
== str(pid
):
103 kill_process_tree(args
, child_pid
, ppids
, sudo
)
106 def kill_command(args
, pid
, sudo
):
108 Kill a command process and recursively kill its child processes
110 :param pid: process id that will be killed
111 :param sudo: use sudo to kill the process
113 cmd
= ["ps", "-e", "-o", "pid,ppid"]
114 ret
= subprocess
.run(cmd
, check
=True, stdout
=subprocess
.PIPE
)
116 proc_entries
= ret
.stdout
.decode("utf-8").rstrip().split('\n')[1:]
117 for row
in proc_entries
:
120 raise RuntimeError("Unexpected ps output: " + row
)
123 kill_process_tree(args
, pid
, ppids
, sudo
)
126 def foreground_pipe(args
, cmd
, working_dir
=None, output_to_stdout
=False,
127 output_return
=False, output_timeout
=True,
130 Run a subprocess in foreground with redirected output and optionally kill
131 it after being silent for too long.
133 :param cmd: command as list, e.g. ["echo", "string with spaces"]
134 :param working_dir: path in host system where the command should run
135 :param output_to_stdout: copy all output to waydroid's stdout
136 :param output_return: return the output of the whole program
137 :param output_timeout: kill the process when it doesn't print any output
138 after a certain time (configured with --timeout)
139 and raise a RuntimeError exception
140 :param sudo: use sudo to kill the process when it hits the timeout
141 :returns: (code, output)
142 * code: return code of the program
144 * output: full program output string (output_return is True)
146 # Start process in background (stdout and stderr combined)
147 process
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
,
148 stderr
=subprocess
.STDOUT
, cwd
=working_dir
)
150 # Make process.stdout non-blocking
151 handle
= process
.stdout
.fileno()
152 flags
= fcntl
.fcntl(handle
, fcntl
.F_GETFL
)
153 fcntl
.fcntl(handle
, fcntl
.F_SETFL
, flags | os
.O_NONBLOCK
)
155 # While process exists wait for output (with timeout)
157 sel
= selectors
.DefaultSelector()
158 sel
.register(process
.stdout
, selectors
.EVENT_READ
)
159 timeout
= args
.timeout
if output_timeout
else None
160 while process
.poll() is None:
161 wait_start
= time
.perf_counter() if output_timeout
else None
164 # On timeout raise error (we need to measure time on our own, because
165 # select() may exit early even if there is no data to read and the
166 # timeout was not reached.)
168 wait_end
= time
.perf_counter()
169 if wait_end
- wait_start
>= args
.timeout
:
170 logging
.info("Process did not write any output for " +
171 str(args
.timeout
) + " seconds. Killing it.")
172 logging
.info("NOTE: The timeout can be increased with"
174 kill_command(args
, process
.pid
, sudo
)
177 # Read all currently available output
178 pipe_read(args
, process
, output_to_stdout
, output_return
,
181 # There may still be output after the process quit
182 pipe_read(args
, process
, output_to_stdout
, output_return
, output_buffer
)
184 # Return the return code and output (the output gets built as list of
185 # output chunks and combined at the end, this is faster than extending the
186 # combined string with each new chunk)
187 return (process
.returncode
, b
"".join(output_buffer
).decode("utf-8"))
190 def foreground_tui(cmd
, working_dir
=None):
192 Run a subprocess in foreground without redirecting any of its output.
194 This is the only way text-based user interfaces (ncurses programs like
195 vim, nano or the kernel's menuconfig) work properly.
198 logging
.debug("*** output passed to waydroid stdout, not to this log"
200 process
= subprocess
.Popen(cmd
, cwd
=working_dir
)
201 return process
.wait()
204 def check_return_code(args
, code
, log_message
):
206 Check the return code of a command.
208 :param code: exit code to check
209 :param log_message: simplified and more readable form of the command, e.g.
210 "(native) % echo test" instead of the full command with
211 entering the chroot and more escaping
212 :raises RuntimeError: when the code indicates that the command failed
216 logging
.debug("^" * 70)
217 logging
.info("NOTE: The failed command's output is above the ^^^ line"
218 " in the log file: " + args
.log
)
219 raise RuntimeError("Command failed: " + log_message
)
222 def sudo_timer_iterate():
224 Run sudo -v and schedule a new timer to repeat the same.
227 subprocess
.Popen(["sudo", "-v"]).wait()
229 timer
= threading
.Timer(interval
=60, function
=sudo_timer_iterate
)
234 def sudo_timer_start(args
):
236 Start a timer to call sudo -v periodically, so that the password is only
240 if "sudo_timer_active" in args
.cache
:
242 args
.cache
["sudo_timer_active"] = True
247 def core(args
, log_message
, cmd
, working_dir
=None, output
="log",
248 output_return
=False, check
=None, sudo
=False, disable_timeout
=False):
250 Run a command and create a log entry.
252 This is a low level function not meant to be used directly. Use one of the
253 following instead: tools.helpers.run.user(), tools.helpers.run.root(),
254 tools.chroot.user(), tools.chroot.root()
256 :param log_message: simplified and more readable form of the command, e.g.
257 "(native) % echo test" instead of the full command with
258 entering the chroot and more escaping
259 :param cmd: command as list, e.g. ["echo", "string with spaces"]
260 :param working_dir: path in host system where the command should run
261 :param output: where to write the output (stdout and stderr) of the
262 process. We almost always write to the log file, which can
263 be read with "waydroid log" (output values: "log",
264 "stdout", "interactive", "background"), so it's easy to
265 trace what waydroid does.
267 The exceptions are "tui" (text-based user interface), where
268 it does not make sense to write to the log file (think of
269 ncurses UIs, such as "menuconfig") and "pipe" where the
270 output is written to a pipe for manual asynchronous
271 consumption by the caller.
273 When the output is not set to "interactive", "tui",
274 "background" or "pipe", we kill the process if it does not
275 output anything for 5 minutes (time can be set with
276 "waydroid --timeout").
278 The table below shows all possible values along with
279 their properties. "wait" indicates that we wait for the
282 output value | timeout | out to log | out to stdout | wait
283 -----------------------------------------------------------
285 "stdout" | x | x | x | x
286 "interactive" | | x | x | x
288 "background" | | x | |
291 :param output_return: in addition to writing the program's output to the
292 destinations above in real time, write to a buffer
293 and return it as string when the command has
294 completed. This is not possible when output is
295 "background", "pipe" or "tui".
296 :param check: an exception will be raised when the command's return code
297 is not 0. Set this to False to disable the check. This
298 parameter can not be used when the output is "background" or
300 :param sudo: use sudo to kill the process when it hits the timeout.
301 :returns: * program's return code (default)
302 * subprocess.Popen instance (output is "background" or "pipe")
303 * the program's entire output (output_return is True)
305 sanity_checks(output
, output_return
, check
)
307 if args
.sudo_timer
and sudo
:
308 sudo_timer_start(args
)
310 # Log simplified and full command (waydroid -v)
311 logging
.debug(log_message
)
312 logging
.verbose("run: " + str(cmd
))
315 if output
== "background":
316 return background(args
, cmd
, working_dir
)
320 return pipe(args
, cmd
, working_dir
)
323 output_after_run
= ""
326 code
= foreground_tui(cmd
, working_dir
)
328 # Foreground pipe (always redirects to the error log file)
329 output_to_stdout
= False
330 if not args
.details_to_stdout
and output
in ["stdout", "interactive"]:
331 output_to_stdout
= True
333 output_timeout
= output
in ["log", "stdout"] and not disable_timeout
335 (code
, output_after_run
) = foreground_pipe(args
, cmd
, working_dir
,
341 # Check the return code
342 if check
is not False:
343 check_return_code(args
, code
, log_message
)
345 # Return (code or output string)
346 return output_after_run
if output_return
else code