diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/bin/master.py | 90 | ||||
| -rw-r--r-- | src/mailman/bin/tests/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/bin/tests/test_master.py | 81 | ||||
| -rw-r--r-- | src/mailman/commands/cli_control.py | 13 | ||||
| -rw-r--r-- | src/mailman/commands/cli_status.py | 68 | ||||
| -rw-r--r-- | src/mailman/commands/docs/status.txt | 37 |
6 files changed, 245 insertions, 44 deletions
diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py index 263d65a39..2bc155325 100644 --- a/src/mailman/bin/master.py +++ b/src/mailman/bin/master.py @@ -34,7 +34,7 @@ import logging from datetime import timedelta from flufl.enum import Enum -from flufl.lock import Lock, TimeOutError +from flufl.lock import Lock, NotLockedError, TimeOutError from lazr.config import as_boolean from mailman.config import config @@ -109,26 +109,10 @@ instead of the default set. Multiple -r options may be given. The values for -def get_lock_data(): - """Get information from the master lock file. - - :return: A 3-tuple of the hostname, integer process id, and file name of - the lock file. - """ - with open(config.LOCK_FILE) as fp: - filename = os.path.split(fp.read().strip())[1] - parts = filename.split('.') - # Ignore the timestamp. - parts.pop() - pid = parts.pop() - hostname = parts.pop() - filename = DOT.join(reversed(parts)) - return hostname, int(pid), filename - - -# pylint: disable-msg=W0232 class WatcherState(Enum): """Enum for the state of the master process watcher.""" + # No lock has been acquired by any process. + none = 0 # Another master watcher is running. conflict = 1 # No conflicting process exists. @@ -137,35 +121,49 @@ class WatcherState(Enum): host_mismatch = 3 -def master_state(): +def master_state(lock_file=None): """Get the state of the master watcher. - :return: WatcherState describing the state of the lock file. + :param lock_file: Path to the lock file, otherwise `config.LOCK_FILE`. + :type lock_file: str + :return: 2-tuple of the WatcherState describing the state of the lock + file, and the lock object. """ - # pylint: disable-msg=W0612 - hostname, pid, tempfile = get_lock_data() - if hostname != socket.gethostname(): - return WatcherState.host_mismatch + if lock_file is None: + lock_file = config.LOCK_FILE + # We'll never acquire the lock, so the lifetime doesn't matter. + lock = Lock(lock_file) + try: + hostname, pid, tempfile = lock.details + except NotLockedError: + return WatcherState.none, lock + if hostname != socket.getfqdn(): + return WatcherState.host_mismatch, lock # Find out if the process exists by calling kill with a signal 0. try: os.kill(pid, 0) - return WatcherState.conflict + return WatcherState.conflict, lock except OSError as error: if error.errno == errno.ESRCH: # No matching process id. - return WatcherState.stale_lock + return WatcherState.stale_lock, lock # Some other error occurred. raise -def acquire_lock_1(force): +def acquire_lock_1(force, lock_file=None): """Try to acquire the master queue runner lock. :param force: Flag that controls whether to force acquisition of the lock. + :type force: bool + :param lock_file: Path to the lock file, otherwise `config.LOCK_FILE`. + :type lock_file: str :return: The master queue runner lock. :raises: `TimeOutError` if the lock could not be acquired. """ - lock = Lock(config.LOCK_FILE, LOCK_LIFETIME) + if lock_file is None: + lock_file = config.LOCK_FILE + lock = Lock(lock_file, LOCK_LIFETIME) try: lock.lock(timedelta(seconds=0.1)) return lock @@ -174,10 +172,9 @@ def acquire_lock_1(force): raise # Force removal of lock first. lock.disown() - # pylint: disable-msg=W0612 - hostname, pid, tempfile = get_lock_data() - os.unlink(config.LOCK_FILE) - os.unlink(os.path.join(config.LOCK_DIR, tempfile)) + hostname, pid, tempfile = lock.details + os.unlink(lock_file) + os.unlink(tempfile) return acquire_lock_1(force=False) @@ -192,25 +189,22 @@ def acquire_lock(force): lock = acquire_lock_1(force) return lock except TimeOutError: - status = master_state() - if status == WatcherState.conflict: + status, lock = master_state() + if status is WatcherState.conflict: # Hostname matches and process exists. message = _("""\ The master queue runner lock could not be acquired because it appears as though another master is already running.""") - elif status == WatcherState.stale_lock: + elif status is WatcherState.stale_lock: # Hostname matches but the process does not exist. program = sys.argv[0] message = _("""\ The master queue runner lock could not be acquired. It appears as though there is a stale master lock. Try re-running $program with the --force flag.""") - else: + elif status is WatcherState.host_mismatch: # Hostname doesn't even match. - assert status == WatcherState.host_mismatch, ( - 'Invalid enum value: %s' % status) - # pylint: disable-msg=W0612 - hostname, pid, tempfile = get_lock_data() + hostname, pid, tempfile = lock.details message = _("""\ The master qrunner lock could not be acquired, because it appears as if some process on some other host may have acquired it. We can't @@ -221,6 +215,18 @@ Lock file: $config.LOCK_FILE Lock host: $hostname Exiting.""") + else: + assert status is WatcherState.none, ( + 'Invalid enum value: %s' % status) + hostname, pid, tempfile = lock.details + message = _("""\ +For unknown reasons, the master qrunner lock could not be acquired. + + +Lock file: $config.LOCK_FILE +Lock host: $hostname + +Exiting.""") config.options.parser.error(message) @@ -300,7 +306,6 @@ class Loop: # Set up our signal handlers. Also set up a SIGALRM handler to # refresh the lock once per day. The lock lifetime is 1 day + 6 hours # so this should be plenty. - # pylint: disable-msg=W0613,C0111 def sigalrm_handler(signum, frame): self._lock.refresh() signal.alarm(SECONDS_IN_A_DAY) @@ -490,7 +495,6 @@ qrunner %s reached maximum restart limit of %d, not restarting.""", # Wait for all the children to go away. while self._kids: try: - # pylint: disable-msg=W0612 pid, status = os.wait() self._kids.drop(pid) except OSError as error: diff --git a/src/mailman/bin/tests/__init__.py b/src/mailman/bin/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/bin/tests/__init__.py diff --git a/src/mailman/bin/tests/test_master.py b/src/mailman/bin/tests/test_master.py new file mode 100644 index 000000000..cb15f08f9 --- /dev/null +++ b/src/mailman/bin/tests/test_master.py @@ -0,0 +1,81 @@ +# Copyright (C) 2010 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test master watcher utilities.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import os +import errno +import tempfile +import unittest + +from flufl.lock import Lock + +from mailman.bin import master + + + +class TestMasterLock(unittest.TestCase): + def setUp(self): + fd, self.lock_file = tempfile.mkstemp() + os.close(fd) + # The lock file should not exist before we try to acquire it. + os.remove(self.lock_file) + + def tearDown(self): + # Unlocking removes the lock file, but just to be safe (i.e. in case + # of errors). + try: + os.remove(self.lock_file) + except OSError as error: + if error.errno != errno.ENOENT: + raise + + def test_acquire_lock_1(self): + lock = master.acquire_lock_1(False, self.lock_file) + is_locked = lock.is_locked + lock.unlock() + self.failUnless(is_locked) + + def test_master_state(self): + my_lock = Lock(self.lock_file) + # Mailman is not running. + state, lock = master.master_state(self.lock_file) + self.assertEqual(state, master.WatcherState.none) + # Acquire the lock as if another process had already started the + # master qrunner. + my_lock.lock() + try: + state, lock = master.master_state(self.lock_file) + finally: + my_lock.unlock() + self.assertEqual(state, master.WatcherState.conflict) + # XXX test stale_lock and host_mismatch states. + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestMasterLock)) + return suite diff --git a/src/mailman/commands/cli_control.py b/src/mailman/commands/cli_control.py index 83432a6e8..2c693621d 100644 --- a/src/mailman/commands/cli_control.py +++ b/src/mailman/commands/cli_control.py @@ -36,6 +36,7 @@ import logging from zope.interface import implements +from mailman.bin.master import WatcherState, master_state from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.command import ICLISubCommand @@ -54,6 +55,7 @@ class Start: def add(self, parser, command_parser): """See `ICLISubCommand`.""" + self.parser = parser command_parser.add_argument( '-f', '--force', default=False, action='store_true', @@ -92,6 +94,15 @@ class Start: def process(self, args): """See `ICLISubCommand`.""" + # Although there's a potential race condition here, it's a better user + # experience for the parent process to refuse to start twice, rather + # than having it try to start the master, which will error exit. + status, lock = master_state() + if status is WatcherState.conflict: + self.parser.error(_('GNU Mailman is already running')) + elif status in (WatcherState.stale_lock, WatcherState.host_mismatch): + self.parser.error(_('A previous run of GNU Mailman did not exit ' + 'cleanly. Try using --force.')) def log(message): if not args.quiet: print message @@ -152,7 +163,7 @@ def kill_watcher(sig): class SignalCommand: """Common base class for simple, signal sending commands.""" - + implements(ICLISubCommand) name = None diff --git a/src/mailman/commands/cli_status.py b/src/mailman/commands/cli_status.py new file mode 100644 index 000000000..abe8cfe3e --- /dev/null +++ b/src/mailman/commands/cli_status.py @@ -0,0 +1,68 @@ +# Copyright (C) 2010 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Module stuff.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'Status', + ] + + +import socket + +from zope.interface import implements + +from mailman.bin.master import WatcherState, master_state +from mailman.core.i18n import _ +from mailman.interfaces.command import ICLISubCommand + + + +class Status: + """Status of the Mailman system.""" + + implements(ICLISubCommand) + + name = 'status' + + def add(self, parser, command_parser): + """See `ICLISubCommand`.""" + pass + + def process(self, args): + """See `ICLISubCommand`.""" + status, lock = master_state() + if status is WatcherState.none: + message = _('GNU Mailman is not running') + elif status is WatcherState.conflict: + hostname, pid, tempfile = lock.details + message = _('GNU Mailman is running (master pid: $pid)') + elif status is WatcherState.stale_lock: + hostname, pid, tempfile = lock.details + message =_('GNU Mailman is stopped (stale pid: $pid)') + else: + hostname, pid, tempfile = lock.details + fqdn_name = socket.getfqdn() + assert status is WatcherState.host_mismatch, ( + 'Invalid enum value: %s' % status) + message = _('GNU Mailman is in an unexpected state ' + '($hostname != $fqdn_name)') + print message + return int(status) diff --git a/src/mailman/commands/docs/status.txt b/src/mailman/commands/docs/status.txt new file mode 100644 index 000000000..6deb7fdc0 --- /dev/null +++ b/src/mailman/commands/docs/status.txt @@ -0,0 +1,37 @@ +============== +Getting status +============== + +The status of the Mailman master process can be queried from the command line. +It's clear at this point that nothing is running. +:: + + >>> from mailman.commands.cli_status import Status + >>> status = Status() + + >>> class FakeArgs: + ... pass + +The status is printed to stdout and a status code is returned. + + >>> status.process(FakeArgs) + GNU Mailman is not running + 0 + +We can simulate the master queue runner starting up by acquiring its lock. + + >>> from flufl.lock import Lock + >>> lock = Lock(config.LOCK_FILE) + >>> lock.lock() + +Getting the status confirms that the master queue runner is running. + + >>> status.process(FakeArgs) + GNU Mailman is running (master pid: ... + +We shutdown the master queue runner, and confirm the status. + + >>> lock.unlock() + >>> status.process(FakeArgs) + GNU Mailman is not running + 0 |
