diff --git a/test/dbus/lvmdbustest.py b/test/dbus/lvmdbustest.py index c4e95b7cc..5c352313d 100755 --- a/test/dbus/lvmdbustest.py +++ b/test/dbus/lvmdbustest.py @@ -9,23 +9,28 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import signal # noinspection PyUnresolvedReferences +import subprocess +import unittest +from glob import glob +from subprocess import Popen, PIPE + import dbus +import pyudev # noinspection PyUnresolvedReferences from dbus.mainloop.glib import DBusGMainLoop -import unittest -import pyudev -from testlib import * + import testlib -from subprocess import Popen, PIPE -from glob import glob -import os +from testlib import * g_tmo = 0 # Approx. min size VDO_MIN_SIZE = mib(8192) +EXE_NAME="/lvmdbusd" + # Prefix on created objects to enable easier clean-up g_prefix = os.getenv('PREFIX', '') @@ -210,6 +215,171 @@ def supports_vdo(): return True +def process_exists(name): + # Walk the process table looking for executable 'name' + for p in [pid for pid in os.listdir('/proc') if pid.isdigit()]: + try: + cmdline_args = read_file_split_nuls("/proc/%s/cmdline" % p) + except OSError: + continue + for arg in cmdline_args: + if name in arg: + return int(p) + return None + + +def read_file_split_nuls(fn): + with open(fn, "rb") as fh: + return [p.decode("utf-8") for p in fh.read().split(b'\x00') if len(p) > 0] + + +def read_file_build_hash(fn): + rc = dict() + lines = read_file_split_nuls(fn) + for line in lines: + if line.count("=") == 1: + k, v = line.split("=") + rc[k] = v + return rc + + +class DaemonInfo(object): + def __init__(self, pid): + # The daemon is running, we have a pid, lets see how it's being run. + # When running under systemd, fd 0 -> /dev/null, fd 1&2 -> socket + # when ran manually it may have output re-directed to a file etc. + # we need the following + # command line arguments + # cwd + # where the output is going (in case it's directed to a file) + # Which lvm binary is being used (check LVM_BINARY env. variable) + # PYTHONPATH + base = "/proc/%d" % pid + self.cwd = os.readlink("%s/cwd" % base) + self.cmdline = read_file_split_nuls("%s/cmdline" % (base))[1:] + self.env = read_file_build_hash("%s/environ" % base) + self.stdin = os.readlink("%s/fd/0" % base) + self.stdout = os.readlink("%s/fd/1" % base) + self.stderr = os.readlink("%s/fd/2" % base) + + if self.cwd == "/" and self.stdin == "/dev/null": + self.systemd = True + else: + self.systemd = False + + self.process = None + + @classmethod + def get(cls): + pid = process_exists(EXE_NAME) + if pid: + return cls(pid) + return None + + def start(self, expect_fail=False): + if self.systemd: + pass + else: + stdin_stream = None + stdout_stream = None + stderr_stream = None + try: + stdout_stream = open(self.stdout, "ab") + stdin_stream = open(self.stdin, "rb") + stderr_stream = open(self.stderr, "ab") + + self.process = Popen(self.cmdline, cwd=self.cwd, stdin=stdin_stream, + stdout=stdout_stream, stderr=stderr_stream, env=self.env) + + if expect_fail: + # Let's wait a bit to see if this process dies as expected and return the exit code + try: + self.process.wait(10) + return self.process.returncode + except subprocess.TimeoutExpired as e: + # Process did not fail as expected, lets kill it + os.kill(self.process.pid, signal.SIGKILL) + self.process.wait(20) + raise e + else: + # This is a hack to set the returncode. When the Popen object goes out of scope during the unit test + # the __del__ method gets called. As we leave the daemon running the process.returncode + # hasn't been set, so it incorrectly raises an exception that the process is still running + # which in our case is correct and expected. + self.process.returncode = 0 + finally: + # Close these in the parent + if stdin_stream: + stdin_stream.close() + if stderr_stream: + stderr_stream.close() + if stdout_stream: + stdout_stream.close() + + # Make sure daemon is responding to dbus events before returning + DaemonInfo._ensure_daemon("Daemon is not responding on dbus within 20 seconds of starting!") + + # During local testing it usually takes ~0.25 seconds for daemon to be ready + return None + + @staticmethod + def _ensure_no_daemon(): + start = time.time() + pid = process_exists(EXE_NAME) + while pid is not None and (time.time() - start) <= 20: + time.sleep(0.3) + pid = process_exists(EXE_NAME) + + if pid: + raise Exception( + "lsmd daemon did not exit within 20 seconds, pid = %s" % pid) + + @staticmethod + def _ensure_daemon(msg): + start = time.time() + running = False + while True and (time.time() - start) < 20: + try: + get_objects() + running = True + break + except dbus.exceptions.DBusException: + time.sleep(0.2) + pass + if not running: + raise RuntimeError(msg) + + def term_signal(self, sig_number): + # Used for signals that we expect with terminate the daemon, eg. SIGINT, SIGKILL + if self.process: + os.kill(self.process.pid, sig_number) + # Note: The following should work, but doesn't! + # self.process.send_signal(sig_number) + try: + self.process.wait(10) + except subprocess.TimeoutExpired: + std_err_print("Daemon hasn't exited within 10 seconds") + if self.process.poll() is None: + std_err_print("Daemon still running...") + else: + self.process = None + else: + pid = process_exists(EXE_NAME) + os.kill(pid, sig_number) + + # Make sure there is no daemon present before we return for things to be "good" + DaemonInfo._ensure_no_daemon() + + def non_term_signal(self, sig_number): + if sig_number not in [signal.SIGUSR1, signal.SIGUSR2]: + raise ValueError("Incorrect signal number! %d" % sig_number) + if self.process: + os.kill(self.process.pid, sig_number) + else: + pid = process_exists(EXE_NAME) + os.kill(pid, sig_number) + + # noinspection PyUnresolvedReferences class TestDbusService(unittest.TestCase): def setUp(self):