3 http://hathawaymix.org/Software/Sketches/daemon.py
5 Provides a framework for daemonizing a process. Features:
7 - reads the command line
9 - reads a configuration file
13 - calls root-level setup code
17 - calls user-level setup code
19 - detaches from the controlling terminal
21 - checks and writes a pidfile
30 class HelloDaemon(daemon.Daemon):
31 default_conf = '/etc/hellodaemon.conf'
36 logging.info('The daemon says hello')
39 if __name__ == '__main__':
43 Example hellodaemon.conf:
48 pidfile = ./hellodaemon.pid
49 logfile = ./hellodaemon.log
57 import logging, logging.handlers
67 """Daemon base class"""
69 default_conf = '' # override this
70 section = 'daemon' # override this
73 """Override to perform setup tasks with root privileges.
75 When this is called, logging has been initialized, but the
76 terminal has not been detached and the pid of the long-running
77 process is not yet known.
81 """Override to perform setup tasks with user privileges.
83 Like setup_root, the terminal is still attached and the pid is
84 temporary. However, the process has dropped root privileges.
90 The terminal has been detached at this point.
94 """Read the command line and either start or stop the daemon"""
96 action = self.options.action
97 self.read_basic_config()
100 elif action == 'stop':
103 raise ValueError(action)
105 def parse_options(self):
106 """Parse the command line"""
107 p = optparse.OptionParser()
108 p.add_option('--start', dest='action',
109 action='store_const', const='start', default='start',
110 help='Start the daemon (the default action)')
111 p.add_option('-s', '--stop', dest='action',
112 action='store_const', const='stop', default='start',
113 help='Stop the daemon')
114 p.add_option('-c', dest='config_filename',
115 action='store', default=self.default_conf,
116 help='Specify alternate configuration file name')
117 p.add_option('-n', '--nodaemon', dest='daemonize',
118 action='store_false', default=True,
119 help='Run in the foreground')
120 self.options, self.args = p.parse_args()
121 if not os.path.exists(self.options.config_filename):
122 p.error('configuration file not found: %s'
123 % self.options.config_filename)
125 def read_basic_config(self):
126 """Read basic options from the daemon config file"""
127 self.config_filename = self.options.config_filename
128 cp = ConfigParser.ConfigParser()
129 cp.read([self.config_filename])
130 self.config_parser = cp
133 self.uid, self.gid = get_uid_gid(cp, self.section)
134 except ValueError, e:
137 self.pidfile = cp.get(self.section, 'pidfile')
138 self.logfile = cp.get(self.section, 'logfile')
139 self.loglevel = cp.get(self.section, 'loglevel')
141 def on_sigterm(self, signalnum, frame):
142 """Handle segterm by treating as a keyboard interrupt"""
143 raise KeyboardInterrupt('SIGTERM')
145 def add_signal_handlers(self):
146 """Register the sigterm handler"""
147 signal.signal(signal.SIGTERM, self.on_sigterm)
150 """Initialize and run the daemon"""
151 # The order of the steps below is chosen carefully.
152 # - don't proceed if another instance is already running.
154 # - start handling signals
155 self.add_signal_handlers()
156 # - create log file and pid file directories if they don't exist
159 # - start_logging must come after check_pid so that two
160 # processes don't write to the same log file, but before
161 # setup_root so that work done with root privileges can be
165 # - set up with root privileges
169 # - check_pid_writable must come after set_uid in order to
170 # detect whether the daemon user can write to the pidfile
171 self.check_pid_writable()
172 # - set up with user privileges before daemonizing, so that
173 # startup failures can appear on the console
177 if self.options.daemonize:
180 logging.exception("failed to start due to an exception")
183 # - write_pid must come after daemonizing since the pid of the
184 # long running process is known only after daemonizing
187 logging.info("started")
190 except (KeyboardInterrupt, SystemExit):
193 logging.exception("stopping with an exception")
197 logging.info("stopped")
200 """Stop the running process"""
201 if self.pidfile and os.path.exists(self.pidfile):
202 pid = int(open(self.pidfile).read())
203 os.kill(pid, signal.SIGTERM)
204 # wait for a moment to see if the process dies
208 # poll the process state
211 if why[0] == errno.ESRCH:
217 sys.exit("pid %d did not die" % pid)
219 sys.exit("not running")
221 def prepare_dirs(self):
222 """Ensure the log and pid file directories exist and are writable"""
223 for fn in (self.pidfile, self.logfile):
226 parent = os.path.dirname(fn)
227 if not os.path.exists(parent):
232 """Drop root privileges"""
236 except OSError, (code, message):
237 sys.exit("can't setgid(%d): %s, %s" %
238 (self.gid, code, message))
242 except OSError, (code, message):
243 sys.exit("can't setuid(%d): %s, %s" %
244 (self.uid, code, message))
247 """Change the ownership of a file to match the daemon uid/gid"""
248 if self.uid or self.gid:
251 uid = os.stat(fn).st_uid
254 gid = os.stat(fn).st_gid
256 os.chown(fn, uid, gid)
257 except OSError, (code, message):
258 sys.exit("can't chown(%s, %d, %d): %s, %s" %
259 (repr(fn), uid, gid, code, message))
261 def start_logging(self):
262 """Configure the logging module"""
264 level = int(self.loglevel)
266 level = int(logging.getLevelName(self.loglevel.upper()))
270 handlers.append(logging.handlers.RotatingFileHandler( \
271 self.logfile, maxBytes=10000, backupCount=5))
272 self.chown(self.logfile)
273 if not self.options.daemonize:
275 handlers.append(logging.StreamHandler())
277 log = logging.getLogger()
280 h.setFormatter(logging.Formatter(
281 "%(asctime)s %(process)d %(levelname)s %(message)s"))
285 """Check the pid file.
287 Stop using sys.exit() if another instance is already running.
288 If the pid file exists but no other instance is running,
293 # based on twisted/scripts/twistd.py
294 if os.path.exists(self.pidfile):
296 pid = int(open(self.pidfile).read().strip())
298 msg = 'pidfile %s contains a non-integer value' % self.pidfile
302 except OSError, (code, text):
303 if code == errno.ESRCH:
304 # The pid doesn't exist, so remove the stale pidfile.
305 os.remove(self.pidfile)
307 msg = ("failed to check status of process %s "
308 "from pidfile %s: %s" % (pid, self.pidfile, text))
311 msg = ('another instance seems to be running (pid %s), '
315 def check_pid_writable(self):
316 """Verify the user has access to write to the pid file.
318 Note that the eventual process ID isn't known until after
319 daemonize(), so it's not possible to write the PID here.
323 if os.path.exists(self.pidfile):
326 check = os.path.dirname(self.pidfile)
327 if not os.access(check, os.W_OK):
328 msg = 'unable to write to pidfile %s' % self.pidfile
332 """Write to the pid file"""
334 open(self.pidfile, 'wb').write(str(os.getpid()))
336 def remove_pid(self):
337 """Delete the pid file"""
338 if self.pidfile and os.path.exists(self.pidfile):
339 os.remove(self.pidfile)
342 def get_uid_gid(cp, section):
343 """Get a numeric uid/gid from a configuration file.
345 May return an empty uid and gid.
347 uid = cp.get(section, 'uid')
352 # convert user name to uid
354 uid = pwd.getpwnam(uid)[2]
356 raise ValueError("user is not in password database: %s" % uid)
358 gid = cp.get(section, 'gid')
363 # convert group name to gid
365 gid = grp.getgrnam(gid)[2]
367 raise ValueError("group is not in group database: %s" % gid)
371 class PrintLogger(object):
373 This class by Peter Parente, for the Jambu project
374 http://www.oatsoft.org/trac/jambu/browser/trunk/JambuLog.py?rev=1
376 @author: Peter Parente
377 @organization: IBM Corporation
378 @copyright: Copyright (c) 2005, 2007 IBM Corporation
379 @license: The BSD License
381 All rights reserved. This program and the accompanying materials are made
382 available under the terms of the BSD license which accompanies
383 this distribution, and is available at
384 U{http://www.opensource.org/licenses/bsd-license.php}
386 Provides a dirt-simple interface compatible with stdout and stderr. When
387 assigned to sys.stdout or sys.stderr, an instance of this class redirects
388 print statements to the logging system. This means the result of the
389 print statements can be silenced, sent to a file, etc. using the command
390 line options to LSR. The log level used is defined by the L{LEVEL} constant
393 @cvar LEVEL: Logging level for writes directed through this class, above
396 @ivar log: Reference to the Print log channel
397 @type log: logging.Logger
399 With minor adjustments by WTK
403 def __init__(self, logger=None):
408 self.log = logging.getLogger('print')
414 def write(self, data):
416 Write the given data at the debug level to the logger. Stores chunks of
417 text until a new line is encountered, then sends to the logger.
419 @param data: Any object that can be converted to a string
420 @type data: stringable
422 s = data.encode('utf-8')
424 self.chunks.append(s[:-1])
425 s = ''.join(self.chunks)
426 self.log.log(self.LEVEL, s)
429 self.chunks.append(s)
432 """Detach from the terminal and continue as a daemon.
433 Added support for redirecting stdout & stderr to logs.
435 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
437 # swiped from twisted/scripts/twistd.py
438 # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16
439 if os.fork(): # launch child and...
440 os._exit(0) # kill off parent
442 # some people os.chdir('/') here so they don't hog the start directory.
443 if os.fork(): # launch child and...
444 os._exit(0) # kill off parent again.
446 # flush any pending output
451 # hook up to /dev/null
452 null=os.open('/dev/null', os.O_RDWR)
457 if e.errno != errno.EBADF:
460 # I'd like to hook up to the logfiles instead, but they don't have filenos...
461 #os.dup2(out_log.fileno(), sys.stdout.fileno())
462 #os.dup2(err_log.fileno(), sys.stderr.fileno())
463 #os.dup2(dev_null.fileno(), sys.stdin.fileno())
465 # I can hook up sys.stdin and sys.stderr to the logfiles
466 log = logging.getLogger()
467 #sys.stdout = PrintLogger(log) # let web.py's stdout through...
468 sys.stderr = PrintLogger(log)