#!/usr/local/bin/python2.7

###
# Copyright (c) 2003-2004, Jeremiah Fincher
# Copyright (c) 2009, James Vega
# 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.
"""

import supybot

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 = 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:
                    quitmsg = conf.supybot.plugins.Owner.quitMsg() or \
                              'Ctrl-C at console.'
                    irc.queueMsg(ircmsgs.quit(quitmsg))
                    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.gen.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.gen.timeElapsed(seconds))
    (user, system, _, _, _) = os.times()
    log.info('Total CPU time taken: %s seconds.', user+system)
    log.info('No more Irc objects, exiting.')

version = '0.83.4.1'
if __name__ == '__main__':
    ###
    # Options:
    # -p (profiling)
    # -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 %s' % version)
    parser.add_option('-P', '--profile', action='store_true', dest='profile',
                      help='enables profiling')
    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-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.str.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)

    try:
        import supybot.log as log
    except supybot.registry.InvalidRegistryValue, e:
        # This is raised here because supybot.log imports supybot.conf.
        name = e.value._name
        errmsg = textwrap.fill('%s: %s' % (name, e),
                               width=78, subsequent_indent=' '*len(name))
        sys.stderr.write(errmsg)
        sys.stderr.write('\n')
        sys.stderr.write('Please fix this error in your configuration file '
                         'and restart your bot.\n')
        sys.exit(-1)
    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.
        logger = log.debug
        if world.dying:
            logger = log.info
        logger('Writing registry file to %s', registryFilename)
        registry.close(conf.supybot, registryFilename)
        logger('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 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...
        # ...It did.  I don't know why, but it seems largely useless.  It seems
        # to me reasonable that we should respect the user's umask.
        #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.stdin.close()
        sys.stdout.close()
        sys.stderr.close()
        sys.stdout = StringIO.StringIO()
        sys.stderr = StringIO.StringIO()
        # We have to be really methodical here.
        os.close(0)
        os.close(1)
        os.close(2)
        fd = os.open('/dev/null', os.O_RDWR)
        os.dup2(fd, 0)
        os.dup2(fd, 1)
        os.dup2(fd, 2)
        signal.signal(signal.SIGHUP, signal.SIG_IGN)
        log.info('Completed daemonization.  Current PID: %s', os.getpid())

    # Stop setting our own umask.  See comment above.
    #os.umask(077)

    # 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.fatal('Error opening/writing pid file %s: %s', pidFile, e)
            sys.exit(-1)

    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.irclib as irclib
    import supybot.ircmsgs as ircmsgs
    import supybot.drivers as drivers
    import supybot.callbacks as callbacks
    import supybot.plugins.Owner as Owner

    owner = Owner.Class()

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


# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
