Skip to content

Instantly share code, notes, and snippets.

@paulsmith
Created November 19, 2010 01:22
Show Gist options
  • Select an option

  • Save paulsmith/705988 to your computer and use it in GitHub Desktop.

Select an option

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.
"""
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