Revision a8ce25a3ee97c6e9277521432cbeb4f55366b375 authored by Jeremy Fincher on 16 September 2004, 14:50:34 UTC, committed by Jeremy Fincher on 16 September 2004, 14:50:34 UTC
1 parent d631b3e
Raw File
supybot
#!/usr/bin/env python

###
# Copyright (c) 2003-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###

"""
This is the main program to run Supybot.
"""

__revision__ = "$Id$"

import re
import os
import sys
import atexit
import shutil
import signal
import cStringIO as StringIO

if sys.version_info < (2, 3, 0):
    sys.stderr.write('This program requires Python >= 2.3.0\n')
    sys.exit(-1)

def _termHandler(signalNumber, stackFrame):
    raise SystemExit, 'Signal #%s' % signalNumber

signal.signal(signal.SIGTERM, _termHandler)

import time
import optparse
import textwrap

started = time.time()

import supybot
import supybot.utils as utils
import supybot.registry as registry
import supybot.questions as questions

def main():
    import supybot.conf as conf
    import supybot.world as world
    import supybot.drivers as drivers
    import supybot.schedule as schedule
    # We schedule this event rather than have it actually run because if there
    # is a failure between now and the time it takes the Owner plugin to load
    # all the various plugins, our registry file might be wiped.  That's bad.
    interrupted = False
    when = time.time() + conf.supybot.upkeepInterval()
    schedule.addPeriodicEvent(world.upkeep, when, name='upkeep', now=False)
    world.startedAt = started
    while world.ircs:
        try:
            drivers.run()
        except KeyboardInterrupt:
            if interrupted:
                # Interrupted while waiting for queues to clear.  Let's clear
                # them ourselves.
                for irc in world.ircs:
                    irc._reallyDie()
                    continue
            else:
                interrupted = True
                log.info('Exiting due to Ctrl-C.  '
                         'If the bot doesn\'t exit within a few seconds, '
                         'feel free to press Ctrl-C again to make it exit '
                         'without flushing its message queues.')
                world.upkeep()
                for irc in world.ircs:
                    irc.queueMsg(ircmsgs.quit('Ctrl-C at console.'))
                    irc.die()
        except SystemExit, e:
            s = str(e)
            if s:
                log.info('Exiting due to %s', s)
            break
        except:
            try: # Ok, now we're *REALLY* paranoid!
                log.exception('Exception raised out of drivers.run:')
            except Exception, e:
                print 'Exception raised in log.exception.  This is *really*'
                print 'bad.  Hopefully it won\'t happen again, but tell us'
                print 'about it anyway, this is a significant problem.'
                print 'Anyway, here\'s the exception: %s'% utils.exnToString(e)
            except:
                print 'Man, this really sucks.  Not only did log.exception'
                print 'raise an exception, but freaking-a, it was a string'
                print 'exception.  People who raise string exceptions should'
                print 'die a slow, painful death.'
    now = time.time()
    seconds = now - world.startedAt
    log.info('Total uptime: %s.', utils.timeElapsed(seconds))
    (user, system, _, _, _) = os.times()
    log.info('Total CPU time taken: %s seconds.', user+system)
    log.info('No more Irc objects, exiting.')

