blob: 9ca0c7ee5a8655d6305a7b740e5b5c773d89f051 [file] [log] [blame]
# Copyright 2016 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Daemon-related utilities."""
# TODO(kitching): Moving this to the cros.factory.utils directory once
# feature-complete. We may want to convert gooftools/wipe.py
# to use it.
# TODO(kitching): Write unittests for this module.
from __future__ import print_function
import atexit
import os
import signal
import sys
import time
CHILD = 0
PARENT = 1
class Daemon(object):
"""A generic daemon class.
Usage: subclass the Daemon class and override the run() method.
Based on Sander Marechal's public domain code sample: http://goo.gl/9tMDrh
"""
def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null',
stderr='/dev/null'):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
def _Daemonize(self):
"""Daemonizes the daemon with a UNIX double-fork.
Do the UNIX double-fork magic. See Stevens' "Advanced
Programming in the UNIX Environment" for details (ISBN 0201563177)
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
"""
try:
pid = os.fork()
if pid > 0:
# Exit first parent.
return PARENT
except OSError as e:
sys.stderr.write('fork #1 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)
# Decouple from parent environment.
os.chdir('/')
os.setsid()
os.umask(0)
# Do second fork.
try:
pid = os.fork()
if pid > 0:
# Exit from second parent.
sys.exit(0)
except OSError as e:
sys.stderr.write('fork #2 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)
# Redirect standard file descriptors.
sys.stdout.flush()
sys.stderr.flush()
si = file(self.stdin, 'r')
so = file(self.stdout, 'a+')
se = file(self.stderr, 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# Register pidfile.
self._RegisterPID()
def _RegisterPID(self):
"""Saves the PID and registers a handler to remove the file on exit."""
# Remove pidfile when exiting.
atexit.register(self._RemovePID)
# Write pidfile.
self._WritePID()
def _WritePID(self):
"""Writes the current process's PID to the pidfile."""
pid = str(os.getpid())
file(self.pidfile, 'w+').write('%s\n' % pid)
def _RemovePID(self):
"""Unlinks the pidfile."""
os.remove(self.pidfile)
def GetPID(self):
"""Returns the current PID of the daemon process, or None if not found."""
try:
with open(self.pidfile, 'r') as f:
return int(f.read().strip())
except IOError:
return None
def IsRunning(self):
"""Checks that the service is running."""
pid = self.GetPID()
if not pid:
return False
try:
os.kill(pid, 0)
except OSError:
return False
return True
def IsStopped(self):
"""Checks that the service is stopped."""
return not self.IsRunning()
def Start(self, foreground=False):
"""Starts the daemon."""
# Check for a pidfile to see if the daemon is already running.
if self.GetPID() and self.IsStopped():
# Not sure if this is the safest thing to do...
message = 'pidfile %s exists, but pid is down. Removing pidfile\n'
sys.stderr.write(message % self.pidfile)
os.remove(self.pidfile)
elif self.IsRunning():
message = 'pidfile %s already exists, and pid is running\n'
sys.stderr.write(message % self.pidfile)
sys.exit(1)
# Foreground mode: Registering the PID is sufficient.Daemonize if necessary.
if foreground:
self._RegisterPID()
# Daemon mode: Daemonize. The function will fork and return with two
# possible values: PARENT or CHILD.
# PARENT is the original process and should return to caller.
# CHILD is the daemon and should invoke self.Run.
elif self._Daemonize() == PARENT:
return
self.Run(foreground)
def Stop(self):
"""Stops the daemon."""
pid = self.GetPID()
if not pid:
message = 'pidfile %s does not exist. Daemon not running?\n'
sys.stderr.write(message % self.pidfile)
return # Not an error in a restart.
# Try killing the daemon process.
try:
while True:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
if 'No such process' in str(err):
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print(str(err))
sys.exit(1)
def Status(self):
"""Prints the status of the daemon."""
pid = self.GetPID()
if not pid:
message = 'pidfile %s does not exist. Daemon not running?\n'
sys.stderr.write(message % self.pidfile)
return
print('Running at PID %d' % pid)
def Restart(self):
"""Restarts the daemon."""
self.Stop()
self.Start()
def Run(self, foreground):
"""Runs the code that represents the daemon process.
Override this method when subclasssing Daemon. It will be called
after the process has been daemonized by Start() or Restart().
It is expected that Run will not necessarily return (daemon main loop
may be contained in its thread).
"""
raise NotImplementedError