Back to index

moin  1.9.0~rc2
daemon.py
Go to the documentation of this file.
00001 """
00002 Daemon - daemon script and controller
00003 
00004 This module is based on twisted.scripts.twistd, modified by Nir Soffer
00005 to work with non Twisted code.
00006 
00007 The Daemon class, represents a background process using a pid
00008 file. When you create an instance, the process may be running or not.
00009 After creating an instance, you can call one of its do_xxx() methods.
00010 
00011 The DaemonScript class is a script that control a daemon, with a
00012 functionality similar to apachectl. To create a daemon script, create an
00013 instacne and call its run() method.
00014 
00015 Typical usage::
00016 
00017     # Daemon script
00018     import daemon
00019     import myserver
00020     script = daemon.DaemonScript('myserver', 'myserver.pid',
00021                                  myserver.run, myserver.Config)
00022     script.run()
00023 
00024 
00025 Copyright (c) 2005 Nir Soffer <nirs@freeshell.org>
00026 
00027 Twisted, the Framework of Your Internet
00028 Copyright (c) 2001-2004 Twisted Matrix Laboratories.
00029 
00030 Permission is hereby granted, free of charge, to any person obtaining
00031 a copy of this software and associated documentation files (the
00032 "Software"), to deal in the Software without restriction, including
00033 without limitation the rights to use, copy, modify, merge, publish,
00034 distribute, sublicense, and/or sell copies of the Software, and to
00035 permit persons to whom the Software is furnished to do so, subject to
00036 the following conditions:
00037 
00038 The above copyright notice and this permission notice shall be
00039 included in all copies or substantial portions of the Software.
00040 
00041 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
00042 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
00043 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
00044 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
00045 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
00046 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
00047 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
00048 """
00049 
00050 import sys, os, errno, signal, time
00051 
00052 
00053 class Error(Exception):
00054     """ Daemon error """
00055 
00056 
00057 class Daemon:
00058     """ A background process
00059 
00060     Represent a background process, which may be running or not. The
00061     process can be started, stopped, restarted or killed.
00062     """
00063     commandPrefix = 'do_'
00064 
00065     def __init__(self, name, pidfile, function, *args, **kw):
00066         """ Create a daemon
00067 
00068         @param name: name of the process
00069         @param pidfile: pid filename
00070         @param function: the server main function, will block until the
00071             server is done.
00072         @param args: arguments to pass to function
00073         @param kw: keyword arguments to pass to function
00074         """
00075         self.name = name
00076         self.function = function
00077         self.args = args
00078         self.kw = kw
00079         self.pidFile = os.path.abspath(pidfile)
00080 
00081     # --------------------------------------------------------------------
00082     # Commands
00083 
00084     def do_start(self):
00085         """ Start the daemon process
00086 
00087         Start will daemonize then block until the server is killed and
00088         then cleanup the pid file on the way out.
00089         """
00090         running, pid = self.status()
00091         if running:
00092             raise Error("another application is running with pid %s "
00093                         "(try restart)" % pid)
00094         self.daemonize()
00095         self.writePID()
00096         try:
00097             self.function(*self.args, **self.kw)
00098         finally:
00099             self.removePID()
00100 
00101     def do_stop(self):
00102         """ Stop the daemon process
00103 
00104         Terminate or raise an error we can't handle here. On success,
00105         the pid file will be cleaned by the terminated process.
00106         """
00107         running, pid = self.status()
00108         if not running:
00109             return self.log("%s is not running" % self.name)
00110         os.kill(pid, signal.SIGINT)
00111 
00112     def do_kill(self):
00113         """ Kill the daemon process
00114 
00115         Kill or raise an error which we can't handle here. Clean the
00116         pid file for the killed process.
00117         """
00118         running, pid = self.status()
00119         if not running:
00120             return self.log("%s is not running" % self.name)
00121         os.kill(pid, signal.SIGKILL)
00122         self.removePID()
00123 
00124     def do_restart(self):
00125         """ stop, wait until pid file gone and start again """
00126         running, pid = self.status()
00127         if not running:
00128             self.log("%s is not running, trying to start" % self.name)
00129         else:
00130             self.do_stop()
00131         timeoutSeconds = 2.0
00132         start = time.time()
00133         while time.time() - start < timeoutSeconds:
00134             running, pid = self.status()
00135             if not running:
00136                 break
00137             time.sleep(0.1)
00138         else:
00139             raise Error("could not start after %s seconds" % timeoutSeconds)
00140         self.do_start()
00141 
00142     # -------------------------------------------------------------------
00143     # Private
00144 
00145     def status(self):
00146         """ Return status tuple (running, pid)
00147 
00148         See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC18
00149         """
00150         running = False
00151         pid = self.readPID()
00152         if pid is not None:
00153             try:
00154                 os.kill(pid, 0)
00155                 running = True
00156             except OSError, err:
00157                 if err.errno == errno.ESRCH:
00158                     # No such process or security enhancements are causing
00159                     # the system to deny its existence.
00160                     self.log("removing stale pid file: %s" % self.pidFile)
00161                     self.removePID()
00162                 else:
00163                     raise
00164         return running, pid
00165 
00166     def readPID(self):
00167         """ Return the pid from the pid file
00168 
00169         If there is no pid file, return None. If pid file is corrupted,
00170         remove it. If its not readable, raise.
00171         """
00172         pid = None
00173         try:
00174             pid = int(file(self.pidFile).read())
00175         except IOError, err:
00176             if err.errno != errno.ENOENT:
00177                 raise
00178         except ValueError:
00179             self.warn("removing corrupted pid file: %s" % self.pidFile)
00180             self.removePID()
00181         return pid
00182 
00183     def daemonize(self):
00184         """ Make the current process a daemon
00185 
00186         See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16
00187         """
00188         if os.fork():   # launch child and...
00189             os._exit(0) # kill off parent
00190         os.setsid()
00191         if os.fork():   # launch child and...
00192             os._exit(0) # kill off parent again.
00193         os.umask(0077)
00194         null = os.open('/dev/null', os.O_RDWR)
00195         for i in range(3):
00196             try:
00197                 os.dup2(null, i)
00198             except OSError, e:
00199                 if e.errno != errno.EBADF:
00200                     raise
00201         os.close(null)
00202 
00203     def writePID(self):
00204         pid = str(os.getpid())
00205         open(self.pidFile, 'wb').write(pid)
00206 
00207     def removePID(self):
00208         try:
00209             os.remove(self.pidFile)
00210         except OSError, err:
00211             if err.errno != errno.ENOENT:
00212                 raise
00213 
00214     def warn(self, message):
00215         self.log('warning: %s' % message)
00216 
00217     def log(self, message):
00218         """ TODO: does it work after daemonize? """
00219         sys.stderr.write(message + '\n')
00220 
00221 
00222 class DaemonScript(Daemon):
00223     """ A script controlling a daemon
00224 
00225     TODO: add --pid-dir option?
00226     """
00227 
00228     def run(self):
00229         """ Check commandline arguments and run a command """
00230         args = len(sys.argv)
00231         if args == 1:
00232             self.usage('nothing to do')
00233         elif args > 2:
00234             self.usage("too many arguments")
00235         try:
00236             command = sys.argv[1]
00237             func = getattr(self, self.commandPrefix + command)
00238             func()
00239         except AttributeError:
00240             self.usage('unknown command %r' % command)
00241         except Exception, why:
00242             sys.exit("error: %s" % str(why))
00243 
00244     def usage(self, message):
00245         sys.stderr.write('error: %s\n' % message)
00246         sys.stderr.write(self.help())
00247         sys.exit(1)
00248 
00249     def help(self):
00250         return """
00251 %(name)s - MoinMoin daemon
00252 
00253 usage: %(name)s command
00254 
00255 commands:
00256   start     start the server
00257   stop      stop the server
00258   restart   stop then start the server
00259   kill      kill the server
00260 
00261 @copyright: 2004-2005 Thomas Waldmann, Nir Soffer
00262 @license: GNU GPL, see COPYING for details.
00263 """ % {
00264     'name': self.name,
00265 }
00266