if __name__ == '__main__':
    ###
    # Options:
    # -p (profiling)
    # -O (optimizing)
    # -n, --nick (nick)
    # --startup (commands to run onStart)
    # --connect (commands to run afterConnect)
    # --config (configuration values)
    parser = optparse.OptionParser(usage='Usage: %prog [options] configFile',
                                   version='supybot 0.80.0pre1')
    parser.add_option('-P', '--profile', action='store_true', dest='profile',
                      help='enables profiling')
    parser.add_option('-O', action='count', dest='optimize',
                      help='-O optimizes asserts out of the code; ' \
                           '-OO optimizes asserts and uses psyco.')
    parser.add_option('-n', '--nick', action='store',
                      dest='nick', default='',
                      help='nick the bot should use')
    parser.add_option('-u', '--user', action='store',
                      dest='user', default='',
                      help='full username the bot should use')
    parser.add_option('-i', '--ident', action='store',
                      dest='ident', default='',
                      help='ident the bot should use')
    parser.add_option('-d', '--daemon', action='store_true',
                      dest='daemon',
                      help='Determines whether the bot will daemonize.  '
                           'This is a no-op on non-POSIX systems.')
    parser.add_option('', '--allow-eval', action='store_true',
                      dest='allowEval',
                      help='Determines whether the bot will '
                           'allow the evaluation of arbitrary Python code.')
    parser.add_option('', '--allow-default-owner', action='store_true',
                      dest='allowDefaultOwner',
                      help='Determines whether the bot will allow its '
                           'defaultCapabilities not to include "-owner", thus '
                           'giving all users the owner capability by default. '
                           ' This is dumb, hence we require a command-line '
                           'option.  Don\'t do this.')
    parser.add_option('', '--allow-root', action='store_true',
                      dest='allowRoot',
                      help='Determines whether the bot will be allowed to run '
                           'as root.  You don\'t want this.  Don\'t do it.  '
                           'Even if you think you want it, you don\'t.  '
                           'You\'re probably dumb if you do this.')
    parser.add_option('', '--debug', action='store_true', dest='debug',
                      help='Determines whether some extra debugging stuff will '
                           'be logged in this script.')

    (options, args) = parser.parse_args()

    if os.name == 'posix':
        if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot:
            sys.stderr.write('Dude, don\'t even try to run this as root.\n')
            sys.exit(-1)

    if len(args) > 1:
        parser.error("""Only one configuration option should be specified.""")
    elif not args:
        parser.error(utils.normalizeWhitespace("""It seems you've given me no
        configuration file.  If you have a configuration file, be sure to tell
        its filename.  If you don't have a configuration file, read
        docs/GETTING_STARTED and follow its directions."""))
    else:
        registryFilename = args.pop()
        try:
            # The registry *MUST* be opened before importing log or conf.
            registry.open(registryFilename)
            shutil.copy(registryFilename, registryFilename + '.bak')
        except registry.InvalidRegistryFile, e:
            s = '%s in %s.  Please fix this error and start supybot again.' % \
                (e, registryFilename)
            s = textwrap.fill(s)
            sys.stderr.write(s)
            sys.stderr.write(os.linesep)
            raise
            sys.exit(-1)
        except EnvironmentError, e:
            sys.stderr.write(str(e))
            sys.stderr.write(os.linesep)
            sys.exit(-1)

    import supybot.log as log
    import supybot.conf as conf
    import supybot.world as world
    world.starting = True

    def closeRegistry():
        # We only print if world.dying so we don't see these messages during
        # upkeep.
        if world.dying:
            log.info('Writing registry file to %s', registryFilename)
        registry.close(conf.supybot, registryFilename, annotated=True)
        if world.dying:
            log.info('Finished writing registry file.')
    world.flushers.append(closeRegistry)
    world.registryFilename = registryFilename

    nick = options.nick or conf.supybot.nick()
    user = options.user or conf.supybot.user()
    ident = options.ident or conf.supybot.ident()

    networks = conf.supybot.networks()
    if not networks:
        questions.output("""No networks defined.  Perhaps you should re-run the
        wizard?""", fd=sys.stderr)
        # XXX We should turn off logging here for a prettier presentation.
        sys.exit(-1)



    if options.optimize:
        # This doesn't work anymore.
        __builtins__.__debug__ = False
        if options.optimize > 1:
            try:
                import psyco
                psyco.full()
            except ImportError:
                log.warning('Psyco isn\'t installed, cannot -OO.')

    conf.allowEval = options.allowEval
    conf.allowDefaultOwner = options.allowDefaultOwner

    if not os.path.exists(conf.supybot.directories.log()):
        os.mkdir(conf.supybot.directories.log())
    if not os.path.exists(conf.supybot.directories.conf()):
        os.mkdir(conf.supybot.directories.conf())
    if not os.path.exists(conf.supybot.directories.data()):
        os.mkdir(conf.supybot.directories.data())
    if not os.path.exists(conf.supybot.directories.data.tmp()):
        os.mkdir(conf.supybot.directories.tmp())

    userdataFilename = os.path.join(conf.supybot.directories.conf(),
                                    'userdata.conf')
    # Let's open this now since we've got our directories setup.
    if not os.path.exists(userdataFilename):
        fd = file(userdataFilename, 'w')
        fd.write('\n')
        fd.close()
    registry.open(userdataFilename)

    import supybot.Owner as Owner
    import supybot.irclib as irclib
    import supybot.ircmsgs as ircmsgs
    import supybot.drivers as drivers
    import supybot.callbacks as callbacks

    owner = Owner.Class()
    irclib._callbacks.append(owner)

    if options.debug:
        for (name, module) in sys.modules.iteritems():
            if hasattr(module, '__file__') and hasattr(module, '__revision__'):
                if module.__file__.startswith(supybot.installDir):
                    print '%s: %s' % (name, module.__revision__.split()[2])

    if os.name == 'posix' and options.daemon:
        def fork():
            child = os.fork()
            if child != 0:
                if options.debug:
                    print 'Parent exiting, child PID: %s' % child
                # We must us os._exit instead of sys.exit so atexit handlers
                # don't run.  They shouldn't be dangerous, but they're ugly.
                os._exit(0)
        fork()
        os.setsid()
        # What the heck does this do?  I wonder if it breaks anything...
        os.umask(0)
        # Let's not do this for now (at least until I can make sure it works):
        # Actually, let's never do this -- we'll always have files open in the
        # bot directories, so they won't be able to be unmounted anyway.
        # os.chdir('/')
        fork()
        # Since this is the indicator that no writing should be done to stdout,
        # we'll set it to True before closing stdout et alii.
        conf.daemonized = True
        # Closing stdin shouldn't cause problems.  We'll let it raise an
        # exception if it does.
        sys.stdin.close()
        # Closing these two might cause problems; we log writes to them as
        # level WARNING on upkeep.
        sys.stdout.close()
        sys.stderr.close()
        sys.stdout = StringIO.StringIO()
        sys.stderr = StringIO.StringIO()
        signal.signal(signal.SIGHUP, signal.SIG_IGN)
        log.info('Completed daemonization.  Current PID: %s', os.getpid())

    # Let's write the PID file.  This has to go after daemonization, obviously.
    pidFile = conf.supybot.pidFile()
    if pidFile:
        try:
            fd = file(pidFile, 'w')
            pid = os.getpid()
            fd.write('%s\n' % pid)
            fd.close()
            def removePidFile():
                try:
                    os.remove(pidFile)
                except EnvironmentError, e:
                    log.error('Could not remove pid file: %s', e)
            atexit.register(removePidFile)
        except EnvironmentError, e:
            log.error('Error opening pid file %s: %s', pidFile, e)

    if options.profile:
        import profile
        world.profiling = True
        profile.run('main()', '%s-%i.prof' % (nick, time.time()))
    else:
        main()


# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
back to top