diff --git a/README.md b/README.md index b32abb8c..d2063d39 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Package build scripts live in the [`pmaports`](https://gitlab.com/postmarketOS/p * Linux distribution on the host system (`x86`, `x86_64`, or `aarch64`) * [Windows subsystem for Linux (WSL)](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux) does **not** work! Please use [VirtualBox](https://www.virtualbox.org/) instead. * Kernels based on the grsec patchset [do **not** work](https://github.com/postmarketOS/pmbootstrap/issues/107) *(Alpine: use linux-vanilla instead of linux-hardened, Arch: linux-hardened [is not based on grsec](https://www.reddit.com/r/archlinux/comments/68b2jn/linuxhardened_in_community_repo_a_grsecurity/))* - * On Alpine Linux only: `apk add coreutils` + * On Alpine Linux only: `apk add coreutils procps` * [Linux kernel 3.17 or higher](https://postmarketos.org/oldkernel) * Python 3.4+ * OpenSSL diff --git a/pmb/helpers/run_core.py b/pmb/helpers/run_core.py index 9b97ac6e..8dd27326 100644 --- a/pmb/helpers/run_core.py +++ b/pmb/helpers/run_core.py @@ -93,6 +93,46 @@ def pipe_read(args, process, output_to_stdout=False, output_return=False, return +def kill_process_tree(args, pid, ppids, kill_as_root): + """ + Recursively kill a pid and its child processes + + :param pid: process id that will be killed + :param ppids: list of process id and parent process id tuples (pid, ppid) + :param kill_as_root: use sudo to kill the process + """ + if kill_as_root: + pmb.helpers.run.root(args, ["kill", "-9", str(pid)], + check=False) + else: + pmb.helpers.run.user(args, ["kill", "-9", str(pid)], + check=False) + + for (child_pid, child_ppid) in ppids: + if child_ppid == str(pid): + kill_process_tree(args, child_pid, ppids, kill_as_root) + + +def kill_command(args, pid, kill_as_root): + """ + Kill a command process and recursively kill its child processes + + :param pid: process id that will be killed + :param kill_as_root: use sudo to kill the process + """ + cmd = ["ps", "-e", "-o", "pid=,ppid=", "--noheaders"] + ret = subprocess.run(cmd, check=True, stdout=subprocess.PIPE) + ppids = [] + proc_entries = ret.stdout.decode("utf-8").rstrip().split('\n') + for row in proc_entries: + items = row.split() + if len(items) != 2: + raise RuntimeError("Unexpected ps output: " + row) + ppids.append(items) + + kill_process_tree(args, pid, ppids, kill_as_root) + + def foreground_pipe(args, cmd, working_dir=None, output_to_stdout=False, output_return=False, output_timeout=True, kill_as_root=False): @@ -141,11 +181,7 @@ def foreground_pipe(args, cmd, working_dir=None, output_to_stdout=False, str(args.timeout) + " seconds. Killing it.") logging.info("NOTE: The timeout can be increased with" " 'pmbootstrap -t'.") - if kill_as_root: - pmb.helpers.run.root(args, ["kill", "-9", - str(process.pid)]) - else: - process.kill() + kill_command(args, process.pid, kill_as_root) continue # Read all currently available output diff --git a/test/test_run_core.py b/test/test_run_core.py index a3b0b16c..18c99a1b 100644 --- a/test/test_run_core.py +++ b/test/test_run_core.py @@ -23,6 +23,7 @@ This file tests functions from pmb.helpers.run_core import os import sys +import subprocess import pytest # Import from parent directory @@ -108,6 +109,29 @@ def test_foreground_pipe(args): ret = func(args, cmd, output_return=True, output_timeout=True) assert ret == (0, "first\nsecond\nthird\nfourth\n") + # Check if all child processes are killed after timeout. + # The first command uses ps to get its process group id (pgid) and echo it + # to stdout. All of the test commmands will be running under that pgid. + cmd = ["sudo", "sh", "-c", + "pgid=$(ps -p ${1:-$$} -o pgid=);echo $pgid | tr -d '\n';" + + "sleep 10 | sleep 20 | sleep 30"] + args.timeout = 0.3 + ret = func(args, cmd, output_return=True, output_timeout=True, + kill_as_root=True) + pgid = str(ret[1]) + + cmd = ["ps", "-e", "-o", "pgid=,comm=", "--noheaders"] + ret = subprocess.run(cmd, check=True, stdout=subprocess.PIPE) + procs = str(ret.stdout.decode("utf-8")).rstrip().split('\n') + child_procs = [] + for process in procs: + items = process.split(maxsplit=1) + if len(items) != 2: + continue + if pgid == items[0] and "sleep" in items[1]: + child_procs.append(items) + assert len(child_procs) == 0 + def test_foreground_tui(): func = pmb.helpers.run_core.foreground_tui