#! /usr/bin/env python # # Copyright (C) 1998,1999,2000,2001 by the Free Software Foundation, Inc. # # This program 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 2 # of the License, or (at your option) any later version. # # This program 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 this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """Master queue runner watchdog. This script should be started from an init script. It simply makes sure that the various long-running qrunners are still alive and kicking. It does this by forking the individual qrunners and waiting on their pids. When it detects a subprocess has exited, it will restart it. Sending a SIGINT to the qrunner process raises a KeyboardInterrupt, which is caught and propagated to each subprocess. You can substitute your own queue management system for qrunner, or you can edit this file to tailor which Mailman qrunners actually get started. This way you can say, drop in your own archiver qrunner or your own outgoing qrunner, or define entirely new qrunner paths through the system. The only one that's required is the IncomingRunner, which handles passing a message through the Mailman system. The current set of queues are: - incoming queue for messages from the MTA, Usenet, or other injection processes. - outgoing queue for messages processed by Mailman and intended to go out through the MTA to final destination recipients - news queue for messages processed by Mailman and destined for posting via NNTP. - archive queue for messages processed by Mailman and destined for the internal or external archiver. - bounce queue for bounce messages to be processed by the detector This subsystem is in a bit of flux and should be considered experimental. Specifically, we'd like to support non-Mailman queue processors and a more flexible queuing subsystem (e.g. for authentication queues, etc.) When run as a script, the follow usage is allowed: Usage: qrunner [options] Options: -r runnername --runner=runnername Run the named qrunner exactly once. runnername is the name of a module in the Mailman.Queue package, but it should be just the first part of the package name (i.e. sans `Runner'). E.g. qrunner -r Virgin runs the Mailman.Queue.VirginRunner qrunner once. With no --runner flag, this starts the qrunner watchdog master script. -b/--background Run the qrunner process in the background. Only effective if --runner option is not used. -n/--no-restart Don't restart queue runners when they exit because of an error. -h/--help Print this help message and exit. """ import sys import os import getopt import errno import time from signal import SIGINT import paths from Mailman import mm_cfg from Mailman import Utils from Mailman import LockFile from Mailman.i18n import _ from Mailman.Logging.Syslog import syslog from Mailman.Logging.Utils import LogStdErr # Work around known problems with some RedHat cron daemons import signal signal.signal(signal.SIGCHLD, signal.SIG_DFL) LOCKFILE = os.path.join(mm_cfg.LOCK_DIR, 'master-qrunner') LOCK_LIFETIME = mm_cfg.days(10) SNOOZE = mm_cfg.days(1) def usage(code, msg=''): print >> sys.stderr, _(__doc__) if msg: print >> sys.stderr, msg sys.exit(code) def start_runner(qrclass, slice, count): pid = os.fork() if pid: # parent syslog('qrunner', '%s qrunner started with pid: %d' % (qrclass, pid)) return pid else: # child qrunner = qrclass(slice, count).run() syslog('qrunner', '%s qrunner exiting' % qrclass) os._exit(0) def start_lock_refresher(lock): # This runs in its own subprocess, and it owns the global qrunner lock. pid = os.fork() if pid: # parent return pid # In the child, we simply wake up once per day and refresh the lock try: while 1: lock.refresh() time.sleep(SNOOZE) except KeyboardInterrupt: pass os._exit(0) def master(restart, lock): # Start up the lock refresher process watchdog_pid = start_lock_refresher(lock) kids = {} # Start up all the qrunners for classname, count in mm_cfg.QRUNNERS: modulename = 'Mailman.Queue.' + classname __import__(modulename) qrclass = getattr(sys.modules[modulename], classname) for slice in range(count): info = (qrclass, slice, count) pid = start_runner(*info) kids[pid] = info # # Now just wait for children to end, but also catch KeyboardInterrupts if restart: restarting = '[restarting]' else: restarting = '' try: while 1: try: pid, status = os.wait() killsig = status & 0xff exitstatus = (status >> 8) & 0xff # What should we do with this information other than log it? if pid == watchdog_pid: syslog('qrunner', '''\ qrunner watchdog detected lock refresher exit (pid: %d, sig: %d, sts: %d) %s''' % (pid, killsig, exitstatus, restarting)) if restart: watchdog_pid = start_lock_refresher(lock) else: qrclass, slice, count = kids[pid] syslog('qrunner', '''\ qrunner watchdog detected subprocess exit (pid: %d, sig: %d, sts: %d, class: %s, slice %d of %d) %s''' % (pid, killsig, exitstatus, qrclass, slice, count, restarting)) del kids[pid] # Now perhaps restart the process if restart: newpid = start_runner(qrclass, slice, count) kids[newpid] = (qrclass, slice, count) except KeyboardInterrupt: break finally: # Should we leave the main loop for any reason, we want to be sure all # of our children are exited cleanly. Send SIGINTs to all the child # processes and wait for them all to exit. for pid in kids.keys(): try: os.kill(pid, SIGINT) except OSError, e: if e.errno == errno.ESRCH: # The child has already exited del kids[pid] # Wait for all the childred to go away Utils.reap(kids) def main(): try: opts, args = getopt.getopt(sys.argv[1:], 'r:nh', ['runner=', 'no-restart', 'help']) except getopt.error, msg: usage(1, msg) bg = None runner = None restart = 1 for opt, arg in opts: if opt in ('-h', '--help'): usage(0) elif opt in ('-r', '--runner'): runner = arg elif opt in ('-b', '--background'): bg = 1 elif opt in ('-n', '--no-restart'): restart = 0 COMMASPACE = ', ' if args: usage(1, _('Bad arguments: %s' % COMMASPACE.join(args))) if runner is None: # If we're running as a long-running process in the background, stderr # should go to the error log file. Otherwise it should continue to go # to stderr. LogStdErr('error', 'qrunner', manual_reprime=0, tee_to_stdout=(not bg)) # Be sure we can acquire the master qrunner lock. If not, it means # some other long running qrunner is already going. lock = LockFile.LockFile(LOCKFILE, LOCK_LIFETIME) try: lock.lock(0.5) except LockFile.TimeOutError: print >> sys.stderr, 'Another qrunner is already running, exiting.' sys.exit(0) if bg and not os.fork(): # child master(restart, lock) os._exit(0) master(restart, lock) else: classname = runner + 'Runner' modulename = 'Mailman.Queue.%s' % classname try: __import__(modulename) except ImportError: print >> sys.stderr, 'Qrunner module not found:', modulename raise class_ = getattr(sys.modules[modulename], classname) # Subclass to hack in the setting of the stop flag in the # _doperiodic() subclass. class Once(class_): def _doperiodic(self): self.stop() runner = Once() runner.run() if __name__ == '__main__': main()