Changeset 87

Show
Ignore:
Timestamp:
06/30/06 20:24:59 (3 years ago)
Author:
robin
Message:

significant rationalization of process management

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • slavetools/trunk/lib/slavetools/noseplug.py

    r82 r87  
    66protocol in unsupported ways. 
    77 
    8 Overrides `nose.plugins.base.Plugin.begin` to trigger service start/restart. 
    9 overrides `finalize` to trigger service shutdown.  
    10  
     8This plugin implements `nose.plugins.base.Plugin.begin` to trigger service 
     9start/restart and `finalize` to trigger service shutdown.  
     10  
    1111This plugin does not attempt to introspect or otherwise monkey with 
    1212sys.argv. It is driven entirely by: the options passed in by nose; the 
     
    1414there corresponding default values; 
    1515 
    16 By default all options defined by this plugin are prefixed with `slave`. 
    17 This can be changed by derving a from SlaveService and modfying the class 
    18 attribute OPT_PREFIX. command line help automaticaly substitutes the default 
    19 for the value of OPT_PREFIX when the help is generated. 
    20  
    2116In `begin` (ie., before any tests run), this plugin attempt to launch the 
    2217slave. Excepting certain plugin specific options (listed below); `begin` will 
    23 forward `--slave-` decorated options, in undecorated form, to the slave *only* 
    24 if they are set to non default values. The principle advantage of this is that 
    25 it reduces the number of options that slaves are forced to handle in order to 
    26 be compatible with this plugin. Typically only a subset of the process 
    27 management options (listed below) need be supported.  Excepting those, if your 
    28 slave does not support a particular option offered by this plugin, then never 
    29 pass it to nose and it will *never* get passed to your slave.  
     18forward any `--slave-` decorated options, in undecorated form, to the slave if 
     19they are set to non default values. If your slave does not support a particular 
     20option offered by this plugin, then never pass it to nose and it will *never* 
     21get passed to your slave. 
     22 
     23.. _`process identifying options`:  
     24 
     25``--slave-program``, ``--slave-directory``, ``--slave-pidfile``, 
     26``--slave-prevpid`` 
     27 
     28Before launching your process this plugin attempts to read a process id from 
     29the file inferred by the `process identifying options`_. If it is successfull 
     30it attempts to kill of that process before continuing. 
     31 
     32A short delay is imposed in `begin` to allow your slave time to start. After 
     33this (configurable) delay this plugin checks to see if the process it launched 
     34is still alive. If it is alive then the pid is cached for finalize and the 
     35plugin returns control to `nose`. If it has terminated this plugin assumes that 
     36it was a daemon or some other indirect startup script. It then (again) attempts 
     37to read a pid from the file infered by the the `process identifying options`_. 
     38If a pid was successfully read the plugin performs a second check, this time 
     39using the pid it read from file. If it is still not alive the plugin logs a 
     40warnign and returns control to `nose`. 
     41 
     42In all cases: If the plugin finds a 'live' pid after the warm up delay that is 
     43still alive then it caches it for use in finalize and immediately returns 
     44control to `nose`. If it does not find a live pid after the warmup period it 
     45will not attempt to kill off anything in finalize. 
     46 
     47In all cases: The *first* thing that happens in `begin` is an attempt to kill  
     48based on the `process identifying options`_ 
    3049 
    3150python's standard `subprocess.Popen` is used to launch the process.  The 
     
    3857plugin. 
    3958 
    40 `--slave-directory` and `--slave-configfile`; by default these 
    41 are set to the empty string and are treated as `not set`. If you use them 
    42 be aware that they are converted to absoloute paths before being 
    43 incorporated into the slave process arguments.`--slave-configfile` 
    44 takes `--slave-directory into account for this conversion. 
    45  
     59By default all options defined by this plugin are prefixed with `--slave-`. 
     60This can be changed by derving a from SlaveService and modfying the class 
     61attribute OPT_PREFIX. command line help automaticaly substitutes the default 
     62for the value of OPT_PREFIX when the help is generated. 
     63  
    4664NOTE:setuptools test - If you are running tests through setuptools, 
    4765`finalize` does not get called, however the next test run will attempt to 
     
    5472from subprocess import Popen 
    5573from nose.plugins import Plugin 
    56 from optrun.cliapi import * 
    57 from optrun.clitool import * 
     74from slavetools.optionsapi import add_options, prefix_longopts 
     75#from slavetools.optionsapi import prune_shortopts, update_options 
     76#from slavetools.optionsapi import COMMON_OPTS 
     77#from slavetools.slaveapi_posix import COMMON_POSIX_OPTS, START_POSIX_OPTS 
     78from slavetools.slaveapi_posix import read_pidfile, pidfile_name 
     79from slavetools.slaveapi_posix import kill_posix, pollpid_posix 
    5880 
    5981PRUNE_SLAVE_LONGOPTS=['--dont-forward-opts'] 
     
    6385        help='By default no slave program is invoked. Set this option ' 
    6486        'to reflect the program name part of the command line desired ' 
    65         'For starting and restarting the slave program. ' 
    66         'Only a very limmited amount of spliting is supported (see ' 
    67         'SLAVE_PROGRAM_SPLIT for details) ' 
    68         'If the option ' 
    69         'to enable forwarding of slave options is enabled (the default is ' 
    70         'off) they are inserted into the command line *after* the command ' 
    71         'line parts implied by this option. ')), 
     87        'for starting the slave program. Only a very limmited amount of ' 
     88        'spliting is supported (see SLAVE_PROGRAM_SPLIT for details) ' 
     89        'If the option to enable forwarding of slave options is enabled ( ' 
     90        'the default is off) they are inserted into the command line ' 
     91        '*after* the command line parts implied by this option. ')), 
     92    ('--slave-prevpid', dict( 
     93        default=None, 
     94        help='set to -1 to prevent attempts to kill of predecessor. ' 
     95        'If you know the predecessor pid and want to disable cleanup ' 
     96        'of the pidfile then set this option to the predecessors pid. ' 
     97        'it will get killed but no attempt is made to clean up the pid ' 
     98        'file.')), 
     99    ('--slave-grace', dict( 
     100        default="2.5", help='wait this long during `begin` before attempting' 
     101        'to locate the slave process id')), 
    72102   ('--slave-program-split', dict( 
    73         default=None, help='set this to \' \' to enable the most ' 
     103        default=' ', help='set this to \' \' to enable the most ' 
    74104        'simplistic (and only supported) splitting technique')), 
    75105   ('--slave-args', dict( 
     
    81111        'command line is `\' \'.join([SLAVE_PROGRAM, SLAVE_ARGS])`.')), 
    82112   ('--slave-args-split', dict( 
    83         default=None, help='see above')), 
    84    ('--slave-pidfile-writeable', dict( 
    85        default=False, action='store_true', 
    86        help='By default we do *NOT* assume it is reasonable to write ' 
    87        'to the pidfile implied by the --slave- options. If this flag set, ' 
    88        ' AND --slave-daemonize is *not* set, a pidfile is writen to the ' 
    89        'location we beleive the slave would use if it was running as ' 
    90        'a daemon. If this flag is *not* set AND --slave-daemonize is ' 
    91        'also not set then we write a pidfile to a location derived ' 
    92        'from the current working directory. NOTE: In all cases '  
    93        'if --slave-daemonize is set we NEVER write a pidfile. Further ' 
    94        'if we do write a pidfile an absoloute path passed via ' 
    95        '--slave-pidfile will trump all options affecting the pidfile name ' 
    96        )), 
     113        default=' ', help='see above')), 
    97114   ('--slave-more-help', dict(default=False, action='store_true')) 
    98115    ] 
     
    102119USAGE="""\ 
    103120This plugin attempts to manage the slave process by inspecting the options: 
    104 `--slave-directory`, `--slave-daemonize`, 
    105 `--slave-pidfile`, `--slave-pid`, 
    106 `--slave-pidfile-prefix` 
     121`--slave-directory`, `--slave-pidfile`, `--slave-prevpid`, 
    107122 
    108123In all cases The following options are *NEVER* forwarded:  
    109124%(NEVER_FORWARDS)s 
    110125 
    111 If the current working directory and `--slave-program` are sufficient to infer 
    112 the file your slave writes it's pid to then your slave need not support *any* 
    113 of the options offered by this plugin. (notetest -w is not subverted in any way 
    114 so your current working directory need not be your testsuite location). 
     126nosetest -w is not subverted in any way so your current working directory  
     127need not be your testsuite location. 
    115128 
    116129nosetest --slave-more-help will print a much more detailed description of 
     
    123136    OPT_PREFIX='slave-' 
    124137    LOGCHAN='slaveservice' 
    125  
     138    SLAVE_OPTIONS=[ 
     139        ('--directory', dict( 
     140            default='./',  
     141            help='Set the initial working directory. plugin does all' 
     142            'relative file guess work based on this directory. Its converted ' 
     143            'to an absoloute path before passing it on to the slave.')), 
     144        ('--pidfile', dict(default=None,help= 
     145            'Direct the plugin to where the "live" pid should be found.' 
     146            'use this when --slave-program is just an indirect step.')), 
     147        ('--dont-forward-opts', dict(default=False, action='store_true', 
     148            help='For convenience, all options that are specified to the "start" ' 
     149            ' command are copied into the *begining* of the argumets passed ' 
     150            ' to the child. Set this flag to disable this behaviour.')), 
     151        ] 
     152    # much more comprehensive set of options by doing: 
     153    #SLAVE_OPTIONS=list(prune_shortopts(reduce(update_options, [ 
     154    #    COMMON_OPTS, COMMON_POSIX_OPTS, START_POSIX_OPTS]))) 
     155    # but the help will be confusing unless you patch it up 
     156  
    126157    def help(self): 
    127158        # used for help generation only, changing this does *not* effect  
     
    130161            self.usage.replace('--slave-','--%s' % self.OPT_PREFIX)) 
    131162 
    132     SLAVE_OPTIONS=list(prune_shortopts(reduce(update_options, [ 
    133         COMMON_OPTS, COMMON_UNIX_OPTS, START_UNIX_OPTS]))) 
    134163 
    135164    # END user customizable class parameters 
     
    181210        self.slave_args=self._getprefixedattr(options, 'args') 
    182211        self.slave_args_split=self._getprefixedattr(options, 'args_split') 
    183         self.slave_pidfile_writeable=self._getprefixedattr( 
    184             options, 'pidfile_writeable') 
     212        self.slave_prevpid=self._getprefixedattr( 
     213            options, 'prevpid') 
     214        try: 
     215            self.slave_grace=float( 
     216                self._getprefixedattr(options, 'grace')) 
     217        except ValueError: 
     218            self.slave_grace=2.5 
    185219        if not self.slave_program: 
    186220            self.enabled=False 
    187221 
    188222    def _getprefixedattr(self, options, attr): 
    189        return getattr(options,  
    190            '%s%s' % (self.OPT_PREFIX.lower().replace('-','_'), attr) 
    191            
     223        return getattr(options,  
     224            '%s%s' % (self.OPT_PREFIX.lower().replace('-','_'), attr) 
     225           
    192226  
    193227    def prepare_slave_program(self): 
     
    259293        (typical) idiom of read pid, kill if read, write pid then that child 
    260294        becomes a suicide. (this abnormal termination will be noticed by this 
    261         function, and avoided by using the `kill_unix` method used by this 
    262         plugin - it does kill if readpid and readpid != pid) 
    263   
    264         If --slave-daemonize *is* set: 
    265             We assume the daemon writes a pid file 
    266             according to semantics compatible with this plugin. (ie., it 
    267             changes into --directory then writes --pidfile exactly as passed 
    268             on the command line. If no pidfile is passed, --slave-program 
    269             and --slave-pidfile-prefix are enough control to arrange for  
    270             aggrement. 
    271  
    272         If --slave-daemonize is *not* set: 
    273             If it is not we write the pidfile ourselves but in 
    274             a directory infered from the *current working directory*. 
    275             The reason we do not just rely on the pid we get from Popen is 
    276             that the slave program may be a simple starter script that 
    277             that terminates immediately. 
    278  
    279         In all cases, after launching the process we attempt to read the 
    280         process id back from the pidfile after a small amount of time has 
    281         passed. This allows for children that fork, daemonize or simply launch 
    282         another programe. In the end *something* is expected to write to the 
    283         pidfile implied by the options to this plugin.  
    284          
    285         If this function decides to write the pidfile then it caches the  
    286         *absoloute* file name it wrote, waits a bit after launch and reads the 
    287         file it wrote. 
    288  
    289        
     295        function. It can be avoided in your own programs by using the  
     296        `kill_posix` method used by this plugin - it does kill if: 
     297            ``readpid and readpid != os.getpid()`` 
    290298        """ 
    291         wrotepidfile = False 
     299        readpidfile = False 
     300        pidfile = None 
    292301        abspidfile = None 
    293302        log = logging.getLogger(self.LOGCHAN) 
    294         options=self.slave_known_options 
    295         self.slave_progname=args[0] 
    296         cwd=os.getcwd() 
     303        options = self.slave_known_options 
     304        self.slave_progname = args[0] 
     305        cwd = os.getcwd() 
    297306        try: 
    298307            os.chdir(options.directory) 
    299             # Note: if --slave-pidfile was specified then that is *exactly* 
    300             # the filename we get back here. 
    301             pidfile=kill_unix(log, options, self.slave_progname) 
    302             abspidfile=os.path.abspath(pidfile) 
     308            # deal with predecessor unless otherwise directed 
     309            if self.slave_prevpid != 1: 
     310                log.info('*** attempting to kill of predecessor') 
     311                # we could possibly avoid the directory change if an explicit 
     312                # pid for the predecessor is provided (or either of  
     313                # --slave-directory or --slave-pidfile are absoloute). Not 
     314                # convinced this would be usefull. 
     315                # Note: if --slave-pidfile was specified then that is *exactly* 
     316                # the filename we get back here. 
     317                pidfile=kill_posix(log, options, self.slave_progname, 
     318                    # unless the user explicitly sets it on the cl via 
     319                    # --slave-prevpid then self.slave_prevpid will be None 
     320                    pid=self.slave_prevpid) 
     321            if pidfile is None: # None if explicit prevpid is provided 
     322                pidfile = pidfile_name(options, self.slave_program) 
     323            if pidfile: # could still be None 
     324                abspidfile=os.path.abspath(pidfile) 
     325            # launch the process 
    303326            p = Popen(args) 
    304             pid = p.pid 
    305             try: 
    306                 os.chdir(cwd) 
    307                 if not options.daemonize and self.slave_pidfile_writeable: 
    308                     abspidfile, wrotepidfile = write_pidfile( 
    309                         pidfile, pid, log) 
    310             finally: 
    311                 os.chdir(options.directory) 
    312  
    313             log.info('*** abspidfile: %s' % abspidfile) 
    314             log.info('*** pidfile: %s' % pidfile) 
    315             if wrotepidfile: 
    316                 log.info('*** WROTE pid to file: %s' % abspidfile) 
    317             log.info('*** SHOULD READ pid from file: %s' % abspidfile)  
    318   
    319             self.tstart = time() 
    320             sleep(2.5) #XXX: make this an option 
    321             # *always* attempt to read back the pidfile 
    322             pid, readpidfile=read_pidfile(abspidfile, p.pid) 
    323             if not readpidfile: 
     327            self.tstart = time() # for benefit of `finalize` 
     328            sleep(self.slave_grace) 
     329            rcode=p.poll() 
     330            if rcode is None: 
     331                log.info('*** [%s] is running, pid = [%s]' % 
     332                    (args[0], p.pid)) 
     333                self.pid=p.pid 
     334                log.info('****** LEAVING SlaveService.begin') 
     335                return 
     336            if abspidfile: 
     337                log.info(( 
     338                    '*** [%s] exited with [%s], ' % (p.pid, rcode))) 
     339                log.info('*** ATTEMPTING TO READ pid from file: %s' % abspidfile) 
     340                pid, readpidfile = read_pidfile(abspidfile, p.pid) 
     341                if not readpidfile: 
     342                    log.warning( 
     343                        '*** FAILED TO READ pid from file %s' % abspidfile) 
     344                else: 
     345                    log.info("*** READ pid [%s] from [%s]" % (pid, abspidfile)) 
     346            else: 
     347                pid=p.pid 
     348            if readpidfile: 
     349                rcode = pollpid_posix(pid) 
     350            if rcode is not None: 
    324351                log.warning( 
    325                     "could not read slave process id from file %s" % abspidfile) 
     352                    '*** FAILED TO GET SLAVE PID, slave-program ' 
     353                     'exit code: %s' % rcode) 
     354                self.pid = -1 
    326355            else: 
    327                 log.info("***DID READ pid [%s] from [%s]" % (abspidfile, 
    328                     pid)) 
    329             self.pid = pid 
    330             self.abspidfile=None 
    331             if wrotepidfile: 
    332                 self.abspidfile = abspidfile 
    333             self.wrotepidfile = wrotepidfile 
    334             self.readpidfile = readpidfile 
    335             # daemon or otherwise, poll the pid we read from file in 
    336             # preference, but fall back on subprocess if we could not read the 
    337             # file. In the case where we read from file subprocess can't help 
    338             # us be platform neutral. 
    339             if readpidfile: 
    340                 rcode = pollpid_unix(pid) 
    341             else: 
    342                 rcode = p.poll() 
    343             if rcode is not None: 
    344                 log.warning("slave pid=%s has terminated [%s]" % (pid, rcode)) 
     356                self.pid = pid 
    345357        finally: 
    346358            os.chdir(cwd) 
     
    348360    def finalize(self, result): 
    349361        log = logging.getLogger(self.LOGCHAN) 
    350         options=self.slave_known_options 
    351         t=self.tstart-time() 
    352         if (0.5 - t > 0.0):  # XXX: make this minimum life an option 
    353             sleep(0.5 - t) 
    354         cwd=os.getcwd() 
    355         try: 
    356             os.chdir(options.directory) 
    357             # if we wrote the file we want to be certain that is the file 
    358             # we read the pid file from, other wise we leave it to the  
    359             # same combination of options & guess work used for startup 
    360             assert self.abspidfile is None or self.wrotepidfile 
    361             pidfile = kill_unix(log, options, self.slave_progname, 
    362                 abspidfile=self.abspidfile, 
    363                 removepidfile=self.slave_pidfile_writeable) 
    364         finally: 
    365             os.chdir(cwd) 
    366   
    367  
    368  
     362        if self.pid == -1: 
     363            return 
     364        kill_posix(log, None, self.slave_progname, 
     365                pid=self.pid) 
     366 
     367 
  • slavetools/trunk/lib/slavetools/optionsapi.py

    r82 r87  
    33""" 
    44__all__=( 
    5         'COMMON_OPTS fmt_msg get_log get_program_name
    6         'add_options add_options_common update_options option_defaults
     5        'COMMON_OPTS
     6        'add_options update_options option_defaults build_parser
    77        'prune_shortopts prefix_longopts ' 
    8         'build_parser run_wrapper run_common ' 
    98        ).split() 
    109 
    11 import sys, os, logging, optparse, types 
     10import optparse 
    1211 
    1312COMMON_OPTS=[ 
     
    2726            default='', 
    2827            help='Read a python syntax configuration file by importing it.')), 
    29    ('--wingdbstub', dict( 
     28    ('--wingdbstub', dict( 
    3029            action='store_true', 
    3130            default=False, help= 
     
    4746 
    4847def add_options(parser, options): 
     48    """add a list of options to an optparse.OptionParser compatible thing.""" 
    4949    for opt in options: 
    5050        if not isinstance(opt[-1], dict): 
     
    5353 
    5454def prune_shortopts(options): 
     55    """return a copy of options with all short options removed. 
     56     
     57    transformations are:: 
     58 
     59        (shopt, lopt, kwargs, ...)  => (lopt, kwargs, ...) 
     60        (lopt, shopt, kwargs, ...)  => (lopt, kwargs, ...) 
     61        (shopt, kwargs)             => ** discarded completely ** 
     62        (lopt, kwargs, ...)         => (lopt, kwargs, ...) 
     63 
     64    """ 
    5565    for opt in options: 
    5666        if opt[0].startswith('--'): 
     
    98108    both lists any items after the first dict are ignored (and will not be 
    99109    reflected in the result) 
    100  
     110     
     111    options that specify both short and long options may do so either way 
     112    round, (shopt,lopt, ...) and (lopt,shopt, ...) are both acceptable. 
     113    However, all option names must be specified within the first two items so 
     114    (shopt, kwargs, lopt, ...) is *not* supported 
    101115    """ 
    102116    optmaps=[] 
     
    120134  
    121135def add_options_common(parser,  
    122     options=COMMON_OPTS # intentionaly *not* copying the list 
     136    options=COMMON_OPTS[:] 
    123137    ): 
     138    """trivial helper, 
     139     
     140    calls `add_options` with the options defined as COMMON 
     141    by this module""" 
    124142    add_options(parser, options) 
    125143 
    126 def get_program_name(argv=None): 
    127     if argv is None: 
    128         argv=sys.argv 
    129     return os.path.splitext(os.path.basename(argv[0]))[0] 
    130  
    131 def get_log(options, defaultlogger=''): 
    132     """get the logger spefified by COMMON_OPTS""" 
    133     if options.logger is None: 
    134         log = logging.getLogger(defaultlogger) 
    135     # this call to basicConfig does nothing if the caller has allready 
    136     # configured logging or called any of the logging module top level loging 
    137     # functions 
    138     logging.basicConfig() 
    139     return log 
    140  
    141 def run_wrapper(programname, options, args, run, *runargs, **runkw): 
    142     """handles the common options 
    143      
    144     including set and restore of working directory. 
    145  
    146     exists mainly to make it easy to do `os.chdri(cwd)` after run completes 
    147     """ 
    148  
    149     rcode = -1 
    150     log = get_log(options, programname) 
    151     if options.loglevel is not None: 
    152         try: 
    153             lvl=int(options.loglevel) 
    154         except ValueError: 
    155             try: 
    156                 lvl=getattr(logging,options.loglevel) 
    157             except AttributeError: 
    158                 # the default configuration of the loggin package will show 
    159                 # this message 
    160                 log.warn("Unable to set loglevel %s" % str(options.loglevel)) 
    161                 lvl = None 
    162         if lvl is not None: 
    163             log.setLevel(lvl) 
    164     if options.wingdbstub: 
    165         try: 
    166             import wingdbstub 
    167         except ImportError: 
    168             log.warning( 
    169                     'Unable to import wingdbstub, ' 
    170                     'have you copied it into place ?') 
    171     cwd = os.getcwd() 
    172     try: 
    173         os.chdir(options.directory) 
    174         # do the path dance *after* changing directory and *before* reading 
    175         # the config. 
    176         if options.insert_path: 
    177             sys.path[:1]=options.insert_path.split(os.path.sep) 
    178         if options.append_path: 
    179             sys.path.extend(options.append_path.split(os.path.sep)) 
    180         configfile={} 
    181         try: 
    182             if options.configfile: 
    183                 execfile(options.configfile, configfile) 
    184         except IOError,e: 
    185             log.warning( 
    186                 'Failed to read configuration file %s' %  
    187                 options.configfile) 
    188         conf = type('confile',(),configfile) 
    189         rcode = run(programname, options, args, conf, *runargs, **runkw) 
    190     finally: 
    191         os.chdir(cwd) 
    192     return rcode 
    193  
    194 def fmt_msg(msgkey, msgfmtmap, extrafmtargs, **fmtargs): 
    195     fmtargs.update(extrafmtargs) 
    196     msgfmt = msgfmtmap[msgkey] 
    197     if callable(msgfmt): 
    198         return msgfmt(**fmtargs) 
    199     else: 
    200         return msgfmt % fmtargs 
    201  
    202144def build_parser(optparsekwopts=None, *optlists): 
     145    """build an optparse.OptionParser from the argument option *lists*.""" 
    203146    if optparsekwopts is None: 
    204147        optparsekwopts = {} 
     
    280223    return type('options',(),defaults) 
    281224 
    282  
    283 def run_common(argv, commandmap, msgfmtmap=dict( 
    284         LIST_AVAILABLE_COMMANDS='Available commands are: %(commandnamelist)s', 
    285         ERROR_UNKNOWN_COMMAND='ERROR: Unknown command: %(commandname)s', 
    286         SHOW_USAGE='%(commandname)s:%(commandusage)s' 
    287         ),  
    288     extrafmtargs={}, 
    289     stderr=sys.stderr, stdout=sys.stdout): 
    290     if not argv: 
    291         argv=sys.argv 
    292     def show_usage(): 
    293         usage=[(command, build_parser( 
    294                 *bpargs, **bpkwargs).get_usage())  
    295                 for command, (_, build_parser, bpargs, bpkwargs, 
    296                         _1, _2, _3) in commandmap.items()] 
    297         print >> stdout, fmt_msg( 
    298             'LIST_AVAILABLE_COMMANDS', msgfmtmap, extrafmtargs, 
    299             commandnamelist =','.join(commandmap.keys())) 
    300         for c,u in usage: 
    301             print >> stdout 
    302             print >> stdout, fmt_msg('SHOW_USAGE', msgfmtmap, extrafmtargs, 
    303                 commandname=c, commandusage=u) 
    304     if (len(argv) >= 2 and argv[1].find('--help') == 0): 
    305         show_usage() 
    306         return 
    307     if len(argv)==1 or argv[1].startswith('-'): 
    308         command = sorted( 
    309             [(cvals[0], c) for c, cvals in commandmap.items()])[0][1] 
    310     else: 
    311         command=argv[1] 
    312         argv=len(argv) > 1 and argv[1:] or [] 
    313     if command not in commandmap: 
    314         print >> stderr, fmt_msg('ERROR_UNKNOWN_COMMAND', msgfmtmap, 
    315                 extrafmtargs, commandname=command) 
    316         show_usage() 
    317         return 
    318     build_parser, bpargs, bpkwargs = commandmap[command][1:4] 
    319     parser = build_parser(*bpargs, **bpkwargs) 
    320     options, args = parser.parse_args(argv[1:]) 
    321     runner, runxargs, runxkwargs = commandmap[command][4:] 
    322     return runner(parser.get_prog_name(), options, args, *runxargs, **runxkwargs) 
    323   
    324  
  • slavetools/trunk/lib/slavetools/slaveapi_posix.py

    r82 r87  
    11#!/usr/bin/env python 
    2 import signal,errno 
    3 import os, sys, optparse, logging, itertools 
    4  
    5 from optrun.cliapi import * 
     2import signal,errno, os, sys 
     3from subprocess import Popen 
     4from optionsapi import COMMON_OPTS, build_parser 
     5from slaveapi import get_program_name, get_log 
     6from slaveapi import run_withconfig, run_wrapper_withconfig 
    67 
    78__all__=( 
    8         'pidfile_name write_pidfile read_pidfile pollpid_unix kill_unix ' 
     9        'pidfile_name write_pidfile read_pidfile pollpid_posix kill_posix ' 
    910        'daemonize_posix ' 
    10         'start_unix start_unix_not_optrun stop_unix main_unix_boilerplate ' 
     11        'start_posix start_posix_noforward stop_posix main_posix_boilerplate ' 
    1112        'minimal_daemon_example ' 
    12         'COMMON_UNIX_OPTS START_UNIX_OPTS STOP_UNIX_OPTS MAIN_UNIX_OPTS ' 
     13        'COMMON_POSIX_OPTS START_POSIX_OPTS STOP_POSIX_OPTS MAIN_POSIX_OPTS ' 
    1314        #COMMANDMAP is typicaly defined by many modules so its not included in 
    1415        #all 
     
    1617 
    1718def pidfile_name(options, program=None): 
     19    """Workout the pidfile name according to slavetool conventions.""" 
    1820    pidfile = getattr(options, 'pidfile', None) 
    1921    if not pidfile: 
     22        # if pidfile is not set derive based on program name. we only 
     23        # inspect sys.argv as a last resort. typicaly the caller will 
     24        # supply a non None value for program 
    2025        if program is not None: 
    2126            argv=[program] 
    2227        else: 
    2328            argv=sys.argv 
    24         program = get_program_name(argv) 
    25         pidfile = '%s-%s.pid' % (getattr(options, 'pidfile_prefix', ''), program) 
     29        program = get_program_name(argv) # deal with file extentions 
     30        pidfile = '%s%s.pid' % (getattr(options, 'pidfile_prefix', '') or '',  
     31            '%s' % program) 
     32    else: 
     33        # if the caller specifies an absoloute path, that is *exactly* what 
     34        # the get. pidfile-prefix is *not* applied. 
     35        if not os.path.isabs(pidfile): 
     36            prefix=getattr(options, 'pidfile_prefix', None) 
     37            if prefix: 
     38                head,tail = os.path.split(pidfile) 
     39                return os.path.join(head, '%s%s' %(prefix,tail)) 
    2640    return pidfile 
    2741 
    2842def write_pidfile(pidfile, pid, log=None): 
     43    """Write a pidfile and return the absoloute path of the file written 
     44 
     45    If the write succedes return: 
     46     (abspidfile, True) 
     47    Otherwise: 
     48     (abspidfile, False) 
     49 
     50    NOTE: this function does *not* take options into account. The caller 
     51    is expected to do this. (see pidfile_name)""" 
    2952    try: 
    3053        pidf = open(pidfile, 'w+b') 
     
    3861     
    3962def read_pidfile(pidfile, defaultpid=None): 
     63    """read a pid from a file but return defaultpid if it can't be read. 
     64 
     65    If the pid is read *AND* successfully converted to an int return: 
     66     (pid, True) 
     67    Otherwise: 
     68     (defaultpid, False) 
     69    """ 
    4070    if pidfile and os.path.exists(pidfile): 
    4171        try: 
    4272            return (int(open(pidfile).read()), True) 
    43         except (IOError, ValueError), e
     73        except (IOError, ValueError)
    4474            pass 
    4575    return (defaultpid,False) 
    4676 
    47 def pollpid_unix(pid): 
     77def pollpid_posix(pid): 
    4878    """Determine if a process has terminated. 
    4979 
     
    6696    return rcode 
    6797    
    68 def kill_unix(log,  
     98def kill_posix(log,  
    6999    options,  
    70100    program=None,  
    71     pid=None,  
     101    pid=None, # If set prevents *all* attempts to read or delete pidfiles 
    72102    killsignalname='SIGINT', 
    73103    abspidfile=None, # usualy you pass pid or derive pidfile from options 
     
    79109    allwaysremovestalepidfile=True 
    80110    ): 
     111    """kill the posix process implied by the arguments. 
     112 
     113    `pid`: defaults to None, setting this disables *all* attempts to read 
     114    or delete the pidfile, irrespective of other options.  
     115 
     116    `abdpidfile`: defaults to None, setting this disables *all* attempts to 
     117    gues the pidfile name. Provided `pid` is None, this is *exactly* the file 
     118    that is read and possibly deleted. 
     119 
     120    `removepidfile`: provided that `pid` is None setting this option will cause 
     121    the derived pidfile to be deleted if it was successfully read. defaults to 
     122    False. 
     123 
     124    `allwaysremovestalepidfile`: by default, and provided `pid` is None, this 
     125    function will allways delete the pidfile it derives if: a pid was read from 
     126    that file AND the process does not exist (as determined by os.kill). 
     127    Setting this argument True will leave stale pidfiles alone. 
     128 
     129    `killsignalname`: If a pid can be derived from the arguments AND provided  
     130    that the derived pid is *not* equivelent to os.getpid() (the current  
     131    process!) an attempt will be made to kill it using the python signal module  
     132    attribute corresponding to `killsignalname` 
     133    """ 
     134 
    81135    haveremovedpidfile=False 
    82     if abspidfile and not os.path.isfile(abspidfile): 
    83         raise IOError( 
    84             'abspidfile does not exist [%s]' % abspidfile) 
    85     if abspidfile is not None: 
    86         pidfile = abspidfile 
    87     else: 
    88         pidfile = pidfile_name(options, program) 
    89     if pid is None: 
     136    explicitpid=pid 
     137    pidfile=None 
     138    if explicitpid is None: 
     139        if abspidfile and not os.path.isfile(abspidfile): 
     140            raise IOError( 
     141                'abspidfile does not exist [%s]' % abspidfile) 
     142        if abspidfile is not None: 
     143            pidfile = abspidfile 
     144        else: 
     145            pidfile = pidfile_name(options, program) 
     146    if explicitpid is None: 
    90147        # prefer file contents but fall back on options.pid 
    91148        pid,readpidfile=read_pidfile(pidfile, getattr(options, 'pid', None)) 
     
    93150        readpidfile=False 
    94151    if pid and pid != os.getpid(): # Incase the parent wrote the pidfile 
    95         if readpidfile: 
    96             log.info("killing pid [%s], read from [%s]" % (pid,pidfile)) 
     152        if readpidfile and explicitpid is None: 
     153            log.info("killing pid [%s], read from [%s]" % (pid, 
     154                os.path.abspath(pidfile))) 
    97155        else: 
    98156            log.info("killing pid [%s]" % pid) 
     
    100158            os.kill(pid, getattr(signal, killsignalname)) 
    101159        except OSError, e: 
    102             if e[0] == errno.ESRCH and readpidfile and allwaysremovestalepidfile: 
    103                 log.info("removing stale pidfile [%s], process [%s] is gone" % ( 
     160            if e[0] == errno.ESRCH: 
     161                if (explicitpid is None and readpidfile  
     162                        and allwaysremovestalepidfile): 
     163                    log.info( 
     164                        "removing stale pidfile [%s], process [%s] is gone" % ( 
    104165                        pidfile, pid)) 
    105                 os.remove(pidfile) 
    106                 haveremovedpidfile = True 
     166                    os.remove(pidfile) 
     167                    haveremovedpidfile = True 
    107168            elif e[0] == errno.ESRCH: 
    108169                log.info("process [%s] is gone" % pid) 
    109         if removepidfile and readpidfile and not haveremovedpidfile: 
     170        if (explicitpid is None and removepidfile and readpidfile  
     171                and not haveremovedpidfile): 
    110172            os.remove(pidfile) 
    111173    return pidfile 
    112174 
    113175def daemonize_posix(): 
     176    """shamelessy looted from Twisted.""" 
    114177    # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16 
    115178    if os.fork():   # launch child and... 
     
    129192    return os.getpid() # only get here if this is the grandchild. 
    130193  
    131 def start_unix(progname, options, args, conf): 
    132     from subprocess import Popen 
     194def start_posix(progname, options, args, conf): 
     195    """helper for trivial init style start/stop scripts.""" 
    133196    if not args: 
    134197        print "no command specified" 
     
    136199    log=get_log(options, progname) 
    137200    pid=None 
    138     pidfile = kill_unix(log, options, args[0]) 
     201    pidfile = kill_posix(log, options, args[0]) 
    139202    if not options.dont_forward_opts: 
    140203        ifirst = 0 
     
    155218    p = Popen(args) 
    156219    pid=p.pid 
     220    log.info("started: pid=[%s] args: %s" % (str(pid), ' '.join(args))) 
    157221    if pidfile and not options.daemonize: 
    158         write_pidfile(pidfile, pid, log) 
    159     log.info("started: %s\npid=%s,pidfile=%s" % (' '.join(args), str(pid), 
    160             pidfile)) 
    161     return 0 
    162  
    163  
    164 def start_unix_not_optrun(progname, options, args, conf): 
    165     """start a process that does not support 'optrun' main options""" 
    166     from subprocess import Popen 
     222        abspidfile,wrotepidfile = write_pidfile(pidfile, pid, log) 
     223        if wrotepidfile: 
     224            log.info("wrote pid [%s] to file: %s" % ( 
     225                str(pid), abspidfile)) 
     226        else: 
     227            log.info("failed to write pid [%s] to file: %s" % ( 
     228                str(pid), abspidfile)) 
     229    return p 
     230 
     231 
     232def start_posix_noforward(progname, options, args, conf): 
     233    """helper for trivial init style start / stop scripts.""" 
    167234    if not args: 
    168235        print "no command specified" 
     
    170237    log=get_log(options, progname) 
    171238    pid=None 
    172     pidfile = kill_unix(log, options, args[0]) 
     239    pidfile = kill_posix(log, options, args[0]) 
    173240    log.info("launching [%s] args: %s" % (args[0], ' '.join(args[1:]))) 
    174241    p = Popen(args) 
     
    178245    log.info("started: %s\npid=%s,pidfile=%s" % (' '.join(args), str(pid), 
    179246            pidfile)) 
    180     return 0 
     247    return p 
    181248  
    182 def stop_unix(progname, options, args, conf): 
     249def stop_posix(progname, options, args, conf): 
     250    """helper for trivial init style start/stop scripts.""" 
    183251    log=get_log(options, progname) 
    184     print progname 
    185252    if not args and progname is None and options.pid is None: 
    186253        print ('1st argument must be the program name, ' 
     
    191258    # prefer pidfile 
    192259    program = args and args[0] or progname 
    193     if not options.firstarg_is_not_pid: # incase the command *is* a number 
    194         try: 
    195             pid=int(program) 
    196             program=None 
    197         except ValueError: 
    198             pass 
    199         else: 
    200             options.pid=pid 
    201     kill_unix(log, options, program) 
     260    kill_posix(log, options, program) 
    202261    return 0 
    203262 
    204 def main_unix_boilerplate(progname, options, args, conf): 
     263def main_posix_boilerplate(progname, options, args, conf): 
    205264    """start/stop compatible main. 
    206265     
    207266    expected use, write your own main entry point and call this as the 
    208     first order of business 
     267    first order of business. 
    209268    """ 
     269    pidfilewritten=False 
    210270    log = get_log(options, progname) 
    211271    pidfile = pidfile_name(options, progname) 
     
    213273        pid=daemonize_posix() 
    214274        if pidfile: 
    215             write_pidfile(pidfile, pid) 
    216     #log.setLevel(logging.INFO) # do this if you want to see the log message 
     275            (abspidfile,pidfilewritten) = write_pidfile(pidfile, pid) 
    217276    log.info("process started") 
    218     return 0 
     277    if pidfilewritten: 
     278        log.info("wrote pid [%s] to file: [%s]" % (pid, abspidfile)) 
     279    elif options.daemonize: 
     280        log.warning("failed to write pid [%s] to file: [%s]" % ( 
     281            os.getpid(), abspidfile)) 
     282    # The standard logging package makes it easy for you to *not* have to pass 
     283    # logging contexts arround. However if you want your startup script to be 
     284    # agnostic to what ever program it is running writing to this log instance 
     285    # is a good way to log to the channel that will be used by the `program` 
     286    # your script launches. 
     287    return log 
     288 
     289 
     290COMMON_POSIX_OPTS=[ 
     291    ('--daemonize',dict(default=False,action='store_true', 
     292        help='Run in the background'))] 
     293  
     294START_POSIX_OPTS=[ 
     295    ('--pidfile-prefix',dict(default=None, 
     296        help='Set this option if you want to decorate the pidfile name with a' 
     297        'prefix.')), 
     298    ('--pidfile',dict(default=None,help= 
     299        'The process id will be written here. If it is an absoloute path' 
     300        'it trumps all other options affecting the pidfile location')), 
     301    ('--dont-forward-opts', dict(default=False, action='store_true', 
     302        help='For convenience, all options that are specified to the "start" ' 
     303            ' command are copied into the *begining* of the argumets passed ' 
     304            ' to the child. Set this flag to disable this behaviour.')) 
     305    ] 
     306STOP_POSIX_OPTS=[ 
     307    ('--pid',dict(default=None, 
     308        help='process id to kill, trumps pidfile options and prevents' 
     309        'all attempts to read or cleanup the pidfile',type='int')), 
     310    ('--pidfile-prefix',dict(default=None)), 
     311    ('--pidfile',dict(default=None, 
     312        help='Where you expect to find the file containing the process id.')), 
     313    ] 
     314 
     315MAIN_POSIX_OPTS=[] 
     316 
     317COMMANDMAP = dict( 
     318    main = [0, 
     319        build_parser, (dict( 
     320            usage='''usage: %prog mainopts | %prog main mainopts'''), 
     321            COMMON_OPTS, COMMON_POSIX_OPTS, START_POSIX_OPTS,  
     322            MAIN_POSIX_OPTS), {}, 
     323        run_wrapper_withconfig, (main_posix_boilerplate,), {}], 
     324    start = [1, 
     325        build_parser, (dict( 
     326            usage="""\ 
     327usage: %prog start 
     328 
     329start a posix program, seperate the subcommand from setuptool opts using '--' 
     330eg., %prog start -- echo 'trvial subprocess'"""), 
     331           COMMON_OPTS, COMMON_POSIX_OPTS, START_POSIX_OPTS), {}, 
     332       run_wrapper_withconfig, (start_posix,), {}], 
     333    stop = [1, 
     334        build_parser, (dict( 
     335            usage="""\ 
     336usage: %prog stop [programname | --pidfile=filename | --pid=NUMBER] 
     337 
     338stop a posix program"""), 
     339            COMMON_OPTS, COMMON_POSIX_OPTS, STOP_POSIX_OPTS), {}, 
     340        run_wrapper_withconfig, (stop_posix,), {}]) 
     341 
     342def run(argv=None): 
     343    run_withconfig(argv, COMMANDMAP) 
     344 
     345if __name__=='__main__': 
     346    run() 
    219347 
    220348def minimal_daemon_example(): 
    221349    """provided for exposition only.""" 
    222350    def main(progname, options, args, conf): 
    223         rcode = main_unix_boilerplate(progname,options,args,conf) 
    224         if rcode: 
    225             return rcode 
     351        main_posix_boilerplate(progname,options,args,conf) 
    226352        from time import time, sleep 
    227353        try: 
     
    232358            return 0 
    233359    COMMANDMAP['main'][-2][0]=main 
    234     run_common(None, commandmap
     360    run_withconfig(None, COMMANDMAP
    235361  
    236 COMMON