diff options
-rw-r--r-- | static/website/js/cloud.js | 3 | ||||
-rw-r--r-- | website/helpers.py | 8 | ||||
-rw-r--r-- | website/timeout.py | 204 | ||||
-rw-r--r-- | website/views.py | 6 |
4 files changed, 216 insertions, 5 deletions
diff --git a/static/website/js/cloud.js b/static/website/js/cloud.js index 0c01c62..523cea0 100644 --- a/static/website/js/cloud.js +++ b/static/website/js/cloud.js @@ -111,6 +111,8 @@ $(document).ready(function() { data: { csrfmiddlewaretoken: csrfmiddlewaretoken, code: code, + book_id: $("#books").val(), + chapter_id: $("#chapters").val(), example_id: $("#examples").val() }, dataType: "text", @@ -127,5 +129,4 @@ $(document).ready(function() { } }); }); - }); diff --git a/website/helpers.py b/website/helpers.py index f137541..be8165b 100644 --- a/website/helpers.py +++ b/website/helpers.py @@ -3,7 +3,7 @@ import os, re, sys, time, subprocess from soc.settings import PROJECT_DIR from timeout import TimerTask -def scilab_run(code, token, example_id): +def scilab_run(code, token, book_id, dependency_exists): #Check for system commands system_commands = re.compile( 'unix\(.*\)|unix_g\(.*\)|unix_w\(.*\)|unix_x\(.*\)|unix_s\(.*\)|host|newfun|execstr|ascii|mputl|dir\(\)' @@ -37,14 +37,16 @@ def scilab_run(code, token, example_id): #traps even syntax errors eg: endfunton f = open(file_path, "w") f.write('mode(2);\n') + if dependency_exists: + f.write('getd("/var/www/scilab_in/uploads/{0}/DEPENDENCIES/");'.format(book_id)) + f.write('lines(0);\n') f.write(unicode(code)) f.write('\nquit();') f.close() - #this makes it possible to execute scilab without the problem of \ #getting stuck in the prompt in case of error - cmd = 'printf "lines(0)\nexec(\'{0}\',2);\nquit();"'.format(file_path) + cmd = 'printf "exec(\'{0}\',2);\nquit();"'.format(file_path) cmd += ' | /home/cheese/scilab-5.4.1/bin/scilab-adv-cli -nw' task = TimerTask(cmd, timeout=10) diff --git a/website/timeout.py b/website/timeout.py new file mode 100644 index 0000000..4c5f6cb --- /dev/null +++ b/website/timeout.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python + +## This program is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. + +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +## You should have received a copy of the GNU General Public License +## along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Usage: ./timeout.py TIMEOUT COMMAND... + +This is a shell script that runs a command for up to TIMEOUT seconds, sending +the command a SIGKILL once the timeout expires. If the command exits sooner, +then this script will exit as well. + +This functionality is also exposed as a Python module. The TimerTask class +handles running a command in a child process. There are many modules in the +Python standard library that do this, however. Here's what's different about +TimerTask: + +You can set a timeout such that TimerTask.wait() returns when either 1) the +child process exits naturally or 2) the timeout expires and the child process +receives a specified signal causing it to terminate. TimerTask.wait() returns +whenever the *sooner* of 1) or 2) happens. + +TimerTask uses the SIGALRM signal for its timeout, and so will interfere with +programs that need SIGALRM for other purposes. If you do not specify the use of +a timeout, however, no signal handling will be used. + +Finally, the child process is, by default, run in its own process group, to make +it easier to clean up the child process along with any other processes it may +have spawned. +""" + +import errno,os,signal,subprocess,sys + +class TimerTask: + def __init__( self, command, timeout=None, + timeoutSignal=signal.SIGKILL, raisePrevSigalrmError=True, + childProcessGroup=0 ): + """Create a new TimerTask +command : string : the command to run +timeout : int : timeout (in seconds), or None for no timeout (default: None) +signal : int : the signal to send to the child process at timeout (default: SIGKILL) +raisePrevSigalrmError : bool : if True, throw an exception if there's + a previous non-nop SIGALRM handler installed (default: True) +childProcessGroup : int : if 0 (default), put child process into its own process group + if non-zero, put child process into the specified process group + if None, inherit the parent's process group +""" + assert isinstance( command, str ) or isinstance( command, list ) + self.command = command + + if timeout is not None: + assert isinstance( timeout, int ) + assert timeout > 0 + self.timeout = timeout + + assert isinstance( timeoutSignal, int ) + self.timeoutSignal = timeoutSignal + + self.prevAlarmHandler = None + self.raisePrevSigalrmError = raisePrevSigalrmError + + if None == childProcessGroup: + self.preExecFun = None + else: + assert isinstance( childProcessGroup, int ) + self.preExecFun = (lambda : os.setpgid(0,childProcessGroup)) + + + def run( self, + stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + shell=True, cwd=None, env=None ): + """Takes the same arguments as Python's subprocess.Popen(), with the +following exceptions: + +1. this version runs the command in a shell by default (shell=True) +2. this function needs the preexec_fn hook, so that is not available + +This function returns the result from subprocess.Popen() (a subprocess +object), so you can read from pipes, poll, etc. +""" + + self.subprocess = subprocess.Popen( self.command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + + # runs in the child + # process before + # the exec(), putting + # the child process into + # its own process group + preexec_fn=self.preExecFun, + + shell=shell, + cwd=cwd, + env=env ) + self.pgid = os.getpgid( self.subprocess.pid ) + + # Setup the SIGALRM handler: we use a lambda as a "curried" + # function to bind some values + if self.timeout is not None: + + if signal.getsignal( signal.SIGALRM ) not in (None, signal.SIG_IGN, signal.SIG_DFL): + # someone is using a SIGALRM handler! + if self.raisePrevSigalrmError: + ValueError( "SIGALRM handler already in use!" ) + + self.prevAlarmHandler = signal.getsignal( signal.SIGALRM ) + + signal.signal( signal.SIGALRM, + lambda sig,frame : os.killpg(self.pgid,self.timeoutSignal) ) + + # setup handler before scheduling signal, to eliminate a race + signal.alarm( self.timeout ) + + return self.subprocess + + def cancelTimeout(self): + """If we're using a timeout, cancel the SIGALRM timeout when + the child is finished, and restore the previous SIGALRM handler""" + if self.timeout is not None: + signal.alarm( 0 ) + signal.signal( signal.SIGALRM, self.prevAlarmHandler ) + pass + return + + def wait(self): + """Wait for the child process to exit, or the timeout to expire, +whichever comes first. + +This function returns the same thing as Python's +subprocess.wait(). That is, this function returns the exit status of +the child process; if the return value is -N, it indicates that the +child was killed by signal N. """ + + try: + self.subprocess.wait() + except OSError, e: + + # If the child times out, the wait() syscall can get + # interrupted by the SIGALRM. We should then only need to + # wait() once more for the child to actually exit. + + if e.errno == errno.EINTR: + self.subprocess.wait() + else: + raise e + pass + + self.cancelTimeout() + + assert self.subprocess.poll() is not None + return self.subprocess.poll() + + + def kill(self, deathsig=signal.SIGKILL): + """Kill the child process. Optionally specify the signal to be used +(default: SIGKILL)""" + try: + os.killpg( self.pgid, deathsig ) + except OSError, e: + if e.errno == errno.ESRCH: + # We end up here if the process group has already exited, so it's safe to + # ignore the error + pass + else: + raise e + pass + + self.cancelTimeout() + + +if __name__ == "__main__": + if len(sys.argv) <= 2: + print "Usage:", sys.argv[0], "TIMEOUT COMMAND..." + print + + descrip = """Runs COMMAND for up to TIMEOUT seconds, sending COMMAND a SIGKILL if it +attempts to run longer. Exits sooner if COMMAND does so, passing along COMMAND's +exit code. + +All of COMMAND's children run in a new process group, and the entire group is +SIGKILL'ed when the timeout expires. """ + print descrip + + sys.exit( 0 ) + pass + + t = TimerTask( " ".join(sys.argv[2:]), + timeout=int(sys.argv[1]) ) + t.run() + e = t.wait() + sys.exit( e ) + diff --git a/website/views.py b/website/views.py index 050ec92..7d1a3a3 100644 --- a/website/views.py +++ b/website/views.py @@ -78,7 +78,11 @@ def ajax_code(request): def ajax_execute(request): if request.method == "POST": code = request.POST['code'] + book_id = request.POST.get('book_id', None) + chapter_id = request.POST.get('chapter_id ', None) example_id = request.POST.get('example_id', None) token = request.POST['csrfmiddlewaretoken'] - data = scilab_run(code, token, example_id) + dependency_exists = TextbookCompanionExampleDependency.objects.using('scilab')\ + .filter(example_id=example_id).exists() + data = scilab_run(code, token, book_id, dependency_exists) return render(request, 'website/templates/ajax-execute.html', data) |