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.
This commit is contained in:
Daniel Folkinshteyn 2010-08-05 01:20:46 -04:00
parent 9398025088
commit 0c300162d8
4 changed files with 58 additions and 4 deletions

View File

@ -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):
"""<password> <text>

View File

@ -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):

View File

@ -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 <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):
def __init__(self, *args, **kwargs):
assert 'url' in kwargs

View File

@ -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.