##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################

""" Startup package.  Responsible for startup configuration of Zope """

import logging
import os
import sys
import socket
from re import compile
from socket import gethostbyaddr
try:
    import twisted.internet.reactor
    _use_twisted = True
except ImportError:
    _use_twisted = True
    

import ZConfig
from ZConfig.components.logger import loghandler

logger = logging.getLogger("Zope")
started = False

def get_starter():
    check_python_version()
    if sys.platform[:3].lower() == "win":
        return WindowsZopeStarter()
    else:
        return UnixZopeStarter()

def start_zope(cfg, debug_handler):
    """The function called by run.py which starts a Zope appserver."""
    global started
    if started:
        # Don't allow any code to call start_zope() twice.
        return

    starter = get_starter()
    starter.setConfiguration(cfg)
    starter.prepare()

    started = True
    try:
        starter.run()
    finally:
        started = False


class ZopeStarter:
    """This is a class which starts a Zope server.

    Making it a class makes it easier to test.
    """
    def __init__(self):
        self.event_logger = logging.getLogger()
        # We log events to the root logger, which is backed by a
        # "StartupHandler" log handler.  The "StartupHandler" buffers
        # log messages.  When the "real" loggers are set up, we flush
        # accumulated messages in StartupHandler's buffers to the real
        # logger.
        formatter = logging.Formatter(
            "%(asctime)s %(levelname)s %(name)s %(message)s",
            "%Y-%m-%d %H:%M:%S")
        self.debug_handler = loghandler.StreamHandler()
        self.debug_handler.setFormatter(formatter)
        self.debug_handler.setLevel(logging.WARN)

        self.startup_handler = loghandler.StartupHandler()

        self.event_logger.addHandler(self.debug_handler)
        self.event_logger.addHandler(self.startup_handler)

    def setConfiguration(self, cfg):
        self.cfg = cfg

    def prepare(self):
        self.setupInitialLogging()
        self.setupLocale()
        self.setupSecurityOptions()
        self.setupPublisher()
        # Start ZServer servers before we drop privileges so we can bind to
        # "low" ports:
        self.setupZServer()
        self.setupServers()
        # drop privileges after setting up servers
        self.dropPrivileges()
        self.makeLockFile()
        self.makePidFile()
        self.setupInterpreter()
        self.startZope()
        from App.config import getConfiguration
        config = getConfiguration()
        if not config.twisted_servers:
            self.registerSignals()
        # emit a "ready" message in order to prevent the kinds of emails
        # to the Zope maillist in which people claim that Zope has "frozen"
        # after it has emitted ZServer messages.
        logger.info('Ready to handle requests')
        self.setupFinalLogging()

    def run(self):
        # the mainloop.
        try:
            from App.config import getConfiguration
            config = getConfiguration()
            import ZServer
            if config.twisted_servers and config.servers:
                raise ZConfig.ConfigurationError(
                    "You can't run both ZServer servers and twisted servers.")
            if config.twisted_servers:
                if not _use_twisted:
                    raise ZConfig.ConfigurationError(
                        "You do not have twisted installed.")
                twisted.internet.reactor.run()
                # Storing the exit code in the ZServer even for twisted, 
                # but hey, it works...
                sys.exit(ZServer.exit_code)
            else:
                import Lifetime
                Lifetime.loop()
                sys.exit(ZServer.exit_code)
        finally:
            self.shutdown()

    def shutdown(self):
        self.unlinkLockFile()
        self.unlinkPidFile()

    # XXX does anyone actually use these three?

    def info(self, msg):
        logger.info(msg)

    def panic(self, msg):
        logger.critical(msg)

    def error(self, msg):
        logger.error(msg)

    def setupPublisher(self):
        import Globals
        import ZPublisher.HTTPRequest
        import ZPublisher.Publish
        Globals.DevelopmentMode = self.cfg.debug_mode
        ZPublisher.Publish.set_default_debug_mode(self.cfg.debug_mode)
        ZPublisher.Publish.set_default_authentication_realm(
            self.cfg.http_realm)
        if self.cfg.publisher_profile_file:
            filename = self.cfg.publisher_profile_file
            ZPublisher.Publish.install_profiling(filename)
        if self.cfg.trusted_proxies:
            # DM 2004-11-24: added host name mapping (such that examples in conf file really have a chance to work
            mapped = []
            for name in self.cfg.trusted_proxies: mapped.extend(_name2Ips(name))
            ZPublisher.HTTPRequest.trusted_proxies = tuple(mapped)

    def setupSecurityOptions(self):
        import AccessControl
        AccessControl.setImplementation(
            self.cfg.security_policy_implementation)
        AccessControl.setDefaultBehaviors(
            not self.cfg.skip_ownership_checking,
            not self.cfg.skip_authentication_checking,
            self.cfg.verbose_security)

    def setupLocale(self):
        # set a locale if one has been specified in the config
        if not self.cfg.locale:
            return

        # workaround to allow unicode encoding conversions in DTML
        import codecs
        dummy = codecs.lookup('iso-8859-1')

        locale_id = self.cfg.locale

        if locale_id is not None:
            try:
                import locale
            except:
                raise ZConfig.ConfigurationError(
                    'The locale module could not be imported.\n'
                    'To use localization options, you must ensure\n'
                    'that the locale module is compiled into your\n'
                    'Python installation.'
                    )
            try:
                locale.setlocale(locale.LC_ALL, locale_id)
            except:
                raise ZConfig.ConfigurationError(
                    'The specified locale "%s" is not supported by your'
                    'system.\nSee your operating system documentation for '
                    'more\ninformation on locale support.' % locale_id
                    )

    def setupZServer(self):
        # Increase the number of threads
        import ZServer
        ZServer.setNumberOfThreads(self.cfg.zserver_threads)
        ZServer.CONNECTION_LIMIT = self.cfg.max_listen_sockets

    def setupServers(self):
        socket_err = (
            'There was a problem starting a server of type "%s". '
            'This may mean that your user does not have permission to '
            'bind to the port which the server is trying to use or the '
            'port may already be in use by another application. '
            '(%s)'
            )
        servers = []
        for server in self.cfg.servers:
            # create the server from the server factory
            # set up in the config
            try:
                servers.append(server.create())
            except socket.error,e:
                raise ZConfig.ConfigurationError(socket_err
                                                 % (server.servertype(),e[1]))
        self.cfg.servers = servers

    def dropPrivileges(self):
        return dropPrivileges(self.cfg)

    def getLoggingLevel(self):
        if self.cfg.eventlog is None:
            level = logging.INFO
        else:
            # get the lowest handler level.  This is the effective level
            # level at which which we will spew messages to the console
            # during startup.
            level = self.cfg.eventlog.getLowestHandlerLevel()
        return level

    def setupConfiguredLoggers(self):
        # Must happen after ZopeStarter.setupInitialLogging()
        self.event_logger.removeHandler(self.startup_handler)
        if self.cfg.zserver_read_only_mode:
            # no log files written in read only mode
            return

        if self.cfg.eventlog is not None:
            self.cfg.eventlog()
        if self.cfg.access is not None:
            self.cfg.access()
        if self.cfg.trace is not None:
            self.cfg.trace()

        # flush buffered startup messages to event logger
        if self.cfg.debug_mode:
            self.event_logger.removeHandler(self.debug_handler)
            self.startup_handler.flushBufferTo(self.event_logger)
            self.event_logger.addHandler(self.debug_handler)
        else:
            self.startup_handler.flushBufferTo(self.event_logger)

    def setupInitialLogging(self):
        if self.cfg.debug_mode:
            self.debug_handler.setLevel(self.getLoggingLevel())
        else:
            self.event_logger.removeHandler(self.debug_handler)
            self.debug_handler = None

    def startZope(self):
        # Import Zope
        import Zope2
        Zope2.startup()

    def makeLockFile(self):
        if not self.cfg.zserver_read_only_mode:
            # lock_file is used for the benefit of zctl-like systems, so they
            # can tell whether Zope is already running before attempting to
            # fire it off again.
            #
            # We aren't concerned about locking the file to protect against
            # other Zope instances running from our CLIENT_HOME, we just
            # try to lock the file to signal that zctl should not try to
            # start Zope if *it* can't lock the file; we don't panic
            # if we can't lock it.
            # we need a separate lock file because on win32, locks are not
            # advisory, otherwise we would just use the pid file
            from Zope2.Startup.misc.lock_file import lock_file
            lock_filename = self.cfg.lock_filename
            try:
                if os.path.exists(lock_filename):
                    os.unlink(lock_filename)
                self.lockfile = open(lock_filename, 'w')
                lock_file(self.lockfile)
                self.lockfile.write(str(os.getpid()))
                self.lockfile.flush()
            except IOError:
                pass

    def makePidFile(self):
        if not self.cfg.zserver_read_only_mode:
            # write the pid into the pidfile if possible
            try:
                if os.path.exists(self.cfg.pid_filename):
                    os.unlink(self.cfg.pid_filename)
                f = open(self.cfg.pid_filename, 'w')
                f.write(str(os.getpid()))
                f.close()
            except IOError:
                pass

    def unlinkPidFile(self):
        if not self.cfg.zserver_read_only_mode:
            try:
                os.unlink(self.cfg.pid_filename)
            except OSError:
                pass

    def unlinkLockFile(self):
        if not self.cfg.zserver_read_only_mode:
            try:
                self.lockfile.close()
                os.unlink(self.cfg.lock_filename)
            except OSError:
                pass

    def setupInterpreter(self):
        """ make changes to the python interpreter environment """
        sys.setcheckinterval(self.cfg.python_check_interval)


