← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad-buildd/scan-for-processes-python into lp:launchpad-buildd

 

Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/scan-for-processes-python into lp:launchpad-buildd with lp:~cjwatson/launchpad-buildd/start-stop-python as a prerequisite.

Commit message:
Rewrite scan-for-processes in Python, allowing it to have unit tests.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/scan-for-processes-python/+merge/328573
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/scan-for-processes-python into lp:launchpad-buildd.
=== modified file 'bin/scan-for-processes'
--- bin/scan-for-processes	2017-07-25 22:11:19 +0000
+++ bin/scan-for-processes	2017-08-04 13:10:42 +0000
@@ -1,43 +1,32 @@
-#!/bin/bash
+#! /usr/bin/python -u
 #
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-# Buildd Slave tool to scan a chroot in case any processes are underneath it
-
-## This script uses bashisms, must be run under bash
-
-# Expects build id as arg 1, makes build-id to contain the build
-
-# Needs SUDO to be set to a sudo instance for passwordless access
-
-SUDO=/usr/bin/sudo
-BUILDID="$1"
-REALHOME=$(cd $HOME && pwd -P)
-
-set -e
-
-exec 2>&1
-
-[ $(id -u) = "0" ] || exec $SUDO $0 "$REALHOME/build-$BUILDID/chroot-autobuild"
-
-echo "Scanning for processes to kill in build $BUILDID..."
-
-PREFIX="$BUILDID"
-FOUND=0
-
-for ROOT in /proc/*/root; do
-    LINK=$(readlink $ROOT || true)
-    if [ "x$LINK" != "x" ]; then
-	if [ "x${LINK:0:${#PREFIX}}" = "x$PREFIX" ]; then
-	    # this process is in the chroot...
-	    PID=$(basename $(dirname "$ROOT"))
-	    kill -9 "$PID" || true
-	    FOUND=1
-	fi
-    fi
-done
-
-if [ "x$FOUND" = "x1" ]; then
-  exec $0 $1
-fi
+"""Kill any processes running in a chroot."""
+
+from __future__ import print_function
+
+__metaclass__ = type
+
+import os
+import sys
+
+from lpbuildd.target.kill_processes import KillProcesses
+
+
+def main():
+    # This whole script must run as root, since we want to iterate over
+    # other users' processes in Python.
+    if os.geteuid() != 0:
+        cmd = ["sudo"]
+        if "PYTHONPATH" in os.environ:
+            cmd.append("PYTHONPATH=%s" % os.environ["PYTHONPATH"])
+        cmd.append("--")
+        cmd.extend(sys.argv)
+        os.execv("/usr/bin/sudo", cmd)
+    return KillProcesses().run()
+
+
+if __name__ == "__main__":
+    sys.exit(main())

=== modified file 'debian/changelog'
--- debian/changelog	2017-08-04 13:10:42 +0000
+++ debian/changelog	2017-08-04 13:10:42 +0000
@@ -19,6 +19,7 @@
     unit tests.
   * Rewrite mount-chroot and umount-chroot in Python, allowing them to have
     unit tests.
+  * Rewrite scan-for-processes in Python, allowing it to have unit tests.
 
  -- Colin Watson <cjwatson@xxxxxxxxxx>  Tue, 25 Jul 2017 23:07:58 +0100
 

=== modified file 'lpbuildd/slave.py'
--- lpbuildd/slave.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/slave.py	2017-08-04 13:10:42 +0000
@@ -181,9 +181,8 @@
                 iterate = partial(self.iterateReap, state)
             else:
                 iterate = lambda success: None
-            self.runSubProcess(
-                self._scanpath, ["scan-for-processes", self._buildid],
-                iterate=iterate)
+            self.runTargetSubProcess(
+                self._scanpath, ["scan-for-processes"], iterate=iterate)
 
     def doCleanup(self):
         """Remove the build tree etc."""

=== modified file 'lpbuildd/target/backend.py'
--- lpbuildd/target/backend.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/target/backend.py	2017-08-04 13:10:42 +0000
@@ -69,6 +69,14 @@
         """
         raise NotImplementedError
 
+    def kill_processes(self):
+        """Kill any processes left running in the target.
+
+        This is allowed to do nothing if stopping the target will reliably
+        kill all processes running in it.
+        """
+        pass
+
     def stop(self):
         """Stop the backend."""
         raise NotImplementedError

=== modified file 'lpbuildd/target/chroot.py'
--- lpbuildd/target/chroot.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/target/chroot.py	2017-08-04 13:10:42 +0000
@@ -6,6 +6,7 @@
 __metaclass__ = type
 
 import os.path
+import signal
 import stat
 import subprocess
 import time
@@ -81,6 +82,26 @@
             ["sudo", "install", "-o", "root", "-g", "root", "-m", "%o" % mode,
              source_path, full_target_path])
 
+    def kill_processes(self):
+        """See `Backend`."""
+        prefix = os.path.realpath(self.chroot_path)
+        while True:
+            found = False
+            pids = [int(pid) for pid in os.listdir("/proc") if pid.isdigit()]
+            for pid in sorted(pids):
+                try:
+                    link = os.readlink(os.path.join("/proc", str(pid), "root"))
+                except OSError:
+                    continue
+                if link and (link == prefix or link.startswith(prefix + "/")):
+                    try:
+                        os.kill(pid, signal.SIGKILL)
+                    except OSError:
+                        pass
+                    found = True
+            if not found:
+                break
+
     def _get_chroot_mounts(self):
         with open("/proc/mounts") as mounts_file:
             for line in mounts_file:

=== added file 'lpbuildd/target/kill_processes.py'
--- lpbuildd/target/kill_processes.py	1970-01-01 00:00:00 +0000
+++ lpbuildd/target/kill_processes.py	2017-08-04 13:10:42 +0000
@@ -0,0 +1,23 @@
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from __future__ import print_function
+
+__metaclass__ = type
+
+import logging
+
+from lpbuildd.target.operation import Operation
+
+
+logger = logging.getLogger(__name__)
+
+
+class KillProcesses(Operation):
+
+    description = "Kill any processes in the target."
+
+    def run(self):
+        logger.info(
+            "Scanning for processes to kill in build %s", self.args.build_id)
+        self.backend.kill_processes()

=== modified file 'lpbuildd/target/tests/test_chroot.py'
--- lpbuildd/target/tests/test_chroot.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/target/tests/test_chroot.py	2017-08-04 13:10:42 +0000
@@ -4,6 +4,7 @@
 __metaclass__ = type
 
 import os.path
+import signal
 from textwrap import dedent
 import time
 
@@ -17,10 +18,14 @@
     FakeTime,
     )
 from testtools import TestCase
+from testtools.matchers import DirContains
 
 from lpbuildd.target.backend import BackendException
 from lpbuildd.target.chroot import Chroot
-from lpbuildd.target.tests.testfixtures import SudoUmount
+from lpbuildd.target.tests.testfixtures import (
+    KillFixture,
+    SudoUmount,
+    )
 
 
 class TestChroot(TestCase):
@@ -114,6 +119,37 @@
             expected_args,
             [proc._args["args"] for proc in processes_fixture.procs])
 
+    def test_kill_processes(self):
+        self.useFixture(EnvironmentVariable("HOME", "/expected/home"))
+        fs_fixture = self.useFixture(FakeFilesystem())
+        fs_fixture.add("/expected")
+        os.makedirs("/expected/home/build-1/chroot-autobuild")
+        fs_fixture.add("/proc")
+        os.mkdir("/proc")
+        os.mkdir("/proc/1")
+        os.symlink("/", "/proc/1/root")
+        os.mkdir("/proc/10")
+        os.symlink("/expected/home/build-1/chroot-autobuild", "/proc/10/root")
+        os.mkdir("/proc/11")
+        os.symlink("/expected/home/build-1/chroot-autobuild", "/proc/11/root")
+        os.mkdir("/proc/12")
+        os.symlink(
+            "/expected/home/build-1/chroot-autobuild/submount",
+            "/proc/12/root")
+        os.mkdir("/proc/13")
+        os.symlink(
+            "/expected/home/build-1/chroot-autobuildsomething",
+            "/proc/13/root")
+        with open("/proc/version", "w"):
+            pass
+        kill_fixture = self.useFixture(KillFixture(delays={10: 1}))
+        Chroot("1", "xenial", "amd64").kill_processes()
+
+        self.assertEqual(
+            [(pid, signal.SIGKILL) for pid in (11, 12, 10)],
+            kill_fixture.kills)
+        self.assertThat("/proc", DirContains(["1", "13", "version"]))
+
     def _make_initial_proc_mounts(self):
         fs_fixture = self.useFixture(FakeFilesystem())
         fs_fixture.add("/proc")

=== added file 'lpbuildd/target/tests/test_kill_processes.py'
--- lpbuildd/target/tests/test_kill_processes.py	1970-01-01 00:00:00 +0000
+++ lpbuildd/target/tests/test_kill_processes.py	2017-08-04 13:10:42 +0000
@@ -0,0 +1,34 @@
+# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import os.path
+import signal
+
+from fixtures import EnvironmentVariable
+from systemfixtures import FakeFilesystem
+from testtools import TestCase
+from testtools.matchers import DirContains
+
+from lpbuildd.target.kill_processes import KillProcesses
+from lpbuildd.target.tests.testfixtures import KillFixture
+
+
+class TestKillProcesses(TestCase):
+
+    def test_succeeds(self):
+        self.useFixture(EnvironmentVariable("HOME", "/expected/home"))
+        fs_fixture = self.useFixture(FakeFilesystem())
+        fs_fixture.add("/expected")
+        os.makedirs("/expected/home/build-1/chroot-autobuild")
+        fs_fixture.add("/proc")
+        os.mkdir("/proc")
+        os.mkdir("/proc/10")
+        os.symlink("/expected/home/build-1/chroot-autobuild", "/proc/10/root")
+        kill_fixture = self.useFixture(KillFixture())
+        args = ["--backend=chroot", "--series=xenial", "--arch=amd64", "1"]
+        KillProcesses(args=args).run()
+
+        self.assertEqual([(10, signal.SIGKILL)], kill_fixture.kills)
+        self.assertThat("/proc", DirContains([]))

=== modified file 'lpbuildd/target/tests/testfixtures.py'
--- lpbuildd/target/tests/testfixtures.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/target/tests/testfixtures.py	2017-08-04 13:10:42 +0000
@@ -4,6 +4,9 @@
 __metaclass__ = type
 
 import argparse
+import shutil
+
+from fixtures import MonkeyPatch
 
 
 class SudoUmount:
@@ -36,3 +39,32 @@
                 for mount in mounts:
                     mounts_file.write(mount)
             return {}
+
+
+class Kill:
+    """A substitute for `os.kill` that may fail sometimes.
+
+    This must run with a fake `/proc` (e.g. using
+    `systemfixtures.FakeFilesystem`).
+    """
+
+    def __init__(self, delays=None):
+        self.delays = delays or {}
+        self.kills = []
+
+    def __call__(self, pid, sig):
+        if self.delays.get(pid, 0) > 0:
+            self.delays[pid] -= 1
+            raise OSError
+        self.kills.append((pid, sig))
+        shutil.rmtree("/proc/%d" % pid)
+
+
+class KillFixture(MonkeyPatch):
+
+    def __init__(self, delays=None):
+        super(KillFixture, self).__init__("os.kill", Kill(delays=delays))
+
+    @property
+    def kills(self):
+        return self.new_value.kills

=== modified file 'lpbuildd/tests/test_binarypackage.py'
--- lpbuildd/tests/test_binarypackage.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/tests/test_binarypackage.py	2017-08-04 13:10:42 +0000
@@ -152,6 +152,7 @@
         self.assertState(
             BinaryPackageBuildState.SBUILD,
             ['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+             '--backend=chroot', '--series=warty', '--arch=i386',
              self.buildid], final=False)
 
     def assertUnmountsSanely(self):
@@ -252,6 +253,7 @@
         self.assertState(
             BinaryPackageBuildState.SBUILD,
             ['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+             '--backend=chroot', '--series=warty', '--arch=i386',
              self.buildid], final=False)
         self.assertFalse(self.slave.wasCalled('buildFail'))
 
@@ -271,6 +273,7 @@
         self.assertState(
             BinaryPackageBuildState.SBUILD,
             ['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+             '--backend=chroot', '--series=warty', '--arch=i386',
              self.buildid], final=False)
         self.assertFalse(self.slave.wasCalled('builderFail'))
         reap_subprocess = self.buildmanager._subprocess
@@ -310,6 +313,7 @@
         self.assertState(
             BinaryPackageBuildState.INIT,
             ['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+             '--backend=chroot', '--series=warty', '--arch=i386',
              self.buildid], final=False)
 
         self.buildmanager.iterate(0)

=== modified file 'lpbuildd/tests/test_debian.py'
--- lpbuildd/tests/test_debian.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/tests/test_debian.py	2017-08-04 13:10:42 +0000
@@ -161,6 +161,7 @@
         self.assertEqual(MockBuildState.MAIN, self.getState())
         self.assertEqual(
             (['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+              '--backend=chroot', '--series=xenial', '--arch=amd64',
               self.buildid],
              None),
             self.buildmanager.commands[-1])
@@ -280,6 +281,7 @@
         self.assertEqual(MockBuildState.MAIN, self.getState())
         self.assertEqual(
             (['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+              '--backend=chroot', '--series=xenial', '--arch=amd64',
               self.buildid],
              None),
             self.buildmanager.commands[-1])

=== modified file 'lpbuildd/tests/test_livefs.py'
--- lpbuildd/tests/test_livefs.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/tests/test_livefs.py	2017-08-04 13:10:42 +0000
@@ -100,6 +100,7 @@
         self.buildmanager.iterate(0)
         expected_command = [
             "sharepath/slavebin/scan-for-processes", "scan-for-processes",
+            "--backend=chroot", "--series=saucy", "--arch=i386",
             self.buildid,
             ]
         self.assertEqual(

=== modified file 'lpbuildd/tests/test_snap.py'
--- lpbuildd/tests/test_snap.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/tests/test_snap.py	2017-08-04 13:10:42 +0000
@@ -112,6 +112,7 @@
         self.buildmanager.iterate(0)
         expected_command = [
             "sharepath/slavebin/scan-for-processes", "scan-for-processes",
+            "--backend=chroot", "--series=xenial", "--arch=i386",
             self.buildid,
             ]
         self.assertEqual(SnapBuildState.BUILD_SNAP, self.getState())
@@ -157,6 +158,7 @@
         self.buildmanager.iterate(0)
         expected_command = [
             "sharepath/slavebin/scan-for-processes", "scan-for-processes",
+            "--backend=chroot", "--series=xenial", "--arch=i386",
             self.buildid,
             ]
         self.assertEqual(SnapBuildState.BUILD_SNAP, self.getState())

=== modified file 'lpbuildd/tests/test_sourcepackagerecipe.py'
--- lpbuildd/tests/test_sourcepackagerecipe.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/tests/test_sourcepackagerecipe.py	2017-08-04 13:10:42 +0000
@@ -126,6 +126,7 @@
         self.buildmanager.iterate(0)
         expected_command = [
             'sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+            '--backend=chroot', '--series=maverick', '--arch=i386',
             self.buildid,
             ]
         self.assertEqual(
@@ -167,6 +168,7 @@
         self.buildmanager.iterate(RETCODE_FAILURE_INSTALL_BUILD_DEPS)
         expected_command = [
             'sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+            '--backend=chroot', '--series=maverick', '--arch=i386',
             self.buildid,
             ]
         self.assertEqual(
@@ -205,6 +207,7 @@
         self.buildmanager.iterate(RETCODE_FAILURE_INSTALL_BUILD_DEPS)
         expected_command = [
             'sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+            '--backend=chroot', '--series=maverick', '--arch=i386',
             self.buildid,
             ]
         self.assertEqual(

=== modified file 'lpbuildd/tests/test_translationtemplatesbuildmanager.py'
--- lpbuildd/tests/test_translationtemplatesbuildmanager.py	2017-08-04 13:10:42 +0000
+++ lpbuildd/tests/test_translationtemplatesbuildmanager.py	2017-08-04 13:10:42 +0000
@@ -105,6 +105,7 @@
         self.buildmanager.iterate(0)
         expected_command = [
             'sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+            '--backend=chroot', '--series=xenial', '--arch=i386',
             self.buildid,
             ]
         self.assertEqual(
@@ -170,6 +171,7 @@
         self.buildmanager.iterate(-1)
         expected_command = [
             'sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+            '--backend=chroot', '--series=xenial', '--arch=i386',
             self.buildid,
             ]
         self.assertEqual(