Changeset 87
- Timestamp:
- 06/30/06 20:24:59 (3 years ago)
- Files:
-
- slavetools/trunk/lib/slavetools/entrypoints.py (added)
- slavetools/trunk/lib/slavetools/noseplug.py (modified) (12 diffs)
- slavetools/trunk/lib/slavetools/optionsapi.py (moved) (moved from slavetools/trunk/lib/slavetools/cliapi.py) (7 diffs)
- slavetools/trunk/lib/slavetools/slaveapi.py (added)
- slavetools/trunk/lib/slavetools/slaveapi_posix.py (moved) (moved from slavetools/trunk/lib/slavetools/clitool.py) (15 diffs)
- slavetools/trunk/setup.py (modified) (2 diffs)
- slavetools/trunk/slavetools.rst (moved) (moved from slavetools/trunk/optrun.rst) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
slavetools/trunk/lib/slavetools/noseplug.py
r82 r87 6 6 protocol in unsupported ways. 7 7 8 Overrides `nose.plugins.base.Plugin.begin` to trigger service start/restart. 9 overrides`finalize` to trigger service shutdown.10 8 This plugin implements `nose.plugins.base.Plugin.begin` to trigger service 9 start/restart and `finalize` to trigger service shutdown. 10 11 11 This plugin does not attempt to introspect or otherwise monkey with 12 12 sys.argv. It is driven entirely by: the options passed in by nose; the … … 14 14 there corresponding default values; 15 15 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 class18 attribute OPT_PREFIX. command line help automaticaly substitutes the default19 for the value of OPT_PREFIX when the help is generated.20 21 16 In `begin` (ie., before any tests run), this plugin attempt to launch the 22 17 slave. 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. 18 forward any `--slave-` decorated options, in undecorated form, to the slave if 19 they are set to non default values. If your slave does not support a particular 20 option offered by this plugin, then never pass it to nose and it will *never* 21 get passed to your slave. 22 23 .. _`process identifying options`: 24 25 ``--slave-program``, ``--slave-directory``, ``--slave-pidfile``, 26 ``--slave-prevpid`` 27 28 Before launching your process this plugin attempts to read a process id from 29 the file inferred by the `process identifying options`_. If it is successfull 30 it attempts to kill of that process before continuing. 31 32 A short delay is imposed in `begin` to allow your slave time to start. After 33 this (configurable) delay this plugin checks to see if the process it launched 34 is still alive. If it is alive then the pid is cached for finalize and the 35 plugin returns control to `nose`. If it has terminated this plugin assumes that 36 it was a daemon or some other indirect startup script. It then (again) attempts 37 to read a pid from the file infered by the the `process identifying options`_. 38 If a pid was successfully read the plugin performs a second check, this time 39 using the pid it read from file. If it is still not alive the plugin logs a 40 warnign and returns control to `nose`. 41 42 In all cases: If the plugin finds a 'live' pid after the warm up delay that is 43 still alive then it caches it for use in finalize and immediately returns 44 control to `nose`. If it does not find a live pid after the warmup period it 45 will not attempt to kill off anything in finalize. 46 47 In all cases: The *first* thing that happens in `begin` is an attempt to kill 48 based on the `process identifying options`_ 30 49 31 50 python's standard `subprocess.Popen` is used to launch the process. The … … 38 57 plugin. 39 58 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 59 By default all options defined by this plugin are prefixed with `--slave-`. 60 This can be changed by derving a from SlaveService and modfying the class 61 attribute OPT_PREFIX. command line help automaticaly substitutes the default 62 for the value of OPT_PREFIX when the help is generated. 63 46 64 NOTE:setuptools test - If you are running tests through setuptools, 47 65 `finalize` does not get called, however the next test run will attempt to … … 54 72 from subprocess import Popen 55 73 from nose.plugins import Plugin 56 from optrun.cliapi import * 57 from optrun.clitool import * 74 from 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 78 from slavetools.slaveapi_posix import read_pidfile, pidfile_name 79 from slavetools.slaveapi_posix import kill_posix, pollpid_posix 58 80 59 81 PRUNE_SLAVE_LONGOPTS=['--dont-forward-opts'] … … 63 85 help='By default no slave program is invoked. Set this option ' 64 86 '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')), 72 102 ('--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 ' 74 104 'simplistic (and only supported) splitting technique')), 75 105 ('--slave-args', dict( … … 81 111 'command line is `\' \'.join([SLAVE_PROGRAM, SLAVE_ARGS])`.')), 82 112 ('--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')), 97 114 ('--slave-more-help', dict(default=False, action='store_true')) 98 115 ] … … 102 119 USAGE="""\ 103 120 This 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`, 107 122 108 123 In all cases The following options are *NEVER* forwarded: 109 124 %(NEVER_FORWARDS)s 110 125 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). 126 nosetest -w is not subverted in any way so your current working directory 127 need not be your testsuite location. 115 128 116 129 nosetest --slave-more-help will print a much more detailed description of … … 123 136 OPT_PREFIX='slave-' 124 137 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 126 157 def help(self): 127 158 # used for help generation only, changing this does *not* effect … … 130 161 self.usage.replace('--slave-','--%s' % self.OPT_PREFIX)) 131 162 132 SLAVE_OPTIONS=list(prune_shortopts(reduce(update_options, [133 COMMON_OPTS, COMMON_UNIX_OPTS, START_UNIX_OPTS])))134 163 135 164 # END user customizable class parameters … … 181 210 self.slave_args=self._getprefixedattr(options, 'args') 182 211 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 185 219 if not self.slave_program: 186 220 self.enabled=False 187 221 188 222 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 ) 192 226 193 227 def prepare_slave_program(self): … … 259 293 (typical) idiom of read pid, kill if read, write pid then that child 260 294 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()`` 290 298 """ 291 wrotepidfile = False 299 readpidfile = False 300 pidfile = None 292 301 abspidfile = None 293 302 log = logging.getLogger(self.LOGCHAN) 294 options =self.slave_known_options295 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() 297 306 try: 298 307 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 303 326 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: 324 351 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 326 355 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 345 357 finally: 346 358 os.chdir(cwd) … … 348 360 def finalize(self, result): 349 361 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 3 3 """ 4 4 __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 ' 7 7 'prune_shortopts prefix_longopts ' 8 'build_parser run_wrapper run_common '9 8 ).split() 10 9 11 import sys, os, logging, optparse, types10 import optparse 12 11 13 12 COMMON_OPTS=[ … … 27 26 default='', 28 27 help='Read a python syntax configuration file by importing it.')), 29 ('--wingdbstub', dict(28 ('--wingdbstub', dict( 30 29 action='store_true', 31 30 default=False, help= … … 47 46 48 47 def add_options(parser, options): 48 """add a list of options to an optparse.OptionParser compatible thing.""" 49 49 for opt in options: 50 50 if not isinstance(opt[-1], dict): … … 53 53 54 54 def 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 """ 55 65 for opt in options: 56 66 if opt[0].startswith('--'): … … 98 108 both lists any items after the first dict are ignored (and will not be 99 109 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 101 115 """ 102 116 optmaps=[] … … 120 134 121 135 def add_options_common(parser, 122 options=COMMON_OPTS # intentionaly *not* copying the list136 options=COMMON_OPTS[:] 123 137 ): 138 """trivial helper, 139 140 calls `add_options` with the options defined as COMMON 141 by this module""" 124 142 add_options(parser, options) 125 143 126 def get_program_name(argv=None):127 if argv is None:128 argv=sys.argv129 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 allready136 # configured logging or called any of the logging module top level loging137 # functions138 logging.basicConfig()139 return log140 141 def run_wrapper(programname, options, args, run, *runargs, **runkw):142 """handles the common options143 144 including set and restore of working directory.145 146 exists mainly to make it easy to do `os.chdri(cwd)` after run completes147 """148 149 rcode = -1150 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 show159 # this message160 log.warn("Unable to set loglevel %s" % str(options.loglevel))161 lvl = None162 if lvl is not None:163 log.setLevel(lvl)164 if options.wingdbstub:165 try:166 import wingdbstub167 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* reading175 # 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 rcode193 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 % fmtargs201 202 144 def build_parser(optparsekwopts=None, *optlists): 145 """build an optparse.OptionParser from the argument option *lists*.""" 203 146 if optparsekwopts is None: 204 147 optparsekwopts = {} … … 280 223 return type('options',(),defaults) 281 224 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.argv292 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 >> stdout302 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 return307 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 return318 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 1 1 #!/usr/bin/env python 2 import signal,errno 3 import os, sys, optparse, logging, itertools 4 5 from optrun.cliapi import * 2 import signal,errno, os, sys 3 from subprocess import Popen 4 from optionsapi import COMMON_OPTS, build_parser 5 from slaveapi import get_program_name, get_log 6 from slaveapi import run_withconfig, run_wrapper_withconfig 6 7 7 8 __all__=( 8 'pidfile_name write_pidfile read_pidfile pollpid_ unix kill_unix '9 'pidfile_name write_pidfile read_pidfile pollpid_posix kill_posix ' 9 10 '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 ' 11 12 '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 ' 13 14 #COMMANDMAP is typicaly defined by many modules so its not included in 14 15 #all … … 16 17 17 18 def pidfile_name(options, program=None): 19 """Workout the pidfile name according to slavetool conventions.""" 18 20 pidfile = getattr(options, 'pidfile', None) 19 21 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 20 25 if program is not None: 21 26 argv=[program] 22 27 else: 23 28 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)) 26 40 return pidfile 27 41 28 42 def 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)""" 29 52 try: 30 53 pidf = open(pidfile, 'w+b') … … 38 61 39 62 def 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 """ 40 70 if pidfile and os.path.exists(pidfile): 41 71 try: 42 72 return (int(open(pidfile).read()), True) 43 except (IOError, ValueError) , e:73 except (IOError, ValueError): 44 74 pass 45 75 return (defaultpid,False) 46 76 47 def pollpid_ unix(pid):77 def pollpid_posix(pid): 48 78 """Determine if a process has terminated. 49 79 … … 66 96 return rcode 67 97 68 def kill_ unix(log,98 def kill_posix(log, 69 99 options, 70 100 program=None, 71 pid=None, 101 pid=None, # If set prevents *all* attempts to read or delete pidfiles 72 102 killsignalname='SIGINT', 73 103 abspidfile=None, # usualy you pass pid or derive pidfile from options … … 79 109 allwaysremovestalepidfile=True 80 110 ): 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 81 135 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: 90 147 # prefer file contents but fall back on options.pid 91 148 pid,readpidfile=read_pidfile(pidfile, getattr(options, 'pid', None)) … … 93 150 readpidfile=False 94 151 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))) 97 155 else: 98 156 log.info("killing pid [%s]" % pid) … … 100 158 os.kill(pid, getattr(signal, killsignalname)) 101 159 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" % ( 104 165 pidfile, pid)) 105 os.remove(pidfile)106 haveremovedpidfile = True166 os.remove(pidfile) 167 haveremovedpidfile = True 107 168 elif e[0] == errno.ESRCH: 108 169 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): 110 172 os.remove(pidfile) 111 173 return pidfile 112 174 113 175 def daemonize_posix(): 176 """shamelessy looted from Twisted.""" 114 177 # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16 115 178 if os.fork(): # launch child and... … … 129 192 return os.getpid() # only get here if this is the grandchild. 130 193 131 def start_ unix(progname, options, args, conf):132 from subprocess import Popen194 def start_posix(progname, options, args, conf): 195 """helper for trivial init style start/stop scripts.""" 133 196 if not args: 134 197 print "no command specified" … … 136 199 log=get_log(options, progname) 137 200 pid=None 138 pidfile = kill_ unix(log, options, args[0])201 pidfile = kill_posix(log, options, args[0]) 139 202 if not options.dont_forward_opts: 140 203 ifirst = 0 … … 155 218 p = Popen(args) 156 219 pid=p.pid 220 log.info("started: pid=[%s] args: %s" % (str(pid), ' '.join(args))) 157 221 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 232 def start_posix_noforward(progname, options, args, conf): 233 """helper for trivial init style start / stop scripts.""" 167 234 if not args: 168 235 print "no command specified" … … 170 237 log=get_log(options, progname) 171 238 pid=None 172 pidfile = kill_ unix(log, options, args[0])239 pidfile = kill_posix(log, options, args[0]) 173 240 log.info("launching [%s] args: %s" % (args[0], ' '.join(args[1:]))) 174 241 p = Popen(args) … … 178 245 log.info("started: %s\npid=%s,pidfile=%s" % (' '.join(args), str(pid), 179 246 pidfile)) 180 return 0247 return p 181 248 182 def stop_unix(progname, options, args, conf): 249 def stop_posix(progname, options, args, conf): 250 """helper for trivial init style start/stop scripts.""" 183 251 log=get_log(options, progname) 184 print progname185 252 if not args and progname is None and options.pid is None: 186 253 print ('1st argument must be the program name, ' … … 191 258 # prefer pidfile 192 259 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) 202 261 return 0 203 262 204 def main_ unix_boilerplate(progname, options, args, conf):263 def main_posix_boilerplate(progname, options, args, conf): 205 264 """start/stop compatible main. 206 265 207 266 expected use, write your own main entry point and call this as the 208 first order of business 267 first order of business. 209 268 """ 269 pidfilewritten=False 210 270 log = get_log(options, progname) 211 271 pidfile = pidfile_name(options, progname) … … 213 273 pid=daemonize_posix() 214 274 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) 217 276 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 290 COMMON_POSIX_OPTS=[ 291 ('--daemonize',dict(default=False,action='store_true', 292 help='Run in the background'))] 293 294 START_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 ] 306 STOP_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 315 MAIN_POSIX_OPTS=[] 316 317 COMMANDMAP = 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="""\ 327 usage: %prog start 328 329 start a posix program, seperate the subcommand from setuptool opts using '--' 330 eg., %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="""\ 336 usage: %prog stop [programname | --pidfile=filename | --pid=NUMBER] 337 338 stop a posix program"""), 339 COMMON_OPTS, COMMON_POSIX_OPTS, STOP_POSIX_OPTS), {}, 340 run_wrapper_withconfig, (stop_posix,), {}]) 341 342 def run(argv=None): 343 run_withconfig(argv, COMMANDMAP) 344 345 if __name__=='__main__': 346 run() 219 347 220 348 def minimal_daemon_example(): 221 349 """provided for exposition only.""" 222 350 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) 226 352 from time import time, sleep 227 353 try: … … 232 358 return 0 233 359 COMMANDMAP['main'][-2][0]=main 234 run_ common(None, commandmap)360 run_withconfig(None, COMMANDMAP) 235 361 236 COMMON