class WindowsZopeStarter(ZopeStarter):

    def registerSignals(self):
        from Signals import Signals
        Signals.registerZopeSignals([self.cfg.eventlog,
                                     self.cfg.access,
                                     self.cfg.trace])

    def setupInitialLogging(self):
        ZopeStarter.setupInitialLogging(self)
        self.setupConfiguredLoggers()

    def setupFinalLogging(self):
        pass


class UnixZopeStarter(ZopeStarter):

    def registerSignals(self):
        from Signals import Signals
        Signals.registerZopeSignals([self.cfg.eventlog,
                                     self.cfg.access,
                                     self.cfg.trace])

    def setupInitialLogging(self):
        ZopeStarter.setupInitialLogging(self)
        level = self.getLoggingLevel()

        self.startup_handler.setLevel(level)

        # set the initial logging level (this will be changed by the
        # zconfig settings later)
        self.event_logger.setLevel(level)

    def setupFinalLogging(self):
        self.setupConfiguredLoggers()


def check_python_version():
    # check for Python version
    python_version = sys.version.split()[0]
    optimum_version = '2.3.4'
    if python_version < '2.3.4':
        raise ZConfig.ConfigurationError(
            'Invalid python version: %s, the optimal version is %s or higher' %
            (python_version, optimum_version))


