From 0ae05f64d6ec266c8bd76f62b185ac12ad04f981 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 14 Apr 2021 13:30:17 +0200 Subject: [PATCH] Move most of scripts/supybot's code to src/_main.py + add support for running a single loop iteration So it can be ran without the main script, by an external program that imports it. Motivation: I'll use it with Pyoxide --- scripts/supybot | 314 +--------------------------------------- src/_main.py | 372 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 374 insertions(+), 312 deletions(-) create mode 100644 src/_main.py diff --git a/scripts/supybot b/scripts/supybot index 3e4a5e53c..16df20343 100644 --- a/scripts/supybot +++ b/scripts/supybot @@ -34,38 +34,14 @@ This is the main program to run Supybot. """ -import supybot - -import re import os import sys -import atexit -import shutil -import signal - if sys.version_info < (3, 4, 0): sys.stderr.write('This program requires Python 3.4 or later.') sys.stderr.write(os.linesep) sys.exit(-1) -from io import StringIO # Import this after version check since this will fail on Python 2 - -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 -import supybot.ircutils as ircutils try: import supybot.i18n as i18n except ImportError: @@ -82,295 +58,9 @@ except ImportError: from supybot.version import version -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.' - # Because we're quitting from the console, none of the - # standard msg substitutions exist, and these will show as - # raw strings by default. Substitute them here with - # something meaningful instead. - env = dict((key, '') - for key in ('who', 'nick', 'user', 'host')) - quitmsg = ircutils.standardSubstitute(irc, None, quitmsg, - env=env) - irc.queueMsg(ircmsgs.quit(quitmsg)) - irc.die() - except SystemExit as 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 as 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('Oh, 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.') - httpserver.stopServer() - 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: %.2f seconds.', user+system) - log.info('No more Irc objects, exiting.') +from supybot._main import main if __name__ == '__main__': - parser = optparse.OptionParser(usage='Usage: %prog [options] configFile', - version='Limnoria %s running on Python %s' % - (version, sys.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 a security risk since it allows anyone to run ' - 'any command on your bot, so we advise not to use this.') - parser.add_option('', '--allow-root', action='store_true', - dest='allowRoot', - help='Determines whether the bot will be allowed to run ' - 'as root. This should not be used except in special ' - 'circumstances, such as running inside a containerized ' - 'environment.') - parser.add_option('', '--debug', action='store_true', dest='debug', - help='Determines whether some extra debugging stuff ' - 'will be logged in this script.') - parser.add_option('', '--disable-multiprocessing', action='store_true', - dest='disableMultiprocessing', - help='Disables multiprocessing stuff. May lead to ' - 'vulnerabilities.') - - (options, args) = parser.parse_args() - - if os.name == 'posix': - if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot: - sys.stderr.write('Running as root is not supported by default (see --allow-root).') - sys.stderr.write(os.linesep) - sys.exit(-1) - - if len(args) > 1: - parser.error("""Only one configuration file should be specified.""") - elif not args: - parser.error(utils.str.normalizeWhitespace("""It seems you've given me - no configuration file. If you do have a configuration file, be sure to - specify the filename. If you don't have a configuration file, read - docs/GETTING_STARTED and follow the instructions.""")) - else: - registryFilename = args.pop() - try: - # The registry *MUST* be opened before importing log or conf. - registry.open_registry(registryFilename) - shutil.copyfile(registryFilename, registryFilename + '.bak') - except registry.InvalidRegistryFile as 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 as e: - sys.stderr.write(str(e)) - sys.stderr.write(os.linesep) - sys.exit(-1) - - i18n.getLocaleFromRegistryCache() - - try: - import supybot.log as log - except supybot.registry.InvalidRegistryValue as 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(os.linesep) - sys.stderr.write('Please fix this error in your configuration file ' - 'and restart your bot.') - sys.stderr.write(os.linesep) - sys.exit(-1) - import supybot.conf as conf - import supybot.world as world - i18n.import_conf() - 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.stdout.close() - sys.stderr.close() - sys.stdout = StringIO() - sys.stderr = 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 = open(pidFile, 'w') - pid = os.getpid() - fd.write('%s%s' % (pid, os.linesep)) - fd.close() - def removePidFile(): - try: - os.remove(pidFile) - except EnvironmentError as e: - log.error('Could not remove pid file: %s', e) - atexit.register(removePidFile) - except EnvironmentError as e: - log.critical('Error opening/writing pid file %s: %s', pidFile, e) - sys.exit(-1) - - conf.allowDefaultOwner = options.allowDefaultOwner - world.disableMultiprocessing = options.disableMultiprocessing - - 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 = open(userdataFilename, 'w') - fd.write('\n') - fd.close() - registry.open_registry(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 - - # These may take some resources, and it does not need to be run while boot, so - # we import it as late as possible (but before plugins are loaded). - import supybot.httpserver as httpserver - - owner = Owner.Class() - - if options.profile: - import profile - world.profiling = True - profile.run('main()', '%s-%i.prof' % (nick, time.time())) - else: - main() - + main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/src/_main.py b/src/_main.py new file mode 100644 index 000000000..4fea96cd2 --- /dev/null +++ b/src/_main.py @@ -0,0 +1,372 @@ +### +# Copyright (c) 2003-2004, Jeremiah Fincher +# Copyright (c) 2009, James McCoy +# 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. +### + +"""Main script to run Limnoria. +This should only be imported by scripts/supybot.""" + +import re +import os +import sys +import atexit +import shutil +import signal +import platform + +if sys.version_info < (3, 4, 0): + sys.stderr.write('This program requires Python 3.4 or later.') + sys.stderr.write(os.linesep) + sys.exit(-1) + +from io import StringIO # Import this after version check since this will fail on Python 2 + +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.i18n as i18n +import supybot.utils as utils +import supybot.registry as registry +import supybot.questions as questions +import supybot.ircutils as ircutils +from supybot.version import version + +def _main(singleLoop): + import supybot.log as log + import supybot.conf as conf + import supybot.world as world + import supybot.ircmsgs as ircmsgs + import supybot.drivers as drivers + interrupted = False + 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.' + # Because we're quitting from the console, none of the + # standard msg substitutions exist, and these will show as + # raw strings by default. Substitute them here with + # something meaningful instead. + env = dict((key, '') + for key in ('who', 'nick', 'user', 'host')) + quitmsg = ircutils.standardSubstitute(irc, None, quitmsg, + env=env) + irc.queueMsg(ircmsgs.quit(quitmsg)) + irc.die() + except SystemExit as 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 as 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('Oh, 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.') + + if singleLoop: + return + httpserver.stopServer() + 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: %.2f seconds.', user+system) + log.info('No more Irc objects, exiting.') + +def main(): + parser = optparse.OptionParser(usage='Usage: %prog [options] configFile', + version='Limnoria %s running on Python %s' % + (version, sys.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 a security risk since it allows anyone to run ' + 'any command on your bot, so we advise not to use this.') + parser.add_option('', '--allow-root', action='store_true', + dest='allowRoot', + help='Determines whether the bot will be allowed to run ' + 'as root. This should not be used except in special ' + 'circumstances, such as running inside a containerized ' + 'environment.') + parser.add_option('', '--debug', action='store_true', dest='debug', + help='Determines whether some extra debugging stuff ' + 'will be logged in this script.') + parser.add_option('', '--disable-multiprocessing', action='store_true', + dest='disableMultiprocessing', + help='Disables multiprocessing stuff. May lead to ' + 'vulnerabilities.') + parser.add_option('', '--single-loop', action='store_true', + dest='singleLoop', + help='Do not use this unless you understand what it is.') + + (options, args) = parser.parse_args() + + if os.name == 'posix': + if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot: + sys.stderr.write('Running as root is not supported by default (see --allow-root).') + sys.stderr.write(os.linesep) + sys.exit(-1) + + if platform.system() != 'Emscripten' and options.singleLoop: + sys.stderr.write( + 'Do not use --single-loop, unless it is from a web browser ' + 'and you know what you are doing.') + sys.stderr.write(os.linesep) + sys.exit(-1) + + if len(args) > 1: + parser.error("""Only one configuration file should be specified.""") + elif not args: + parser.error(utils.str.normalizeWhitespace("""It seems you've given me + no configuration file. If you do have a configuration file, be sure to + specify the filename. If you don't have a configuration file, read + docs/GETTING_STARTED and follow the instructions.""")) + else: + registryFilename = args.pop() + try: + # The registry *MUST* be opened before importing log or conf. + registry.open_registry(registryFilename) + shutil.copyfile(registryFilename, registryFilename + '.bak') + except registry.InvalidRegistryFile as 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 as e: + sys.stderr.write(str(e)) + sys.stderr.write(os.linesep) + sys.exit(-1) + + i18n.getLocaleFromRegistryCache() + + try: + import supybot.log as log + except supybot.registry.InvalidRegistryValue as 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(os.linesep) + sys.stderr.write('Please fix this error in your configuration file ' + 'and restart your bot.') + sys.stderr.write(os.linesep) + sys.exit(-1) + import supybot.conf as conf + import supybot.world as world + i18n.import_conf() + 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.stdout.close() + sys.stderr.close() + sys.stdout = StringIO() + sys.stderr = 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 = open(pidFile, 'w') + pid = os.getpid() + fd.write('%s%s' % (pid, os.linesep)) + fd.close() + def removePidFile(): + try: + os.remove(pidFile) + except EnvironmentError as e: + log.error('Could not remove pid file: %s', e) + atexit.register(removePidFile) + except EnvironmentError as e: + log.critical('Error opening/writing pid file %s: %s', pidFile, e) + sys.exit(-1) + + conf.allowDefaultOwner = options.allowDefaultOwner + world.disableMultiprocessing = options.disableMultiprocessing + + 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 = open(userdataFilename, 'w') + fd.write('\n') + fd.close() + registry.open_registry(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 + + # These may take some resources, and it does not need to be run while boot, so + # we import it as late as possible (but before plugins are loaded). + import supybot.httpserver as httpserver + + owner = Owner.Class() + + # 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. + import supybot.schedule as schedule + when = conf.supybot.upkeepInterval() + schedule.addPeriodicEvent(world.upkeep, when, name='upkeep', now=False) + world.startedAt = started + + if options.profile: + import profile + world.profiling = True + profile.run('main()', '%s-%i.prof' % (nick, time.time())) + else: + _main(options.singleLoop)