Package flumotion :: Package common :: Module process
[hide private]

Source Code for Module flumotion.common.process

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_common_process -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3   
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007,2008,2009 Fluendo, S.L. 
  6  # Copyright (C) 2010,2011 Flumotion Services, S.A. 
  7  # All rights reserved. 
  8  # 
  9  # This file may be distributed and/or modified under the terms of 
 10  # the GNU Lesser General Public License version 2.1 as published by 
 11  # the Free Software Foundation. 
 12  # This file is distributed without any warranty; without even the implied 
 13  # warranty of merchantability or fitness for a particular purpose. 
 14  # See "LICENSE.LGPL" in the source distribution for more information. 
 15  # 
 16  # Headers in this file shall remain intact. 
 17   
 18  """utilities for interacting with processes""" 
 19   
 20  import errno 
 21  import os 
 22  import signal 
 23  import sys 
 24  import time 
 25   
 26  from flumotion.common import log 
 27  from flumotion.common.common import ensureDir 
 28  from flumotion.configure import configure 
 29   
 30  __version__ = "$Rev: 6690 $" 
 31   
 32   
33 -def startup(processType, processName, daemonize=False, daemonizeTo='/'):
34 """ 35 Prepare a process for starting, logging appropriate standarised messages. 36 First daemonizes the process, if daemonize is true. 37 38 @param processType: The process type, for example 'worker'. Used 39 as the first part of the log file and PID file names. 40 @type processType: str 41 @param processName: The service name of the process. Used to 42 disambiguate different instances of the same daemon. 43 Used as the second part of log file and PID file names. 44 @type processName: str 45 @param daemonize: whether to daemonize the current process. 46 @type daemonize: bool 47 @param daemonizeTo: The directory that the daemon should run in. 48 @type daemonizeTo: str 49 """ 50 log.info(processType, "Starting %s '%s'", processType, processName) 51 52 if daemonize: 53 _daemonizeHelper(processType, daemonizeTo, processName) 54 55 log.info(processType, "Started %s '%s'", processType, processName) 56 57 def shutdownStarted(): 58 log.info(processType, "Stopping %s '%s'", processType, processName)
59 60 def shutdownEnded(): 61 log.info(processType, "Stopped %s '%s'", processType, processName) 62 63 # import inside function so we avoid affecting startup 64 from twisted.internet import reactor 65 reactor.addSystemEventTrigger('before', 'shutdown', 66 shutdownStarted) 67 reactor.addSystemEventTrigger('after', 'shutdown', 68 shutdownEnded) 69 70
71 -def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null', 72 directory='/'):
73 ''' 74 This forks the current process into a daemon. 75 The stdin, stdout, and stderr arguments are file names that 76 will be opened and be used to replace the standard file descriptors 77 in sys.stdin, sys.stdout, and sys.stderr. 78 These arguments are optional and default to /dev/null. 79 80 The fork will switch to the given directory. 81 82 Used by external projects (ft). 83 ''' 84 # Redirect standard file descriptors. 85 si = open(stdin, 'r') 86 os.dup2(si.fileno(), sys.stdin.fileno()) 87 try: 88 log.outputToFiles(stdout, stderr) 89 except IOError, e: 90 if e.errno == errno.EACCES: 91 log.error('common', 'Permission denied writing to log file %s.', 92 e.filename) 93 94 # first fork 95 try: 96 pid = os.fork() 97 if pid > 0: 98 sys.exit(0) # exit first parent 99 except OSError, e: 100 sys.stderr.write("Failed to fork: (%d) %s\n" % (e.errno, e.strerror)) 101 sys.exit(1) 102 103 # decouple from parent environment 104 try: 105 os.chdir(directory) 106 except OSError, e: 107 from flumotion.common import errors 108 raise errors.FatalError, "Failed to change directory to %s: %s" % ( 109 directory, e.strerror) 110 os.umask(0) 111 os.setsid() 112 113 # do second fork 114 try: 115 pid = os.fork() 116 if pid > 0: 117 sys.exit(0) # exit second parent 118 except OSError, e: 119 sys.stderr.write("Failed to fork: (%d) %s\n" % (e.errno, e.strerror)) 120 sys.exit(1)
121 122 # Now I am a daemon! 123 # don't add stuff here that can fail, because from now on the program 124 # will keep running regardless of tracebacks 125 126
127 -def _daemonizeHelper(processType, daemonizeTo='/', processName=None):
128 """ 129 Daemonize a process, writing log files and PID files to conventional 130 locations. 131 132 @param processType: The process type, for example 'worker'. Used 133 as the first part of the log file and PID file names. 134 @type processType: str 135 @param daemonizeTo: The directory that the daemon should run in. 136 @type daemonizeTo: str 137 @param processName: The service name of the process. Used to 138 disambiguate different instances of the same daemon. 139 Used as the second part of log file and PID file names. 140 @type processName: str 141 """ 142 143 ensureDir(configure.logdir, "log dir") 144 ensureDir(configure.rundir, "run dir") 145 ensureDir(configure.cachedir, "cache dir") 146 ensureDir(configure.registrydir, "registry dir") 147 148 pid = getPid(processType, processName) 149 if pid: 150 raise SystemError( 151 "A %s service%s is already running with pid %d" % ( 152 processType, processName and ' named %s' % processName or '', 153 pid)) 154 155 log.debug(processType, "%s service named '%s' daemonizing", 156 processType, processName) 157 158 if processName: 159 logPath = os.path.join(configure.logdir, 160 '%s.%s.log' % (processType, processName)) 161 else: 162 logPath = os.path.join(configure.logdir, 163 '%s.log' % (processType, )) 164 log.debug(processType, 'Further logging will be done to %s', logPath) 165 166 pidFile = _acquirePidFile(processType, processName) 167 168 # here we daemonize; so we also change our pid 169 daemonize(stdout=logPath, stderr=logPath, directory=daemonizeTo) 170 171 log.debug(processType, 'Started daemon') 172 173 # from now on I should keep running until killed, whatever happens 174 path = writePidFile(processType, processName, file=pidFile) 175 log.debug(processType, 'written pid file %s', path) 176 177 # import inside function so we avoid affecting startup 178 from twisted.internet import reactor 179 180 def _deletePidFile(): 181 log.debug(processType, 'deleting pid file') 182 deletePidFile(processType, processName)
183 reactor.addSystemEventTrigger('after', 'shutdown', 184 _deletePidFile) 185 186
187 -def _getPidPath(type, name=None):
188 """ 189 Get the full path to the pid file for the given process type and name. 190 """ 191 path = os.path.join(configure.rundir, '%s.pid' % type) 192 if name: 193 path = os.path.join(configure.rundir, '%s.%s.pid' % (type, name)) 194 log.debug('common', 'getPidPath for type %s, name %r: %s' % ( 195 type, name, path)) 196 return path
197 198
199 -def writePidFile(type, name=None, file=None):
200 """ 201 Write a pid file in the run directory, using the given process type 202 and process name for the filename. 203 204 @rtype: str 205 @returns: full path to the pid file that was written 206 """ 207 # don't shadow builtin file 208 pidFile = file 209 if pidFile is None: 210 ensureDir(configure.rundir, "rundir") 211 filename = _getPidPath(type, name) 212 pidFile = open(filename, 'w') 213 else: 214 filename = pidFile.name 215 pidFile.write("%d\n" % (os.getpid(), )) 216 pidFile.close() 217 os.chmod(filename, 0644) 218 return filename
219 220
221 -def _acquirePidFile(type, name=None):
222 """ 223 Open a PID file for writing, using the given process type and 224 process name for the filename. The returned file can be then passed 225 to writePidFile after forking. 226 227 @rtype: str 228 @returns: file object, open for writing 229 """ 230 ensureDir(configure.rundir, "rundir") 231 path = _getPidPath(type, name) 232 return open(path, 'w')
233 234
235 -def deletePidFile(type, name=None, force=False):
236 """ 237 Delete the pid file in the run directory, using the given process type 238 and process name for the filename. 239 240 @param force: if errors due to the file not existing should be ignored 241 @type force: bool 242 243 @rtype: str 244 @returns: full path to the pid file that was written 245 """ 246 path = _getPidPath(type, name) 247 try: 248 os.unlink(path) 249 except OSError, e: 250 if e.errno == errno.ENOENT and force: 251 pass 252 else: 253 raise 254 return path
255 256
257 -def getPid(type, name=None):
258 """ 259 Get the pid from the pid file in the run directory, using the given 260 process type and process name for the filename. 261 262 @returns: pid of the process, or None if not running or file not found. 263 """ 264 265 pidPath = _getPidPath(type, name) 266 log.log('common', 'pidfile for %s %s is %s' % (type, name, pidPath)) 267 if not os.path.exists(pidPath): 268 return 269 270 pidFile = open(pidPath, 'r') 271 pid = pidFile.readline() 272 pidFile.close() 273 if not pid or int(pid) == 0: 274 return 275 276 return int(pid)
277 278
279 -def signalPid(pid, signum):
280 """ 281 Send the given process a signal. 282 283 @returns: whether or not the process with the given pid was running 284 """ 285 try: 286 os.kill(pid, signum) 287 return True 288 except OSError, e: 289 # see man 2 kill 290 if e.errno == errno.EPERM: 291 # exists but belongs to a different user 292 return True 293 if e.errno == errno.ESRCH: 294 # pid does not exist 295 return False 296 raise
297 298
299 -def termPid(pid):
300 """ 301 Send the given process a TERM signal. 302 303 @returns: whether or not the process with the given pid was running 304 """ 305 return signalPid(pid, signal.SIGTERM)
306 307
308 -def killPid(pid):
309 """ 310 Send the given process a KILL signal. 311 312 @returns: whether or not the process with the given pid was running 313 """ 314 return signalPid(pid, signal.SIGKILL)
315 316
317 -def checkPidRunning(pid):
318 """ 319 Check if the given pid is currently running. 320 321 @returns: whether or not a process with that pid is active. 322 """ 323 return signalPid(pid, 0)
324 325
326 -def waitPidFile(type, name=None):
327 """ 328 Wait for the given process type and name to have started and created 329 a pid file. 330 331 Return the pid. 332 """ 333 # getting it from the start avoids an unneeded time.sleep 334 pid = getPid(type, name) 335 336 while not pid: 337 time.sleep(0.1) 338 pid = getPid(type, name) 339 340 return pid
341 342
343 -def waitForTerm():
344 """ 345 Wait until we get killed by a TERM signal (from someone else). 346 """ 347 348 class Waiter: 349 350 def __init__(self): 351 self.sleeping = True 352 import signal 353 self.oldhandler = signal.signal(signal.SIGTERM, 354 self._SIGTERMHandler)
355 356 def _SIGTERMHandler(self, number, frame): 357 self.sleeping = False 358 359 def sleep(self): 360 while self.sleeping: 361 time.sleep(0.1) 362 363 waiter = Waiter() 364 waiter.sleep() 365