def dropPrivileges(cfg):
    # Drop root privileges if we have them and we're on a posix platform.
    # This needs to be a function so it may be used outside of Zope
    # appserver startup (e.g. from zopectl debug)
    if os.name != 'posix':
        return

    if os.getuid() != 0:
        return

    import pwd

    effective_user  = cfg.effective_user
    if effective_user is None:
        msg = ('A user was not specified to setuid to; fix this to '
               'start as root (change the effective-user directive '
               'in zope.conf)')
        logger.critical(msg)
        raise ZConfig.ConfigurationError(msg)

    try:
        uid = int(effective_user)
    except ValueError:
        try:
            pwrec = pwd.getpwnam(effective_user)
        except KeyError:
            msg = "Can't find username %r" % effective_user
            logger.error(msg)
            raise ZConfig.ConfigurationError(msg)
        uid = pwrec[2]
    else:
        try:
            pwrec = pwd.getpwuid(uid)
        except KeyError:
            msg = "Can't find uid %r" % uid
            logger.error(msg)
            raise ZConfig.ConfigurationError(msg)
    gid = pwrec[3]

    if uid == 0:
        msg = 'Cannot start Zope with the effective user as the root user'
        logger.error(msg)
        raise ZConfig.ConfigurationError(msg)

    try:
        import initgroups
        initgroups.initgroups(effective_user, gid)
        os.setgid(gid)
    except OSError:
        logger.exception('Could not set group id of effective user')

    os.setuid(uid)
    logger.info('Set effective user to "%s"' % effective_user)
    return 1 # for unit testing purposes 


# DM 2004-11-24: added
def _name2Ips(host, isIp_=compile(r'(\d+\.){3}').match):
    '''map a name *host* to the sequence of its ip addresses;
    use *host* itself (as sequence) if it already is an ip address.
    Thus, if only a specific interface on a host is trusted,
    identify it by its ip (and not the host name).
    '''
    if isIp_(host): return [host]
    return gethostbyaddr(host)[2]
