]>
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 forward_stream(pipe
, level
):
40 for line
in iter(pipe
.readline
, b
''):
41 logging
.log(level
, line
.decode(errors
="replace").rstrip())
44 def background(args
, cmd
, working_dir
=None):
45 """ Run a subprocess in background and redirect its output to the log. """
46 proc
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
48 threading
.Thread(target
=forward_stream
, args
=(proc
.stdout
, logging
.DEBUG
), daemon
=True).start()
49 threading
.Thread(target
=forward_stream
, args
=(proc
.stderr
, logging
.DEBUG
), daemon
=True).start()
50 logging
.debug("New background process: pid={}, output=background".format(proc
.pid
))
54 def pipe(args
, cmd
, working_dir
=None):
55 """ Run a subprocess in background and redirect its output to a pipe. """
56 ret
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
58 threading
.Thread(target
=forward_stream
, args
=(proc
.stderr
, logging
.DEBUG
), daemon
=True).start()
59 logging
.verbose("New background process: pid={}, output=pipe".format(ret
.pid
))
63 def pipe_read(args
, process
, output_to_stdout
=False, output_return
=False,
64 output_return_buffer
=False):
66 Read all available output from a subprocess and copy it to the log and
67 optionally stdout and a buffer variable. This is only meant to be called by
68 foreground_pipe() below.
70 :param process: subprocess.Popen instance
71 :param output_to_stdout: copy all output to waydroid's stdout
72 :param output_return: when set to True, output_return_buffer will be
74 :param output_return_buffer: list of bytes that gets extended with the
75 current output in case output_return is True.
78 # Copy available output
79 out
= process
.stdout
.readline()
81 logging
.debug(out
.decode(errors
="replace").rstrip())
83 sys
.stdout
.buffer.write(out
)
85 output_return_buffer
.append(out
)
88 # No more output (flush buffers)
94 def kill_process_tree(args
, pid
, ppids
, sudo
):
96 Recursively kill a pid and its child processes
98 :param pid: process id that will be killed
99 :param ppids: list of process id and parent process id tuples (pid, ppid)
100 :param sudo: use sudo to kill the process
103 tools
.helpers
.run
.root(args
, ["kill", "-9", str(pid
)],
106 tools
.helpers
.run
.user(args
, ["kill", "-9", str(pid
)],
109 for (child_pid
, child_ppid
) in ppids
:
110 if child_ppid
== str(pid
):
111 kill_process_tree(args
, child_pid
, ppids
, sudo
)
114 def kill_command(args
, pid
, sudo
):
116 Kill a command process and recursively kill its child processes
118 :param pid: process id that will be killed
119 :param sudo: use sudo to kill the process
121 cmd
= ["ps", "-e", "-o", "pid,ppid"]
122 ret
= subprocess
.run(cmd
, check
=True, stdout
=subprocess
.PIPE
)
124 proc_entries
= ret
.stdout
.decode("utf-8").rstrip().split('\n')[1:]
125 for row
in proc_entries
:
128 raise RuntimeError("Unexpected ps output: " + row
)
131 kill_process_tree(args
, pid
, ppids
, sudo
)
134 def foreground_pipe(args
, cmd
, working_dir
=None, output_to_stdout
=False,
135 output_return
=False, output_timeout
=True,
138 Run a subprocess in foreground with redirected output and optionally kill
139 it after being silent for too long.
141 :param cmd: command as list, e.g. ["echo", "string with spaces"]
142 :param working_dir: path in host system where the command should run
143 :param output_to_stdout: copy all output to waydroid's stdout
144 :param output_return: return the output of the whole program
145 :param output_timeout: kill the process when it doesn't print any output
146 after a certain time (configured with --timeout)
147 and raise a RuntimeError exception
148 :param sudo: use sudo to kill the process when it hits the timeout
149 :returns: (code, output)
150 * code: return code of the program
152 * output: full program output string (output_return is True)
154 # Start process in background (stdout and stderr combined)
155 process
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
,
156 stderr
=subprocess
.STDOUT
, cwd
=working_dir
)
158 # Make process.stdout non-blocking
159 handle
= process
.stdout
.fileno()
160 flags
= fcntl
.fcntl(handle
, fcntl
.F_GETFL
)
161 fcntl
.fcntl(handle
, fcntl
.F_SETFL
, flags | os
.O_NONBLOCK
)
163 # While process exists wait for output (with timeout)
165 sel
= selectors
.DefaultSelector()
166 sel
.register(process
.stdout
, selectors
.EVENT_READ
)
167 timeout
= args
.timeout
if output_timeout
else None
168 while process
.poll() is None:
169 wait_start
= time
.perf_counter() if output_timeout
else None
172 # On timeout raise error (we need to measure time on our own, because
173 # select() may exit early even if there is no data to read and the
174 # timeout was not reached.)
176 wait_end
= time
.perf_counter()
177 if wait_end
- wait_start
>= args
.timeout
:
178 logging
.info("Process did not write any output for " +
179 str(args
.timeout
) + " seconds. Killing it.")
180 logging
.info("NOTE: The timeout can be increased with"
182 kill_command(args
, process
.pid
, sudo
)
185 # Read all currently available output
186 pipe_read(args
, process
, output_to_stdout
, output_return
,
189 # There may still be output after the process quit
190 pipe_read(args
, process
, output_to_stdout
, output_return
, output_buffer
)
192 # Return the return code and output (the output gets built as list of
193 # output chunks and combined at the end, this is faster than extending the
194 # combined string with each new chunk)
195 return (process
.returncode
, b
"".join(output_buffer
).decode("utf-8"))
198 def foreground_tui(cmd
, working_dir
=None):
200 Run a subprocess in foreground without redirecting any of its output.
202 This is the only way text-based user interfaces (ncurses programs like
203 vim, nano or the kernel's menuconfig) work properly.
206 logging
.debug("*** output passed to waydroid stdout, not to this log"
208 process
= subprocess
.Popen(cmd
, cwd
=working_dir
)
209 return process
.wait()
212 def check_return_code(args
, code
, log_message
):
214 Check the return code of a command.
216 :param code: exit code to check
217 :param log_message: simplified and more readable form of the command, e.g.
218 "(native) % echo test" instead of the full command with
219 entering the chroot and more escaping
220 :raises RuntimeError: when the code indicates that the command failed
224 logging
.debug("^" * 70)
225 logging
.info("NOTE: The failed command's output is above the ^^^ line"
226 " in the log file: " + args
.log
)
227 raise RuntimeError("Command failed: " + log_message
)
230 def sudo_timer_iterate():
232 Run sudo -v and schedule a new timer to repeat the same.
235 subprocess
.Popen(["sudo", "-v"]).wait()
237 timer
= threading
.Timer(interval
=60, function
=sudo_timer_iterate
)
242 def sudo_timer_start(args
):
244 Start a timer to call sudo -v periodically, so that the password is only
248 if "sudo_timer_active" in args
.cache
:
250 args
.cache
["sudo_timer_active"] = True
255 def core(args
, log_message
, cmd
, working_dir
=None, output
="log",
256 output_return
=False, check
=None, sudo
=False, disable_timeout
=False):
258 Run a command and create a log entry.
260 This is a low level function not meant to be used directly. Use one of the
261 following instead: tools.helpers.run.user(), tools.helpers.run.root(),
262 tools.chroot.user(), tools.chroot.root()
264 :param log_message: simplified and more readable form of the command, e.g.
265 "(native) % echo test" instead of the full command with
266 entering the chroot and more escaping
267 :param cmd: command as list, e.g. ["echo", "string with spaces"]
268 :param working_dir: path in host system where the command should run
269 :param output: where to write the output (stdout and stderr) of the
270 process. We almost always write to the log file, which can
271 be read with "waydroid log" (output values: "log",
272 "stdout", "interactive", "background"), so it's easy to
273 trace what waydroid does.
275 The exceptions are "tui" (text-based user interface), where
276 it does not make sense to write to the log file (think of
277 ncurses UIs, such as "menuconfig") and "pipe" where the
278 output is written to a pipe for manual asynchronous
279 consumption by the caller.
281 When the output is not set to "interactive", "tui",
282 "background" or "pipe", we kill the process if it does not
283 output anything for 5 minutes (time can be set with
284 "waydroid --timeout").
286 The table below shows all possible values along with
287 their properties. "wait" indicates that we wait for the
290 output value | timeout | out to log | out to stdout | wait
291 -----------------------------------------------------------
293 "stdout" | x | x | x | x
294 "interactive" | | x | x | x
296 "background" | | x | |
299 :param output_return: in addition to writing the program's output to the
300 destinations above in real time, write to a buffer
301 and return it as string when the command has
302 completed. This is not possible when output is
303 "background", "pipe" or "tui".
304 :param check: an exception will be raised when the command's return code
305 is not 0. Set this to False to disable the check. This
306 parameter can not be used when the output is "background" or
308 :param sudo: use sudo to kill the process when it hits the timeout.
309 :returns: * program's return code (default)
310 * subprocess.Popen instance (output is "background" or "pipe")
311 * the program's entire output (output_return is True)
313 sanity_checks(output
, output_return
, check
)
315 if args
.sudo_timer
and sudo
:
316 sudo_timer_start(args
)
318 # Log simplified and full command (waydroid -v)
319 logging
.debug(log_message
)
320 logging
.verbose("run: " + str(cmd
))
323 if output
== "background":
324 return background(args
, cmd
, working_dir
)
328 return pipe(args
, cmd
, working_dir
)
331 output_after_run
= ""
334 code
= foreground_tui(cmd
, working_dir
)
336 # Foreground pipe (always redirects to the error log file)
337 output_to_stdout
= False
338 if not args
.details_to_stdout
and output
in ["stdout", "interactive"]:
339 output_to_stdout
= True
341 output_timeout
= output
in ["log", "stdout"] and not disable_timeout
343 (code
, output_after_run
) = foreground_pipe(args
, cmd
, working_dir
,
349 # Check the return code
350 if check
is not False:
351 check_return_code(args
, code
, log_message
)
353 # Return (code or output string)
354 return output_after_run
if output_return
else code