From a01b500b91deacf2328ee60ab515b9885678c7c4 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Wed, 26 May 2010 20:58:40 +0200 Subject: [PATCH] utils: Add function to check whether process handles a signal This will be used to avoid a race condition between starting a program (dd for import/export) and sending signals to it. Signed-off-by: Michael Hanselmann <hansmi@google.com> Reviewed-by: Guido Trotter <ultrotter@google.com> --- lib/utils.py | 99 ++++++++++++++++++++++++++++++++++- test/ganeti.utils_unittest.py | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 2 deletions(-) diff --git a/lib/utils.py b/lib/utils.py index 11f207722..216a9eaa9 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -844,6 +844,17 @@ def ForceDictType(target, key_types, allowed_values=None): raise errors.TypeEnforcementError(msg) +def _GetProcStatusPath(pid): + """Returns the path for a PID's proc status file. + + @type pid: int + @param pid: Process ID + @rtype: string + + """ + return "/proc/%d/status" % pid + + def IsProcessAlive(pid): """Check if a given pid exists on the system. @@ -870,15 +881,99 @@ def IsProcessAlive(pid): if pid <= 0: return False - proc_entry = "/proc/%d/status" % pid # /proc in a multiprocessor environment can have strange behaviors. # Retry the os.stat a few times until we get a good result. try: - return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5, args=[proc_entry]) + return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5, + args=[_GetProcStatusPath(pid)]) except RetryTimeout, err: err.RaiseInner() +def _ParseSigsetT(sigset): + """Parse a rendered sigset_t value. + + This is the opposite of the Linux kernel's fs/proc/array.c:render_sigset_t + function. + + @type sigset: string + @param sigset: Rendered signal set from /proc/$pid/status + @rtype: set + @return: Set of all enabled signal numbers + + """ + result = set() + + signum = 0 + for ch in reversed(sigset): + chv = int(ch, 16) + + # The following could be done in a loop, but it's easier to read and + # understand in the unrolled form + if chv & 1: + result.add(signum + 1) + if chv & 2: + result.add(signum + 2) + if chv & 4: + result.add(signum + 3) + if chv & 8: + result.add(signum + 4) + + signum += 4 + + return result + + +def _GetProcStatusField(pstatus, field): + """Retrieves a field from the contents of a proc status file. + + @type pstatus: string + @param pstatus: Contents of /proc/$pid/status + @type field: string + @param field: Name of field whose value should be returned + @rtype: string + + """ + for line in pstatus.splitlines(): + parts = line.split(":", 1) + + if len(parts) < 2 or parts[0] != field: + continue + + return parts[1].strip() + + return None + + +def IsProcessHandlingSignal(pid, signum, status_path=None): + """Checks whether a process is handling a signal. + + @type pid: int + @param pid: Process ID + @type signum: int + @param signum: Signal number + @rtype: bool + + """ + if status_path is None: + status_path = _GetProcStatusPath(pid) + + try: + proc_status = ReadFile(status_path) + except EnvironmentError, err: + # In at least one case, reading /proc/$pid/status failed with ESRCH. + if err.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL, errno.ESRCH): + return False + raise + + sigcgt = _GetProcStatusField(proc_status, "SigCgt") + if sigcgt is None: + raise RuntimeError("%s is missing 'SigCgt' field" % status_path) + + # Now check whether signal is handled + return signum in _ParseSigsetT(sigcgt) + + def ReadPidFile(pidfile): """Read a pid from a file. diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py index dee83b0d8..2c964d57c 100755 --- a/test/ganeti.utils_unittest.py +++ b/test/ganeti.utils_unittest.py @@ -79,6 +79,104 @@ class TestIsProcessAlive(unittest.TestCase): "nonexisting process detected") +class TestGetProcStatusPath(unittest.TestCase): + def test(self): + self.assert_("/1234/" in utils._GetProcStatusPath(1234)) + self.assertNotEqual(utils._GetProcStatusPath(1), + utils._GetProcStatusPath(2)) + + +class TestIsProcessHandlingSignal(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def testParseSigsetT(self): + self.assertEqual(len(utils._ParseSigsetT("0")), 0) + self.assertEqual(utils._ParseSigsetT("1"), set([1])) + self.assertEqual(utils._ParseSigsetT("1000a"), set([2, 4, 17])) + self.assertEqual(utils._ParseSigsetT("810002"), set([2, 17, 24, ])) + self.assertEqual(utils._ParseSigsetT("0000000180000202"), + set([2, 10, 32, 33])) + self.assertEqual(utils._ParseSigsetT("0000000180000002"), + set([2, 32, 33])) + self.assertEqual(utils._ParseSigsetT("0000000188000002"), + set([2, 28, 32, 33])) + self.assertEqual(utils._ParseSigsetT("000000004b813efb"), + set([1, 2, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 17, + 24, 25, 26, 28, 31])) + self.assertEqual(utils._ParseSigsetT("ffffff"), set(range(1, 25))) + + def testGetProcStatusField(self): + for field in ["SigCgt", "Name", "FDSize"]: + for value in ["", "0", "cat", " 1234 KB"]: + pstatus = "\n".join([ + "VmPeak: 999 kB", + "%s: %s" % (field, value), + "TracerPid: 0", + ]) + result = utils._GetProcStatusField(pstatus, field) + self.assertEqual(result, value.strip()) + + def test(self): + sp = utils.PathJoin(self.tmpdir, "status") + + utils.WriteFile(sp, data="\n".join([ + "Name: bash", + "State: S (sleeping)", + "SleepAVG: 98%", + "Pid: 22250", + "PPid: 10858", + "TracerPid: 0", + "SigBlk: 0000000000010000", + "SigIgn: 0000000000384004", + "SigCgt: 000000004b813efb", + "CapEff: 0000000000000000", + ])) + + self.assert_(utils.IsProcessHandlingSignal(1234, 10, status_path=sp)) + + def testNoSigCgt(self): + sp = utils.PathJoin(self.tmpdir, "status") + + utils.WriteFile(sp, data="\n".join([ + "Name: bash", + ])) + + self.assertRaises(RuntimeError, utils.IsProcessHandlingSignal, + 1234, 10, status_path=sp) + + def testNoSuchFile(self): + sp = utils.PathJoin(self.tmpdir, "notexist") + + self.assertFalse(utils.IsProcessHandlingSignal(1234, 10, status_path=sp)) + + @staticmethod + def _TestRealProcess(): + signal.signal(signal.SIGUSR1, signal.SIG_DFL) + if utils.IsProcessHandlingSignal(os.getpid(), signal.SIGUSR1): + raise Exception("SIGUSR1 is handled when it should not be") + + signal.signal(signal.SIGUSR1, lambda signum, frame: None) + if not utils.IsProcessHandlingSignal(os.getpid(), signal.SIGUSR1): + raise Exception("SIGUSR1 is not handled when it should be") + + signal.signal(signal.SIGUSR1, signal.SIG_IGN) + if utils.IsProcessHandlingSignal(os.getpid(), signal.SIGUSR1): + raise Exception("SIGUSR1 is not handled when it should be") + + signal.signal(signal.SIGUSR1, signal.SIG_DFL) + if utils.IsProcessHandlingSignal(os.getpid(), signal.SIGUSR1): + raise Exception("SIGUSR1 is handled when it should not be") + + return True + + def testRealProcess(self): + self.assert_(utils.RunInSeparateProcess(self._TestRealProcess)) + + class TestPidFileFunctions(unittest.TestCase): """Tests for WritePidFile, RemovePidFile and ReadPidFile""" -- GitLab