Created
November 19, 2010 01:22
-
-
Save paulsmith/705988 to your computer and use it in GitHub Desktop.
Library functions for daemonizing in the standard POSIX way, per Stevens et. al.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Helpful utilities for running a standard POSIX daemon. | |
| - daemonize() - creates a proper POSIX daemon | |
| - already_running() - ensures a single instance of the daemon | |
| - get_logger() - sets up logging to syslog for the daemon | |
| - drop_privileges() - change user if started with superuser | |
| Reference: Advanced Programming in the UNIX Environment, 2nd Ed., | |
| Stevens, pg. 426, 432, 454. | |
| """ | |
| import errno | |
| import fcntl | |
| import grp | |
| import logging, logging.handlers | |
| import os | |
| import pwd | |
| import resource | |
| import signal | |
| import sys | |
| def daemonize(): | |
| """ | |
| Make a daemon process the standard POSIX way. | |
| """ | |
| # Bail if already a daemon | |
| if os.getppid() == 1: | |
| return | |
| # Clear the file creation mask | |
| os.umask(0) | |
| # Get the maximum number of file descriptors | |
| nfiles = resource.getrlimit(resource.RLIMIT_NOFILE)[1] | |
| if nfiles == resource.RLIM_INFINITY: | |
| nfiles = 1024 | |
| # Become a session leader to lose controlling TTY | |
| try: | |
| if os.fork() > 0: | |
| sys.exit(0) | |
| except OSError, e: | |
| print >> sys.stderr, "Couldn't fork(): %s" % e.strerror | |
| sys.exit(1) | |
| os.setsid() | |
| # Ensure future opens won't allocate controlling TTYs | |
| signal.signal(signal.SIGHUP, signal.SIG_IGN) | |
| try: | |
| if os.fork() > 0: | |
| sys.exit(0) | |
| except OSError, e: | |
| print >> sys.stderr, "Couldn't fork() in child: %s" % e.strerror | |
| sys.exit(1) | |
| # Change current working dir to root so we won't prevent file | |
| # systems from being unmounted | |
| os.chdir('/') | |
| # Flush any standard I/O buffers before we close them | |
| sys.stdout.flush() | |
| sys.stderr.flush() | |
| # Close all open file descriptors | |
| for i in xrange(nfiles): | |
| try: | |
| os.close(i) | |
| except OSError: | |
| pass | |
| # Attach file descriptors 0, 1, and 2 to /dev/null | |
| fd0 = os.open('/dev/null', os.O_RDWR) | |
| fd1 = os.dup(fd0) | |
| fd2 = os.dup(fd0) | |
| # Ensure that stdin, stdout, and stderr all point to /dev/null | |
| if (fd0 != 0 and fd1 != 1 and fd2 != 2): | |
| sys.exit(1) | |
| def already_running(filename): | |
| """ | |
| Returns True if an instance of the daemon is already running. | |
| """ | |
| # Creates a file with the PID of the daemon the first time it is | |
| # called, and obtains a write-lock on the file. Subsequent calls | |
| # of this function in other processes will fail to obtain the | |
| # lock, so long as the initial daemon process still exists. | |
| fd = os.open(filename, os.O_RDWR | os.O_CREAT, 0644) | |
| try: | |
| fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) | |
| except IOError, e: | |
| if e.errno in (errno.EAGAIN, errno.EACCES): | |
| os.close(fd) | |
| return True | |
| raise | |
| os.ftruncate(fd, 0) | |
| os.write(fd, str(os.getpid())) | |
| LOG_LEVELS = { | |
| 'critical': logging.CRITICAL, | |
| 'error': logging.ERROR, | |
| 'warning': logging.WARNING, | |
| 'info': logging.INFO, | |
| 'debug': logging.DEBUG | |
| } | |
| def get_logger(name, level='info'): | |
| """ | |
| Returns a logger handled by the syslog, suitable for use in daemon processes. | |
| """ | |
| handler = logging.handlers.SysLogHandler(address='/dev/log') | |
| logger = logging.getLogger(name) | |
| logger.setLevel(LOG_LEVELS[level]) | |
| handler.setFormatter(logging.Formatter(r'%(name)s[%(process)d] %(levelname)s: %(message)s')) | |
| logger.addHandler(handler) | |
| return logger | |
| def drop_privileges(user, group=None): | |
| """ | |
| Lower privileges once superuser privileges have been used. | |
| For example, a daemon may need access to a reserved port, or to | |
| write to a root-owned directory, so needs to be started as root, | |
| but then for the main functioning of the daemon process, it is | |
| safer to run as a different, less-privileged user. | |
| """ | |
| # Must be superuser to setuid(), so just bail early otherwise | |
| if os.getuid() != 0: | |
| return False | |
| if group: | |
| grentry = grp.getgrnam(group) | |
| new_gid = grentry[2] | |
| os.setgid(new_gid) | |
| entry = pwd.getpwnam(user) | |
| new_uid = entry[2] | |
| # Don't change user to another privileged user | |
| if new_uid == 0: | |
| raise RuntimeError('user %s is privileged' % user) | |
| os.setuid(new_uid) | |
| if __name__ == '__main__': | |
| # Example for using the utilities provided here | |
| import sys, time | |
| daemonize() | |
| logger = get_logger('my.daemontest', 'debug') | |
| logger.info('created a test daemon') | |
| if (already_running('/tmp/daemontest.pid')): | |
| logger.info('already running, exiting') | |
| sys.exit(1) | |
| else: | |
| logger.info('going to sleep') | |
| time.sleep(60) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment