Mercurial > processmanager
diff ProcessManager.py @ 0:3f775e3235fb
Origination, taken from http://www.humanized.com/ProcessManager/ProcessManager-0.0.4.tar.gz (just renamed README.txt to README).
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Tue, 18 Mar 2008 17:44:55 -0500 |
parents | |
children | 6d1dc2d106f6 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProcessManager.py Tue Mar 18 17:44:55 2008 -0500 @@ -0,0 +1,579 @@ +# ---------------------------------------------------------------------------- +# Copyright (c) 2006, Humanized, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# * Neither the name of Humanized, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# +# ProcessManager.py +# Author: Atul Varma <atul@humanized.com> +# +# Python Version - 2.4 +# +# ---------------------------------------------------------------------------- + +""" + A simple module for process management. Please see the file + README.txt, included with this distribution, for more + information. This file is also available at the following + location: + + http://www.humanized.com/ProcessManager +""" + +# ---------------------------------------------------------------------------- +# TODO's +# +# * Document the public methods better. +# +# * Don't require ProcessManager to be run as root, but do raise +# exceptions if the user tries to control a process that requires +# changing the user ID and it can't be done. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------------- + +import os +import sys +import time + + +# ---------------------------------------------------------------------------- +# Public Names and Version Information +# ---------------------------------------------------------------------------- + +__all__ = [ + "Process", + "init", + "add", + "rcScriptMain", + "main" + ] + +__version__ = "0.0.4" + + +# ---------------------------------------------------------------------------- +# Constants +# ---------------------------------------------------------------------------- + +# Amount of time we wait in seconds after starting a process to see if +# it's still alive. +POST_PROCESS_START_DELAY = 5 + +# Amount of time we wait in seconds after killing a process to see if +# it's dead. +POST_PROCESS_STOP_DELAY = 2 + +# A list of all valid commands, accessible from the command-line; they +# map directly to public instance methods of the Process class. +COMMANDS = { + "stop" : "stop the target", + "start" : "start the target", + "restart" : "restart (stop, then start) the target", + "status" : "show status of the target" + } + +# Usage string when running the module's main() function. +USAGE_TEXT = """\ + + %(scriptName)s <target> <command> [options] + +targets: +%(targets)s\ + all (this target applies the command to all + of the above targets) + +commands: +%(commands)s\ +""" + +# Usage string when running the module's rcScriptMain() function. +RC_SCRIPT_USAGE_TEXT = """\ + + %(scriptName)s <command> [options] + +This script controls %(targetDesc)s. + +commands: +%(commands)s\ +""" + +# ---------------------------------------------------------------------------- +# Module Variables +# ---------------------------------------------------------------------------- + +# Directory where all intermediate data files are kept. +_dataDir = None + +# Our process registry; keys are the name identifiers for processes, +# and the values are Process objects. +_processes = {} + +# OptionParser object representing command-line options parser. +_parser = None + +# object storing command-line options, created by an OptionParser +# object. +_options = None + + +# ---------------------------------------------------------------------------- +# Process Class +# ---------------------------------------------------------------------------- + +class Process: + """ + Encapsulates a process that can be stopped, started, and + restarted. + """ + + def __init__( self, + name, + desc, + program, + args, + workingDir, + uid, + gid, + stopSignal = None ): + """ + Creates a process with the given name/identifier, description, + program executable path, argument tuple, and working + directory. When it is run, it will run with the given user + and group ID privileges. When it is stopped, the given signal + will be sent to tell it to do so. + """ + + if stopSignal == None: + import signal + stopSignal = signal.SIGKILL + + self.name = name + self.desc = desc + self.program = program + self.args = [ program ] + self.args.extend( args ) + self.workingDir = workingDir + self.stopSignal = stopSignal + + import grp + import pwd + + self.gid = grp.getgrnam( gid )[2] + self.uid = pwd.getpwnam( uid )[2] + + def _pidfile( self ): + """ + Returns the filename of the pid file for this process. A pid + file just contains the pid of the process, if it's believed to + be currently running. + """ + + return os.path.join( _dataDir, "%s.pid" % self.name ) + + def _readpid( self ): + """ + Opens the pid file for this process and gets the pid for + it. If the pid file doesn't exist, this method returns None. + """ + + if not os.path.exists( self._pidfile() ): + return None + f = open( self._pidfile(), "r" ) + pid = int( f.read() ) + f.close() + return pid + + def status( self ): + """ + Public method that prints out what this process' status is + (running, stopped, etc). + """ + + print "%-30s%s" % ( self.name, self._getStatus() ) + + def _getStatus( self ): + """ + Returns a single word indicating the status of this process. + """ + + pid = self._readpid() + if pid == None: + return "stopped" + elif _isPidRunning( pid ): + return "running" + else: + return "crashed" + + def start( self ): + """ + Public method that starts the process. If the process is + already deemed to be running, nothing happens. + + If the process fails to launch, raise a + ProcessStartupError exception. + """ + + pid = self._readpid() + if pid != None: + if _isPidRunning( pid ): + print "Process '%s' is already running!" % self.name + return + else: + print "Hmm. Process '%s' seems to have died prematurely." % self.name + + # Start the process now. + leftColumnText = "Launching %s..." % self.name + print "%-30s" % leftColumnText, + sys.stdout.flush() + + self._doStart() + + def _doStart( self ): + """ + Protected implementation method that starts the actual + process. + """ + + forkResult = os.fork() + if forkResult == 0: + # We're the child process. + os.setgid( self.gid ) + os.setuid( self.uid ) + + os.chdir( self.workingDir ) + + nullFile = os.open( "/dev/null", os.O_RDWR ) + + # Replace stdin. + os.dup2( nullFile, 0 ) + + # Replace stdout + if not _options.enableStdout: + os.dup2( nullFile, 1 ) + + # Replace stderr + if not _options.enableStderr: + os.dup2( nullFile, 2 ) + + os.close( nullFile ) + + # Launch the program. + os.execv( self.program, self.args ) + else: + # We're the parent process. + pid = forkResult + f = open( self._pidfile(), "w" ) + f.write( "%d" % pid ) + f.close() + + time.sleep( POST_PROCESS_START_DELAY ) + + retVal = os.waitpid( pid, os.WNOHANG ) + if retVal == (0, 0): + print "OK" + else: + print "FAILED" + raise ProcessStartupError() + + def stop( self, warnCrashed = True ): + """ + Public method that stops the process if it's currently + running. + """ + + pid = self._readpid() + if pid != None: + if _isPidRunning( pid ): + leftColumnText = "Stopping %s..." % self.name + print "%-30s" % leftColumnText, + sys.stdout.flush() + + os.kill( pid, self.stopSignal ) + + time.sleep( POST_PROCESS_STOP_DELAY ) + + if not _isPidRunning( pid ): + print "OK" + else: + print "FAILED" + elif warnCrashed: + print "Hmm. Process '%s' seems to have died prematurely." % self.name + os.remove( self._pidfile() ) + else: + print "Process '%s' is not running." % self.name + + def restart( self ): + """ + Public method that stops the process and then starts it again. + """ + + self.stop( warnCrashed = False ) + self.start() + +class ProcessStartupError( Exception ): + """ + Exception raised when a process fails to start. + """ + + pass + + +# ---------------------------------------------------------------------------- +# Module Functions +# ---------------------------------------------------------------------------- + +def init( dataDir ): + """ + Initializes the module. + + dataDir is the directory where all intermediate data files are + stored (e.g., pidfiles). + """ + + global _dataDir + + _dataDir = dataDir + +def _isPidRunning( pid ): + """ + Returns whether or not a process with the given pid is running. + """ + + return os.path.exists( "/proc/%d" % pid ) + +def add( process ): + """ + Adds the given Process object as a target for the registry of + processes to manage. + """ + + if _processes.has_key( process.name ): + raise TargetAlreadyExistsError() + _processes[process.name] = process + +class TargetAlreadyExistsError( Exception ): + """ + Exception raised when a target is added to the ProcessManager + whose name already exists. + """ + + pass + +def _runCommandOnTarget( command, target ): + """ + Runs the given command on the given target. + """ + + if _dataDir == None: + print "Error! ProcessManager not initialized." + print "Please use ProcessManager.init()." + sys.exit( -1 ) + + errorOccurred = False + + if target == "all": + for process in _processes.values(): + method = getattr( process, command ) + try: + method() + except ProcessStartupError: + errorOccurred = True + else: + method = getattr( _processes[target], command ) + try: + method() + except ProcessStartupError: + errorOccurred = True + + if errorOccurred: + sys.exit( -1 ) + +def _checkPrivileges(): + """ + Checks to ensure that the current user has the proper privileges + to run the ProcessManager; exits the program if not. + """ + + if os.getuid() != 0: + print "ERROR: This script must be run as root." + sys.exit( -1 ) + +def _generateTargetHelpText(): + """ + Returns a string containing a list of available targets with their + descriptions. + """ + + targets = "" + for key in _processes.keys(): + targets += " %-21s%s\n" % ( key, _processes[key].desc ) + return targets + +def _generateCommandHelpText(): + """ + Returns a string containing a list of available commands with a + description of what they do. + """ + + commands = "" + for command in COMMANDS.keys(): + commands += " %-21s%s\n" % ( command, COMMANDS[command] ) + commands = commands[:-1] + return commands + +def rcScriptMain(): + """ + The main function of the rc-script use of the Process Manager, + whereby the name of the script determines the target, and the + first command-line parameter determines the command. + """ + + _checkPrivileges() + + target = os.path.split( sys.argv[0] )[1] + if not _processes.has_key( target ): + # If we're in a rc.d directory, we may have 3 characters + # prepended to our name, such as "S01foo". So let's try + # stripping off the first 3 characters of our name and seeing + # if that works as a target. + if target[0] in ["K", "S"]: + ordering = target[1:3] + try: + # See if these characters constitute a number. + int( ordering ) + # If so, let's try reinterpreting our target. + target = target[3:] + except ValueError: + pass + + if not _processes.has_key( target ): + print "ERROR: Target '%s' does not exist!" % target + print "Consider renaming this script to match one" + print "of the following targets:" + print + print _generateTargetHelpText() + sys.exit( -1 ) + + usageTextDict = { + "scriptName" : target, + "targetDesc" : _processes[ target ].desc, + "commands" : _generateCommandHelpText(), + } + + usageText = RC_SCRIPT_USAGE_TEXT % usageTextDict + + _processCmdLineOptions( usageText ) + + if len( sys.argv ) == 1: + command = "" + else: + command = sys.argv[1] + + if not command in COMMANDS.keys(): + _parser.print_help() + sys.exit( -1 ) + + _runCommandOnTarget( command, target ) + +def _processCmdLineOptions( usageText ): + """ + Parses and processes standard command-line options. + """ + + import optparse + + global _parser + global _options + global _args + + _parser = optparse.OptionParser( usage = usageText ) + _parser.add_option( + "-e", "--enable-stderr", + action = "store_true", dest = "enableStderr", default = False, + help = "enable output of starting target's stderr to console" + ) + _parser.add_option( + "-o", "--enable-stdout", + action = "store_true", dest = "enableStdout", default = False, + help = "enable output of starting target's stdout to console" + ) + _parser.add_option( + "-v", "--version", + action = "store_true", dest = "showVersion", default = False, + help = "print version information and exit" + ) + + ( _options, _args ) = _parser.parse_args() + + if _options.showVersion: + print "ProcessManager v%s (invoked via %s)" % \ + ( __version__, sys.argv[0] ) + sys.exit( 0 ) + +def main(): + """ + The main function of the Process Manager which processes + command-line arguments and acts on them. + """ + + _checkPrivileges() + + usageTextDict = { + "scriptName" : os.path.split( sys.argv[0] )[1], + "targets" : _generateTargetHelpText(), + "commands" : _generateCommandHelpText(), + } + + usageText = USAGE_TEXT % usageTextDict + + _processCmdLineOptions( usageText ) + + if len( _args ) < 2: + _parser.print_help() + sys.exit( -1 ) + + target = _args[0] + command = _args[1] + + if target not in _processes.keys() and target != "all": + print "Invalid target: '%s'" % target + sys.exit( -1 ) + if command not in COMMANDS.keys(): + print "Invalid command: '%s'" % command + sys.exit( -1 ) + + _runCommandOnTarget( command, target ) +