diff options
| -rw-r--r-- | src/mailman/bin/docs/master.txt | 1 | ||||
| -rw-r--r-- | src/mailman/bin/master.py | 22 | ||||
| -rw-r--r-- | src/mailman/commands/cli_control.py | 169 | ||||
| -rw-r--r-- | src/mailman/commands/docs/control.txt | 128 |
4 files changed, 312 insertions, 8 deletions
diff --git a/src/mailman/bin/docs/master.txt b/src/mailman/bin/docs/master.txt index 0d3cade77..2c396d8aa 100644 --- a/src/mailman/bin/docs/master.txt +++ b/src/mailman/bin/docs/master.txt @@ -1,3 +1,4 @@ +============================ Mailman queue runner control ============================ diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py index 579583c67..99555d890 100644 --- a/src/mailman/bin/master.py +++ b/src/mailman/bin/master.py @@ -27,12 +27,13 @@ __all__ = [ import os import sys +import time import errno import signal import socket import logging -from datetime import timedelta +from datetime import datetime, timedelta from lazr.config import as_boolean from locknix import lockfile from munepy import Enum @@ -46,6 +47,7 @@ from mailman.options import Options DOT = '.' LOCK_LIFETIME = timedelta(days=1, hours=6) SECONDS_IN_A_DAY = 86400 +SUBPROC_START_WAIT = timedelta(seconds=20) @@ -146,8 +148,8 @@ def master_state(): try: os.kill(pid, 0) return WatcherState.conflict - except OSError, e: - if e.errno == errno.ESRCH: + except OSError as error: + if error.errno == errno.ESRCH: # No matching process id. return WatcherState.stale_lock # Some other error occurred. @@ -405,10 +407,14 @@ class Loop: and configured to do so. """ log = logging.getLogger('mailman.qrunner') + # Sleep until a signal is received. This prevents the master from + # existing immediately even if there are no qrunners (as happens in + # the test suite). + signal.pause() while True: try: pid, status = os.wait() - except OSError, error: + except OSError as error: # No children? We're done. if error.errno == errno.ECHILD: break @@ -466,7 +472,7 @@ qrunner %s reached maximum restart limit of %d, not restarting.""", for pid in self._kids: try: os.kill(pid, signal.SIGTERM) - except OSError, error: + except OSError as error: if error.errno == errno.ESRCH: # The child has already exited. log.info('ESRCH on pid: %d', pid) @@ -476,10 +482,10 @@ qrunner %s reached maximum restart limit of %d, not restarting.""", # pylint: disable-msg=W0612 pid, status = os.wait() self._kids.drop(pid) - except OSError, e: - if e.errno == errno.ECHILD: + except OSError as error: + if error.errno == errno.ECHILD: break - elif e.errno == errno.EINTR: + elif error.errno == errno.EINTR: continue raise diff --git a/src/mailman/commands/cli_control.py b/src/mailman/commands/cli_control.py new file mode 100644 index 000000000..919ebc0be --- /dev/null +++ b/src/mailman/commands/cli_control.py @@ -0,0 +1,169 @@ +# Copyright (C) 2009 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__ = [ + 'Start', + 'Stop', + ] + + +import os +import sys +import signal +import logging + +from zope.interface import implements + +from mailman.config import config +from mailman.i18n import _ +from mailman.interfaces.command import ICLISubCommand + + +qlog = logging.getLogger('mailman.qrunner') + + + +class Start: + """Start the Mailman daemons.""" + + implements(ICLISubCommand) + + name = 'start' + + def add(self, parser, command_parser): + """See `ICLISubCommand`.""" + command_parser.add_argument( + '-f', '--force', + default=False, action='store_true', + help=_("""\ + If the master watcher finds an existing master lock, it will + normally exit with an error message. With this option,the master + will perform an extra level of checking. If a process matching + the host/pid described in the lock file is running, the master + will still exit, requiring you to manually clean up the lock. But + if no matching process is found, the master will remove the + apparently stale lock and make another attempt to claim the master + lock.""")) + command_parser.add_argument( + '-u', '--run-as-user', + default=True, action='store_false', + help=_("""\ + Normally, this script will refuse to run if the user id and group + id are not set to the 'mailman' user and group (as defined when + you configured Mailman). If run as root, this script will change + to this user and group before the check is made. + + This can be inconvenient for testing and debugging purposes, so + the -u flag means that the step that sets and checks the uid/gid + is skipped, and the program is run as the current user and group. + This flag is not recommended for normal production environments. + + Note though, that if you run with -u and are not in the mailman + group, you may have permission problems, such as begin unable to + delete a list's archives through the web. Tough luck!""")) + command_parser.add_argument( + '-q', '--quiet', + default=False, action='store_true', + help=_("""\ + Don't print status messages. Error messages are still printed to + standard error.""")) + + def process(self, args): + """See `ICLISubCommand`.""" + def log(message): + if not args.quiet: + print message + # Daemon process startup according to Stevens, Advanced Programming in + # the UNIX Environment, Chapter 13. + pid = os.fork() + if pid: + # parent + log(_("Starting Mailman's master qrunner")) + return + # child: Create a new session and become the session leader, but since + # we won't be opening any terminal devices, don't do the + # ultra-paranoid suggestion of doing a second fork after the setsid() + # call. + os.setsid() + # Instead of cd'ing to root, cd to the Mailman runtime directory. + os.chdir(config.VAR_DIR) + # Exec the master watcher. + execl_args = [ + sys.executable, sys.executable, + os.path.join(config.BIN_DIR, 'master'), + ] + if args.force: + execl_args.append('--force') + if args.config: + execl_args.extend(['-C', args.config]) + qlog.debug('starting: %s', execl_args) + os.execl(*execl_args) + # We should never get here. + raise RuntimeError('os.execl() failed') + + + +def kill_watcher(sig): + try: + with open(config.PIDFILE) as fp: + pid = int(fp.read().strip()) + except (IOError, ValueError) as error: + # For i18n convenience + print >> sys.stderr, _('PID unreadable in: $config.PIDFILE') + print >> sys.stderr, error + print >> sys.stderr, _('Is qrunner even running?') + return + try: + os.kill(pid, sig) + except OSError as error: + if e.errno != errno.ESRCH: + raise + print >> sys.stderr, _('No child with pid: $pid') + print >> sys.stderr, e + print >> sys.stderr, _('Stale pid file removed.') + os.unlink(config.PIDFILE) + + + +class Stop: + """Stop the Mailman daemons.""" + + implements(ICLISubCommand) + + name = 'stop' + + def add(self, parser, command_parser): + """See `ICLISubCommand`.""" + command_parser.add_argument( + '-q', '--quiet', + default=False, action='store_true', + help=_("""\ + Don't print status messages. Error messages are still printed to + standard error.""")) + + def process(self, args): + """See `ICLISubCommand`.""" + def log(message): + if not args.quiet: + print message + log(_("Shutting down Mailman's master qrunner")) + kill_watcher(signal.SIGTERM) diff --git a/src/mailman/commands/docs/control.txt b/src/mailman/commands/docs/control.txt new file mode 100644 index 000000000..7b7aa6195 --- /dev/null +++ b/src/mailman/commands/docs/control.txt @@ -0,0 +1,128 @@ +============================= +Starting and stopping Mailman +============================= + +The Mailman daemon processes can be started and stopped from the command +line. + + +Set up +====== + +All we care about is the master process; normally it starts a bunch of +qrunners, but we don't care about any of them, so write a test configuration +file for the master that disables all the qrunners. + + >>> import shutil + >>> from os.path import dirname, join + >>> config_file = join(dirname(config.filename), 'no-qrunners.cfg') + >>> shutil.copyfile(config.filename, config_file) + >>> with open(config_file, 'a') as fp: + ... print >> fp, """\ + ... [qrunner.archive] + ... start: no + ... [qrunner.bounces] + ... start: no + ... [qrunner.command] + ... start: no + ... [qrunner.in] + ... start: no + ... [qrunner.lmtp] + ... start: no + ... [qrunner.news] + ... start: no + ... [qrunner.out] + ... start: no + ... [qrunner.pipeline] + ... start: no + ... [qrunner.rest] + ... start: no + ... [qrunner.retry] + ... start: no + ... [qrunner.virgin] + ... start: no + ... [qrunner.digest] + ... start: no + ... """ + + +Starting +======== + + >>> from mailman.commands.cli_control import Start + >>> start = Start() + + >>> class FakeArgs: + ... force = False + ... run_as_user = True + ... quiet = False + ... config = config_file + >>> args = FakeArgs() + +Starting the daemons prints a useful message and starts the master qrunner +watcher process in the background. + + >>> start.process(args) + Starting Mailman's master qrunner + + >>> import errno, os, time + >>> from datetime import timedelta, datetime + >>> def find_master(): + ... until = timedelta(seconds=10) + datetime.now() + ... while datetime.now() < until: + ... time.sleep(0.1) + ... try: + ... with open(config.PIDFILE) as fp: + ... pid = int(fp.read().strip()) + ... os.kill(pid, 0) + ... except IOError as error: + ... if error.errno != errno.ENOENT: + ... raise + ... except ValueError: + ... pass + ... except OSError as error: + ... if error.errno != errno.ESRCH: + ... raise + ... else: + ... print 'Master process found' + ... return pid + ... else: + ... raise AssertionError('No master process') + +The process exists, and its pid is available in a run time file. + + >>> pid = find_master() + Master process found + + +Stopping +======== + +You can also stop the master watcher process from the command line, which +stops all the child processes too. + + >>> from mailman.commands.cli_control import Stop + >>> stop = Stop() + >>> stop.process(args) + Shutting down Mailman's master qrunner + + >>> def bury_master(): + ... until = timedelta(seconds=10) + datetime.now() + ... while datetime.now() < until: + ... time.sleep(0.1) + ... try: + ... import sys + ... os.kill(pid, 0) + ... os.waitpid(pid, os.WNOHANG) + ... except OSError as error: + ... if error.errno == errno.ESRCH: + ... # The process has exited. + ... print 'Master process went bye bye' + ... return + ... else: + ... raise + ... else: + ... raise AssertionError('Master process lingered') + + >>> bury_master() + Master process went bye bye |
