summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/bin/master.py90
-rw-r--r--src/mailman/bin/tests/__init__.py0
-rw-r--r--src/mailman/bin/tests/test_master.py81
-rw-r--r--src/mailman/commands/cli_control.py13
-rw-r--r--src/mailman/commands/cli_status.py68
-rw-r--r--src/mailman/commands/docs/status.txt37
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