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.

Signed-off-by: James McCoy <jamessan@users.sourceforge.net>
This commit is contained in:
Daniel Folkinshteyn 2010-08-05 01:20:46 -04:00 committed by James McCoy
parent d691a91636
commit a2985c37d6
4 changed files with 57 additions and 5 deletions

View File

@ -34,10 +34,10 @@ import binascii
import supybot.utils as utils import supybot.utils as utils
from supybot.commands import * from supybot.commands import *
import supybot.plugins as plugins import supybot.plugins as plugins
import supybot.commands as commands
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.callbacks as callbacks import supybot.callbacks as callbacks
class String(callbacks.Plugin): class String(callbacks.Plugin):
def ord(self, irc, msg, args, letter): def ord(self, irc, msg, args, letter):
"""<letter> """<letter>
@ -141,10 +141,10 @@ class String(callbacks.Plugin):
s = 'You probably don\'t want to match the empty string.' s = 'You probably don\'t want to match the empty string.'
irc.error(s) irc.error(s)
else: else:
irc.reply(f(text)) v = commands.process(f, text, timeout=10)
re = wrap(re, [('checkCapability', 'trusted'), irc.reply(v)
first('regexpMatcher', 'regexpReplacer'), re = thread(wrap(re, [first('regexpMatcher', 'regexpReplacer'),
'text']) 'text']))
def xor(self, irc, msg, args, password, text): def xor(self, irc, msg, args, password, text):
"""<password> <text> """<password> <text>

View File

@ -970,6 +970,23 @@ class CommandThread(world.SupyThread):
finally: finally:
self.cb.threaded = self.originalThreaded 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): class CanonicalString(registry.NormalizedString):
def normalize(self, s): def normalize(self, s):

View File

@ -33,10 +33,12 @@ Includes wrappers for commands.
""" """
import time import time
import Queue
import types import types
import getopt import getopt
import inspect import inspect
import threading import threading
import multiprocessing
from . import callbacks, conf, ircdb, ircmsgs, ircutils, log, utils, world from . import callbacks, conf, ircdb, ircmsgs, ircutils, log, utils, world
@ -59,6 +61,29 @@ def thread(f):
f(self, irc, msg, args, *L, **kwargs) f(self, irc, msg, args, *L, **kwargs)
return utils.python.changeFunctionName(newf, f.func_name, f.__doc__) 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 <timeout> 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): class UrlSnarfThread(world.SupyThread):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
assert 'url' in kwargs assert 'url' in kwargs

View File

@ -37,6 +37,7 @@ import sys
import time import time
import atexit import atexit
import threading import threading
import multiprocessing
if sys.version_info >= (2, 5, 0): if sys.version_info >= (2, 5, 0):
import re as sre import re as sre
@ -63,6 +64,15 @@ class SupyThread(threading.Thread):
super(SupyThread, self).__init__(*args, **kwargs) super(SupyThread, self).__init__(*args, **kwargs)
log.debug('Spawning thread %q.', self.getName()) 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 commandsProcessed = 0
ircs = [] # A list of all the IRCs. ircs = [] # A list of all the IRCs.