0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003# @@: This should be moved to paste.deploy
0004# For discussion of daemonizing:
0005#   http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
0006# Code taken also from QP:
0007#   http://www.mems-exchange.org/software/qp/
0008#   From lib/site.py
0009import re
0010import os
0011import errno
0012import signal
0013import sys
0014import time
0015try:
0016    import subprocess
0017except ImportError:
0018    from paste.util import subprocess24 as subprocess
0019from command import Command, BadCommand
0020from paste.deploy import loadapp, loadserver
0021import threading
0022import atexit
0023import logging
0024import ConfigParser
0025
0026MAXFD = 1024
0027
0028class DaemonizeException(Exception):
0029    pass
0030
0031
0032class ServeCommand(Command):
0033
0034    min_args = 0
0035    usage = 'CONFIG_FILE [start|stop|restart|status] [var=value]'
0036    takes_config_file = 1
0037    summary = "Serve the described application"
0038    description = """\
0039    This command serves a web application that uses a paste.deploy
0040    configuration file for the server and application.  
0041    
0042    If start/stop/restart is given, then --daemon is implied, and it will
0043    start (normal operation), stop (--stop-daemon), or do both.
0044
0045    You can also include variable assignments like 'http_port=8080'
0046    and then use %(http_port)s in your config files.
0047    """
0048
0049    # used by subclasses that configure apps and servers differently
0050    requires_config_file = True
0051
0052    parser = Command.standard_parser(quiet=True)
0053    parser.add_option('-n', '--app-name',
0054                      dest='app_name',
0055                      metavar='NAME',
0056                      help="Load the named application (default main)")
0057    parser.add_option('-s', '--server',
0058                      dest='server',
0059                      metavar='SERVER_TYPE',
0060                      help="Use the named server.")
0061    parser.add_option('--server-name',
0062                      dest='server_name',
0063                      metavar='SECTION_NAME',
0064                      help="Use the named server as defined in the configuration file (default: main)")
0065    if hasattr(os, 'fork'):
0066        parser.add_option('--daemon',
0067                          dest="daemon",
0068                          action="store_true",
0069                          help="Run in daemon (background) mode")
0070    parser.add_option('--pid-file',
0071                      dest='pid_file',
0072                      metavar='FILENAME',
0073                      help="Save PID to file (default to paster.pid if running in daemon mode)")
0074    parser.add_option('--log-file',
0075                      dest='log_file',
0076                      metavar='LOG_FILE',
0077                      help="Save output to the given log file (redirects stdout)")
0078    parser.add_option('--reload',
0079                      dest='reload',
0080                      action='store_true',
0081                      help="Use auto-restart file monitor")
0082    parser.add_option('--reload-interval',
0083                      dest='reload_interval',
0084                      default=1,
0085                      help="Seconds between checking files (low number can cause significant CPU usage)")
0086    parser.add_option('--monitor-restart',
0087                      dest='monitor_restart',
0088                      action='store_true',
0089                      help="Auto-restart server if it dies")
0090    parser.add_option('--status',
0091                      action='store_true',
0092                      dest='show_status',
0093                      help="Show the status of the (presumably daemonized) server")
0094
0095
0096    if hasattr(os, 'setuid'):
0097        # I don't think these are available on Windows
0098        parser.add_option('--user',
0099                          dest='set_user',
0100                          metavar="USERNAME",
0101                          help="Set the user (usually only possible when run as root)")
0102        parser.add_option('--group',
0103                          dest='set_group',
0104                          metavar="GROUP",
0105                          help="Set the group (usually only possible when run as root)")
0106
0107    parser.add_option('--stop-daemon',
0108                      dest='stop_daemon',
0109                      action='store_true',
0110                      help='Stop a daemonized server (given a PID file, or default paster.pid file)')
0111
0112
0113    _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
0114
0115    default_verbosity = 1
0116
0117    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
0118    _monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN'
0119
0120    possible_subcommands = ('start', 'stop', 'restart', 'status')
0121    def command(self):
0122        if self.options.stop_daemon:
0123            return self.stop_daemon()
0124
0125        if not hasattr(self.options, 'set_user'):
0126            # Windows case:
0127            self.options.set_user = self.options.set_group = None
0128        # @@: Is this the right stage to set the user at?
0129        self.change_user_group(
0130            self.options.set_user, self.options.set_group)
0131
0132        if self.requires_config_file:
0133            if not self.args:
0134                raise BadCommand('You must give a config file')
0135            app_spec = self.args[0]
0136            if (len(self.args) > 1
0137                and self.args[1] in self.possible_subcommands):
0138                cmd = self.args[1]
0139                restvars = self.args[2:]
0140            else:
0141                cmd = None
0142                restvars = self.args[1:]
0143        else:
0144            app_spec = ""
0145            if (self.args
0146                and self.args[0] in self.possible_subcommands):
0147                cmd = self.args[0]
0148                restvars = self.args[1:]
0149            else:
0150                cmd = None
0151                restvars = self.args[:]
0152
0153        if self.options.reload:
0154            if os.environ.get(self._reloader_environ_key):
0155                from paste import reloader
0156                if self.verbose > 1:
0157                    print 'Running reloading file monitor'
0158                reloader.install(int(self.options.reload_interval))
0159                if self.requires_config_file:
0160                    reloader.watch_file(self.args[0])
0161            else:
0162                return self.restart_with_reloader()
0163
0164        if cmd not in (None, 'start', 'stop', 'restart', 'status'):
0165            raise BadCommand(
0166                'Error: must give start|stop|restart (not %s)' % cmd)
0167
0168        if cmd == 'status' or self.options.show_status:
0169            return self.show_status()
0170
0171        if cmd == 'restart' or cmd == 'stop':
0172            result = self.stop_daemon()
0173            if result:
0174                if cmd == 'restart':
0175                    print "Could not stop daemon; aborting"
0176                else:
0177                    print "Could not stop daemon"
0178                return result
0179            if cmd == 'stop':
0180                return result
0181
0182        app_name = self.options.app_name
0183        vars = self.parse_vars(restvars)
0184        if not self._scheme_re.search(app_spec):
0185            app_spec = 'config:' + app_spec
0186        server_name = self.options.server_name
0187        if self.options.server:
0188            server_spec = 'egg:PasteScript'
0189            assert server_name is None
0190            server_name = self.options.server
0191        else:
0192            server_spec = app_spec
0193        base = os.getcwd()
0194
0195        if getattr(self.options, 'daemon', False):
0196            if not self.options.pid_file:
0197                self.options.pid_file = 'paster.pid'
0198            if not self.options.log_file:
0199                self.options.log_file = 'paster.log'
0200
0201        # Ensure the log file is writeable
0202        if self.options.log_file:
0203            try:
0204                writeable_log_file = open(self.options.log_file, 'a')
0205            except IOError, ioe:
0206                msg = 'Error: Unable to write to log file: %s' % ioe
0207                raise BadCommand(msg)
0208            writeable_log_file.close()
0209
0210        # Ensure the pid file is writeable
0211        if self.options.pid_file:
0212            try:
0213                writeable_pid_file = open(self.options.pid_file, 'a')
0214            except IOError, ioe:
0215                msg = 'Error: Unable to write to pid file: %s' % ioe
0216                raise BadCommand(msg)
0217            writeable_pid_file.close()
0218
0219        if getattr(self.options, 'daemon', False):
0220            try:
0221                self.daemonize()
0222            except DaemonizeException, ex:
0223                if self.verbose > 0:
0224                    print str(ex)
0225                return
0226
0227        if (self.options.monitor_restart
0228            and not os.environ.get(self._monitor_environ_key)):
0229            return self.restart_with_monitor()
0230
0231        if self.options.pid_file:
0232            self.record_pid(self.options.pid_file)
0233
0234        if self.options.log_file:
0235            stdout_log = LazyWriter(self.options.log_file, 'a')
0236            sys.stdout = stdout_log
0237            sys.stderr = stdout_log
0238            logging.basicConfig(stream=stdout_log)
0239
0240        log_fn = app_spec
0241        if log_fn.startswith('config:'):
0242            log_fn = app_spec[len('config:'):]
0243        elif log_fn.startswith('egg:'):
0244            log_fn = None
0245        if log_fn:
0246            log_fn = os.path.join(base, log_fn)
0247            self.logging_file_config(log_fn)
0248
0249        server = self.loadserver(server_spec, name=server_name,
0250                                 relative_to=base, global_conf=vars)
0251        app = self.loadapp(app_spec, name=app_name,
0252                           relative_to=base, global_conf=vars)
0253
0254        if self.verbose > 0:
0255            print 'Starting server in PID %i.' % os.getpid()
0256        try:
0257            server(app)
0258        except (SystemExit, KeyboardInterrupt), e:
0259            if self.verbose > 1:
0260                raise
0261            if str(e):
0262                msg = ' '+str(e)
0263            else:
0264                msg = ''
0265            print 'Exiting%s (-v to see traceback)' % msg
0266
0267    def loadserver(self, server_spec, name, relative_to, **kw):
0268            return loadserver(
0269                server_spec, name=name,
0270                relative_to=relative_to, **kw)
0271
0272    def loadapp(self, app_spec, name, relative_to, **kw):
0273            return loadapp(
0274                app_spec, name=name, relative_to=relative_to,
0275                **kw)
0276
0277    def daemonize(self):
0278        pid = live_pidfile(self.options.pid_file)
0279        if pid:
0280            raise DaemonizeException(
0281                "Daemon is already running (PID: %s from PID file %s)"
0282                % (pid, self.options.pid_file))
0283
0284        if self.verbose > 0:
0285            print 'Entering daemon mode'
0286        pid = os.fork()
0287        if pid:
0288            # The forked process also has a handle on resources, so we
0289            # *don't* want proper termination of the process, we just
0290            # want to exit quick (which os._exit() does)
0291            os._exit(0)
0292        # Make this the session leader
0293        os.setsid()
0294        # Fork again for good measure!
0295        pid = os.fork()
0296        if pid:
0297            os._exit(0)
0298
0299        # @@: Should we set the umask and cwd now?
0300
0301        import resource  # Resource usage information.
0302        maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
0303        if (maxfd == resource.RLIM_INFINITY):
0304            maxfd = MAXFD
0305        # Iterate through and close all file descriptors.
0306        for fd in range(0, maxfd):
0307            try:
0308                os.close(fd)
0309            except OSError:  # ERROR, fd wasn't open to begin with (ignored)
0310                pass
0311
0312        if (hasattr(os, "devnull")):
0313            REDIRECT_TO = os.devnull
0314        else:
0315            REDIRECT_TO = "/dev/null"
0316        os.open(REDIRECT_TO, os.O_RDWR)  # standard input (0)
0317        # Duplicate standard input to standard output and standard error.
0318        os.dup2(0, 1)  # standard output (1)
0319        os.dup2(0, 2)  # standard error (2)
0320
0321    def record_pid(self, pid_file):
0322        pid = os.getpid()
0323        if self.verbose > 1:
0324            print 'Writing PID %s to %s' % (pid, pid_file)
0325        f = open(pid_file, 'w')
0326        f.write(str(pid))
0327        f.close()
0328        atexit.register(_remove_pid_file, pid, pid_file, self.verbose)
0329
0330    def stop_daemon(self):
0331        pid_file = self.options.pid_file or 'paster.pid'
0332        if not os.path.exists(pid_file):
0333            print 'No PID file exists in %s' % pid_file
0334            return 1
0335        pid = read_pidfile(pid_file)
0336        if not pid:
0337            print "Not a valid PID file in %s" % pid_file
0338            return 1
0339        pid = live_pidfile(pid_file)
0340        if not pid:
0341            print "PID in %s is not valid (deleting)" % pid_file
0342            try:
0343                os.unlink(pid_file)
0344            except (OSError, IOError), e:
0345                print "Could not delete: %s" % e
0346                return 2
0347            return 1
0348        for j in range(10):
0349            if not live_pidfile(pid_file):
0350                break
0351            os.kill(pid, signal.SIGTERM)
0352            time.sleep(1)
0353        else:
0354            print "failed to kill web process %s" % pid
0355            return 3
0356        if os.path.exists(pid_file):
0357            os.unlink(pid_file)
0358        return 0
0359
0360    def show_status(self):
0361        pid_file = self.options.pid_file or 'paster.pid'
0362        if not os.path.exists(pid_file):
0363            print 'No PID file %s' % pid_file
0364            return 1
0365        pid = read_pidfile(pid_file)
0366        if not pid:
0367            print 'No PID in file %s' % pid_file
0368            return 1
0369        pid = live_pidfile(pid_file)
0370        if not pid:
0371            print 'PID %s in %s is not running' % (pid, pid_file)
0372            return 1
0373        print 'Server running in PID %s' % pid
0374        return 0
0375
0376    def restart_with_reloader(self):
0377        self.restart_with_monitor(reloader=True)
0378
0379    def restart_with_monitor(self, reloader=False):
0380        if self.verbose > 0:
0381            if reloader:
0382                print 'Starting subprocess with file monitor'
0383            else:
0384                print 'Starting subprocess with monitor parent'
0385        while 1:
0386            args = [self.quote_first_command_arg(sys.executable)] + sys.argv
0387            new_environ = os.environ.copy()
0388            if reloader:
0389                new_environ[self._reloader_environ_key] = 'true'
0390            else:
0391                new_environ[self._monitor_environ_key] = 'true'
0392            proc = None
0393            try:
0394                try:
0395                    _turn_sigterm_into_systemexit()
0396                    proc = subprocess.Popen(args, env=new_environ)
0397                    exit_code = proc.wait()
0398                    proc = None
0399                except KeyboardInterrupt:
0400                    print '^C caught in monitor process'
0401                    if self.verbose > 1:
0402                        raise
0403                    return 1
0404            finally:
0405                if (proc is not None
0406                    and hasattr(os, 'kill')):
0407                    import signal
0408                    try:
0409                        os.kill(proc.pid, signal.SIGTERM)
0410                    except (OSError, IOError):
0411                        pass
0412
0413            if reloader:
0414                # Reloader always exits with code 3; but if we are
0415                # a monitor, any exit code will restart
0416                if exit_code != 3:
0417                    return exit_code
0418            if self.verbose > 0:
0419                print '-'*20, 'Restarting', '-'*20
0420
0421    def change_user_group(self, user, group):
0422        if not user and not group:
0423            return
0424        import pwd, grp
0425        uid = gid = None
0426        if group:
0427            try:
0428                gid = int(group)
0429                group = grp.getgrgid(gid).gr_name
0430            except ValueError:
0431                import grp
0432                try:
0433                    entry = grp.getgrnam(group)
0434                except KeyError:
0435                    raise BadCommand(
0436                        "Bad group: %r; no such group exists" % group)
0437                gid = entry.gr_gid
0438        try:
0439            uid = int(user)
0440            user = pwd.getpwuid(uid).pw_name
0441        except ValueError:
0442            try:
0443                entry = pwd.getpwnam(user)
0444            except KeyError:
0445                raise BadCommand(
0446                    "Bad username: %r; no such user exists" % user)
0447            if not gid:
0448                gid = entry.pw_gid
0449            uid = entry.pw_uid
0450        if self.