Mercurial > processmanager
view ProcessManager.py @ 4:f64af329930f
Any user can now use ProcessManager to manage processes--root is no longer required. However, an error will be displayed if the user doesn't have privileges to manage a process because a user/group change is required.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Wed, 19 Mar 2008 00:01:25 +0000 |
parents | 6d1dc2d106f6 |
children | 7e11415cf272 |
line wrap: on
line source
# ---------------------------------------------------------------------------- # 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, included with this distribution, for more information. """ # ---------------------------------------------------------------------------- # TODO's # # * Document the public methods better. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # 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 = None, gid = None, 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 if gid and uid: import grp import pwd self.gid = grp.getgrnam( gid )[2] self.uid = pwd.getpwnam( uid )[2] elif gid or uid: raise ValueError( "For process '%s', either gid or uid must both be None, " "or both must be set." % self.name ) else: self.gid = None self.uid = None def canCurrentUserManage( self ): """ Returns whether the current user has the ability to manage this process. """ if os.getuid() == 0: # we're running as root, so all is good. result = True elif self.uid is None: # We don't need to change the user to manage the # process, so all is good. result = True elif self.uid == os.getuid() and self.gid == os.getgid(): # uid and gid are specified, but they're the # current user, so all is good. result = True else: # uid and gid are specified, they're different from # the current user, and we're not root, so this # isn't good. result = False return result 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. if self.uid is not None: assert self.gid is not None 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 _runCommandOnProcesses( command, processes ): """ Runs the given command on the given Process objects and returns True if successful, False if an error occurred. """ success = True if command != "status": for process in processes: if not process.canCurrentUserManage(): print ( "The process '%s' cannot be managed " "by the current user." % process.name ) success = False if success: for process in processes: method = getattr( process, command ) try: method() except ProcessStartupError: success = False return success 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 ) if target == "all": processes = [ process for process in _processes.values() ] else: processes = [_processes[target]] if not _runCommandOnProcesses( command, processes ): 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. """ 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. """ 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 )