From 0c300162d843dcf4ede805922ec221aec0884be5 Mon Sep 17 00:00:00 2001 From: Daniel Folkinshteyn Date: Thu, 5 Aug 2010 01:20:46 -0400 Subject: [PATCH] Create a commands.process function which runs a function inside a separate process. This is the only way to limit the execution time of a possibly long-running python statement. Use this on String.re, due to the possibility of pathologically long re matching in python. This allows us to remove the 'trusted-only' restriction on string.re. In the future, this should probably be used in other places that take user-supplied regexps, such as 'misc last --regexp', for example, as well as other potentially long-running tasks that can block the bot. --- plugins/String/plugin.py | 10 ++++++---- src/callbacks.py | 17 +++++++++++++++++ src/commands.py | 25 +++++++++++++++++++++++++ src/world.py | 10 ++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/plugins/String/plugin.py b/plugins/String/plugin.py index d6b4c0dbe..b836cbb1f 100644 --- a/plugins/String/plugin.py +++ b/plugins/String/plugin.py @@ -33,10 +33,12 @@ import binascii import supybot.utils as utils from supybot.commands import * +import supybot.commands as commands import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks +import multiprocessing class String(callbacks.Plugin): def ord(self, irc, msg, args, letter): @@ -136,10 +138,10 @@ class String(callbacks.Plugin): s = 'You probably don\'t want to match the empty string.' irc.error(s) else: - irc.reply(f(text)) - re = wrap(re, [('checkCapability', 'trusted'), - first('regexpMatcher', 'regexpReplacer'), - 'text']) + v = commands.process(f, text, timeout=10) + irc.reply(v) + re = thread(wrap(re, [first('regexpMatcher', 'regexpReplacer'), + 'text'])) def xor(self, irc, msg, args, password, text): """ diff --git a/src/callbacks.py b/src/callbacks.py index a836165e3..822e06616 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -976,6 +976,23 @@ class CommandThread(world.SupyThread): finally: self.cb.threaded = self.originalThreaded +class CommandProcess(world.SupyProcess): + """Just does some extra logging and error-recovery for commands that need + to run in processes. + """ + def __init__(self, target=None, args=(), kwargs={}): + self.command = args[0] + self.cb = target.im_self + procName = 'Process #%s (for %s.%s)' % (world.processesSpawned, + self.cb.name(), + self.command) + log.debug('Spawning process %s (args: %r)', procName, args) + self.__parent = super(CommandProcess, self) + self.__parent.__init__(target=target, name=procName, + args=args, kwargs=kwargs) + + def run(self): + self.__parent.run() class CanonicalString(registry.NormalizedString): def normalize(self, s): diff --git a/src/commands.py b/src/commands.py index 717465a9c..1879082c2 100644 --- a/src/commands.py +++ b/src/commands.py @@ -37,6 +37,8 @@ import types import getopt import inspect import threading +import multiprocessing #python2.6 or later! +import Queue import supybot.log as log import supybot.conf as conf @@ -67,6 +69,29 @@ def thread(f): f(self, irc, msg, args, *L, **kwargs) return utils.python.changeFunctionName(newf, f.func_name, f.__doc__) +def process(f, *args, **kwargs): + """Runs a function in a subprocess. + Takes an extra timeout argument, which, if supplied, limits the length + of execution of target function to seconds.""" + timeout = kwargs.pop('timeout') + q = multiprocessing.Queue() + def newf(f, q, *args, **kwargs): + r = f(*args, **kwargs) + q.put(r) + targetArgs = (f, q,) + args + p = world.SupyProcess(target=newf, + args=targetArgs, kwargs=kwargs) + p.start() + p.join(timeout) + if p.is_alive(): + p.terminate() + q.put("Function call aborted due to timeout.") + try: + v = q.get(block=False) + except Queue.Empty: + v = "Nothing returned." + return v + class UrlSnarfThread(world.SupyThread): def __init__(self, *args, **kwargs): assert 'url' in kwargs diff --git a/src/world.py b/src/world.py index 23ec3789e..c69d37a7a 100644 --- a/src/world.py +++ b/src/world.py @@ -37,6 +37,7 @@ import sys import time import atexit import threading +import multiprocessing # python 2.6 and later! if sys.version_info >= (2, 5, 0): import re as sre @@ -67,6 +68,15 @@ class SupyThread(threading.Thread): super(SupyThread, self).__init__(*args, **kwargs) log.debug('Spawning thread %q.', self.getName()) +processesSpawned = 1 # Starts at one for the initial process. +class SupyProcess(multiprocessing.Process): + def __init__(self, *args, **kwargs): + global processesSpawned + processesSpawned += 1 + super(SupyProcess, self).__init__(*args, **kwargs) + log.debug('Spawning process %q.', self.name) + + commandsProcessed = 0 ircs = [] # A list of all the IRCs.