Ability to run user provided code now

This commit is contained in:
Jim Paris 2013-07-07 20:18:52 -04:00
parent 721d6c4936
commit 01c7165aca
3 changed files with 147 additions and 36 deletions

View File

@ -12,6 +12,7 @@ import time
import uuid
import psutil
import imp
import traceback
class LogReceiver(object):
"""Spawn a thread that listens to a pipe for log messages,
@ -153,8 +154,9 @@ class Process(object):
def exitcode(self):
return self._process.exitcode
def _exec_user_code(code, args):
def _exec_user_code(codeargs): # pragma: no cover (runs in subprocess)
"""Execute 'code' as if it were placed into a file and executed"""
(code, args) = codeargs
# This is split off into a separate function because the Python3
# syntax of "exec" triggers a SyntaxError in Python2, if it's within
# a nested function.
@ -163,9 +165,36 @@ def _exec_user_code(code, args):
module = imp.new_module("__main__")
finally:
imp.release_lock()
module.__file__ = "<user-provided>"
module.__file__ = "<user-code>"
sys.argv = [''] + args
exec(code, module.__dict__, {})
# Wrap the compile and exec in a try/except so we can format the
# exception more nicely.
try:
codeobj = compile(code, '<user-code>', 'exec',
flags = 0, dont_inherit = 1)
exec(codeobj, module.__dict__, {})
except:
# Pull out the exception
info = sys.exc_info()
tblist = traceback.extract_tb(info[2])
# First entry is probably this code; get rid of it
if len(tblist) and tblist[0][2] == '_exec_user_code':
tblist = tblist[1:]
# Add the user's source code to every line that's missing it
lines = code.splitlines()
for (n, (name, line, func, text)) in enumerate(tblist):
if name == '<user-code>' and text is None and line <= len(lines):
tblist[n] = (name, line, func, lines[line-1].strip())
# Print it to stderr in the usual format
out = ['Traceback (most recent call last):\n']
out.extend(traceback.format_list(tblist))
out.extend(traceback.format_exception_only(info[0], info[1]))
sys.stderr.write("".join(out))
sys.stderr.flush()
sys.exit(1)
sys.exit(0)
class ProcessManager(object):
@ -179,21 +208,21 @@ class ProcessManager(object):
def __getitem__(self, key):
return self.processes[key]
def run_function(self, name, function, parameters):
def run_function(self, procname, function, parameters):
"""Run a Python function that already exists"""
new = Process(name, function, parameters)
new = Process(procname, function, parameters)
self.processes[new.pid] = new
return new.pid
def run_code_string(self, name, code, args):
def run_code(self, procname, code, args):
"""Evaluate 'code' as if it were placed into a Python file and
executed. The arguments will be accessible in the code as
sys.argv[1:]."""
return self.run_function(name, _exec_user_code, (code, args))
return self.run_function(procname, _exec_user_code, (code, args))
def run_command(self, name, args):
def run_command(self, procname, argv):
"""Execute a command line program"""
def spwan_user_command(args): # pragma: no cover (runs in subprocess)
def spwan_user_command(argv): # pragma: no cover (runs in subprocess)
try:
maxfd = os.sysconf("SC_OPEN_MAX")
except Exception:
@ -203,8 +232,8 @@ class ProcessManager(object):
os.chdir("/tmp")
except OSError:
pass
os.execvp(args[0], args)
return self.run_function(name, spwan_user_command, args)
os.execvp(argv[0], argv)
return self.run_function(procname, spwan_user_command, argv)
def terminate(self, pid):
return self.processes[pid].terminate()

View File

@ -118,11 +118,22 @@ class AppRun(object):
@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
@cherrypy.tools.CORS_allow(methods = ["POST"])
def command(self, args):
"""Execute an arbitrary program on the server. 'args' is the
argument list, with 'args[0]' being the program and 'args[1]',
'args[2]', etc as arguments."""
return self.manager.run_command("command", args)
def command(self, argv):
"""Execute an arbitrary program on the server. argv is a
list of the program and its arguments: 'argv[0]' is the program
and 'argv[1:]' are arguments"""
return self.manager.run_command("command", argv)
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
@cherrypy.tools.CORS_allow(methods = ["POST"])
def code(self, code, args):
"""Execute arbitrary Python code. 'code' is a formatted string.
It will be run as if it were written into a Python file and
executed, with the arguments in 'args' passed on the command line
(i.e., they end up in sys.argv[1:])"""
return self.manager.run_code("usercode", code, args)
# /run/trainola
@cherrypy.expose

View File

@ -25,6 +25,7 @@ import urllib2
from urllib2 import urlopen, HTTPError
import requests
import pprint
import textwrap
from testutil.helpers import *
@ -46,15 +47,29 @@ def teardown_module():
class TestClient(object):
def wait_end(self, client, pid, timeout = 5):
def wait_kill(self, client, pid, timeout = 1):
time.sleep(timeout)
status = client.get("/process/status", { "pid": pid })
if not status["alive"]:
raise AssertionError("died before we could kill it")
status = client.post("/process/remove", { "pid": pid })
if status["alive"]:
raise AssertionError("didn't get killed")
return status
def wait_end(self, client, pid, timeout = 5, remove = True):
start = time.time()
status = None
while (time.time() - start) < timeout:
status = client.get("/process/status", { "pid": pid })
if status["alive"] == False:
return status
raise AssertionError("process " + str(pid) + " didn't die in " +
str(timeout) + " seconds: " + repr(status))
break
else:
raise AssertionError("process " + str(pid) + " didn't die in " +
str(timeout) + " seconds: " + repr(status))
if remove:
status = client.post("/process/remove", { "pid": pid })
return status
def test_client_01_basic(self):
client = HTTPClient(baseurl = testurl)
@ -101,7 +116,7 @@ class TestClient(object):
nin_("dummy 0\ndummy 1\ndummy 2\ndummy 3\n", status["log"])
# See that it ended properly
status = self.wait_end(client, pid)
status = self.wait_end(client, pid, remove = False)
in_("dummy 27\ndummy 28\ndummy 29\n", status["log"])
eq_(status["exitcode"], 0)
@ -153,7 +168,7 @@ class TestClient(object):
def test_client_05_trainola_simple(self):
client = HTTPClient(baseurl = testurl, post_json = True)
pid = client.post("/run/trainola", { "data": {} })
status = self.wait_end(client, pid)
status = self.wait_end(client, pid, remove = False)
ne_(status["exitcode"], 0)
status = client.post("/process/remove", { "pid": pid })
@ -213,25 +228,16 @@ class TestClient(object):
if i < 3:
raise AssertionError("too fast?")
def test_client_07_process_command(self):
def test_client_07_run_command(self):
client = HTTPClient(baseurl = testurl, post_json = True)
eq_(client.get("/process/list"), [])
def do(args, kill):
pid = client.post("/run/command", { "args": args } )
def do(argv, kill):
pid = client.post("/run/command", { "argv": argv } )
eq_(client.get("/process/list"), [pid])
if kill:
time.sleep(1)
status = client.get("/process/status", { "pid": pid })
if not status["alive"]:
raise AssertionError("died before we could kill it")
status = client.post("/process/remove", { "pid": pid })
if status["alive"]:
raise AssertionError("didn't get killed")
else:
self.wait_end(client, pid)
status = client.post("/process/remove", { "pid": pid })
return status
return self.wait_kill(client, pid)
return self.wait_end(client, pid)
# Simple command
status = do(["pwd"], False)
@ -250,3 +256,68 @@ class TestClient(object):
# Kill a slow command
status = do(["sleep", "60"], True)
ne_(status["exitcode"], 0)
def test_client_08_run_code(self):
client = HTTPClient(baseurl = testurl, post_json = True)
eq_(client.get("/process/list"), [])
def do(code, args, kill):
pid = client.post("/run/code", { "code": code, "args": args } )
eq_(client.get("/process/list"), [pid])
if kill:
return self.wait_kill(client, pid)
return self.wait_end(client, pid)
# basic code snippet
code=textwrap.dedent("""
print 'hello'
def foo(arg):
print 'world'
""")
status = do(code, [], False)
eq_("hello\n", status["log"])
eq_(status["exitcode"], 0)
# compile error
code=textwrap.dedent("""
def foo(arg:
print 'hello'
""")
status = do(code, [], False)
in_("SyntaxError", status["log"])
eq_(status["exitcode"], 1)
# traceback in user code should be formatted nicely
code=textwrap.dedent("""
def foo(arg):
raise Exception(arg)
foo(123)
""")
status = do(code, [], False)
eq_('Traceback (most recent call last):\n' +
' File "<user-code>", line 4, in <module>\n' +
' foo(123)\n' +
' File "<user-code>", line 3, in foo\n' +
' raise Exception(arg)\n' +
'Exception: 123\n', status["log"])
eq_(status["exitcode"], 1)
# argument handling (strings come in as unicode)
code=textwrap.dedent("""
import sys
print sys.argv[1].encode('ascii'), sys.argv[2]
""")
status = do(code, ["hello", 123], False)
eq_(status["log"], "hello 123\n")
eq_(status["exitcode"], 0)
# try killing a long-running process
code=textwrap.dedent("""
import time
print 'hello'
time.sleep(60)
print 'world'
""")
status = do(code, [], True)
eq_(status["log"], "hello\n")
ne_(status["exitcode"], 0)