Limnoria/scripts/supybot
2005-08-30 23:43:51 +00:00

342 lines
14 KiB
Python

#!/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.
"""
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.0'
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())
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.error('Error opening pid file %s: %s', pidFile, e)
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()
#irclib._callbacks.append(owner)
if options.profile:
import profile
world.profiling = True
profile.run('main()', '%s-%i.prof' % (nick, time.time()))
else:
main()
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: