From 15f35dc1c2eb50a8140f1a0abf45d5aa25fdf66b Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Tue, 22 Sep 2015 11:33:43 +0200 Subject: Add support for execing the user's Python program --- python/interpreter.py | 59 ++++++++++++++++++++++++++++++++++++++++-------- server/handlers.py | 24 +++++++++++++++++++- server/python_session.py | 15 +++++++++++- web/main.js | 10 ++++++++ 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/python/interpreter.py b/python/interpreter.py index 87de3aa..3439ae8 100755 --- a/python/interpreter.py +++ b/python/interpreter.py @@ -7,9 +7,13 @@ import seccomp f = seccomp.SyscallFilter(defaction=seccomp.KILL) # Necessary for Python. +f.add_rule(seccomp.ALLOW, "brk") f.add_rule(seccomp.ALLOW, "exit_group") +f.add_rule(seccomp.ALLOW, "ioctl") +f.add_rule(seccomp.ALLOW, "mmap") +f.add_rule(seccomp.ALLOW, "munmap") f.add_rule(seccomp.ALLOW, "rt_sigaction") -f.add_rule(seccomp.ALLOW, "brk") +f.add_rule(seccomp.ALLOW, "rt_sigreturn") # Mostly harmless. f.add_rule(seccomp.ALLOW, "mprotect") @@ -19,22 +23,59 @@ f.add_rule(seccomp.ALLOW, "read", seccomp.Arg(0, seccomp.EQ, sys.stdin.fileno()) f.add_rule(seccomp.ALLOW, "write", seccomp.Arg(0, seccomp.EQ, sys.stdout.fileno())) f.add_rule(seccomp.ALLOW, "write", seccomp.Arg(0, seccomp.EQ, sys.stderr.fileno())) -f.add_rule(seccomp.ALLOW, "ioctl") -f.add_rule(seccomp.ALLOW, "mmap") -f.add_rule(seccomp.ALLOW, "munmap") - # Needed for finding source code for exceptions. f.add_rule(seccomp.ALLOW, "stat") +# Read-only open. f.add_rule(seccomp.ALLOW, "open", seccomp.Arg(1, seccomp.MASKED_EQ, 0x3, 0)) -f.add_rule(seccomp.ALLOW, "fcntl") +f.add_rule(seccomp.ALLOW, "close") +f.add_rule(seccomp.ALLOW, "read") f.add_rule(seccomp.ALLOW, "fstat") f.add_rule(seccomp.ALLOW, "lseek") -f.add_rule(seccomp.ALLOW, "read") -f.add_rule(seccomp.ALLOW, "close") +f.add_rule(seccomp.ALLOW, "fcntl") # Needed for code.InteractiveConsole. f.add_rule(seccomp.ALLOW, "access") f.add_rule(seccomp.ALLOW, "select") f.load() -code.interact(banner='') +class MyConsole(code.InteractiveConsole): + def interact(self, banner=None): + if banner is not None: + self.write('{}\n'.format(banner)) + + buffer = [] + prompt = '>>> ' + while True: + try: + line = input(prompt) + # Assume we are running the user's program; silence the prompt. + if line == 'exec("""\\': + self.write('\n') + prompt = '' + + buffer.append(line) + source = '\n'.join(buffer) + more = self.runsource(source) + if more: + if prompt: + prompt = '... ' + else: + prompt = '>>> ' + buffer = [] + except KeyboardInterrupt: + prompt = '>>> ' + buffer = [] + self.write('^C\n') + except EOFError: + break + + def runcode(self, code): + try: + exec(code, self.locals) + except KeyboardInterrupt: + # Don't show traceback on SIGINT. + raise + except: + self.showtraceback() + +MyConsole().interact() diff --git a/server/handlers.py b/server/handlers.py index 52a33f5..08994a0 100644 --- a/server/handlers.py +++ b/server/handlers.py @@ -106,7 +106,27 @@ class Query(CodeqService): request.reply(result) -# Push stdin to the session's Python interpreter. TODO: convert to async handling +# Push user program to the Python interpreter to be exec'd. +class PythonExec(CodeqService): + def process(self, request): + program = request.data.get('program') + if program is None: + request.reply({'code': 1, 'message': 'No program specified'}) + else: + python = request.session.get_python() + python.exec(program) + request.reply({'code': 0, 'message': 'ok'}) + + +# Send an interrupt to the Python interpreter. +class PythonStop(CodeqService): + def process(self, request): + python = request.session.get_python() + python.stop() + request.reply({'code': 0, 'message': 'ok'}) + + +# Push stdin to the Python interpreter. class PythonPush(CodeqService): def process(self, request): text = request.data.get('text') @@ -194,7 +214,9 @@ incoming_handlers = { 'logout': None, 'activity': Activity(), 'query': Query(), + 'python_exec': PythonExec(), 'python_push': PythonPush(), + 'python_stop': PythonStop(), 'hint': Hint(), 'test': Test() } diff --git a/server/python_session.py b/server/python_session.py index 62fcbf8..6f2c999 100644 --- a/server/python_session.py +++ b/server/python_session.py @@ -6,6 +6,7 @@ import io import multiprocessing import os import queue +import signal import subprocess import sys import threading @@ -55,9 +56,15 @@ class PythonSession(object): p.terminate() return results + def exec(self, program): + self._control.put_nowait(('exec', program)) + def push(self, stdin): self._control.put_nowait(('push', stdin)) + def stop(self): + self._control.put_nowait(('stop', None)) + def destroy(self): self._control.put_nowait(('done', None)) @@ -125,9 +132,15 @@ def _interpreter(control, callback): # Get a control command. try: cmd, data = control.get_nowait() - if cmd == 'push': + if cmd == 'exec': + exec_str = 'exec("""\\\n{}\n""")\n'.format(data.replace('"', '\\"')) + proc.stdin.write(exec_str.encode('utf-8')) + proc.stdin.flush() + elif cmd == 'push': proc.stdin.write(data.encode('utf-8')) proc.stdin.flush() + elif cmd == 'stop': + proc.send_signal(signal.SIGINT) elif cmd == 'done': break except: diff --git a/web/main.js b/web/main.js index 03e9dad..fb393c8 100644 --- a/web/main.js +++ b/web/main.js @@ -132,11 +132,21 @@ var guiHandlers = { sendDataToPython(message).then(session.send, session.end).done(); }, + 'python_exec': function actionPythonExec(session, message) { + logger.debug('Received python_exec from GUI'); + sendDataToPython(message).then(session.send, session.end).done(); + }, + 'python_push': function actionPythonPush(session, message) { logger.debug('Received python_push from GUI'); sendDataToPython(message).then(session.send, session.end).done(); }, + 'python_stop': function actionPythonStop(session, message) { + logger.debug('Received python_stop from GUI'); + sendDataToPython(message).then(session.send, session.end).done(); + }, + 'hint': function actionHint(session, message) { logger.debug('Received hint from GUI'); sendDataToPython(message).then(session.send, session.end).done(); -- cgit v1.2.1 From 0cc91946829c64e894034bdc5cabdd80f983d5be Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Tue, 22 Sep 2015 12:05:20 +0200 Subject: Create the hint-list in individual test functions This will allow us to add more hints, for example "did you know?"-type information when the program is correct. --- server/prolog_session.py | 5 ++--- server/python_session.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/server/prolog_session.py b/server/prolog_session.py index 8d9926d..ccfb91c 100644 --- a/server/prolog_session.py +++ b/server/prolog_session.py @@ -118,10 +118,9 @@ class PrologSession(object): solved_problems = [p for p in CodeqUser.solved_problems(session.get_uid(), language) if p != (problem_group, problem)] try: - n_correct, n_all = problem_module.test(program, solved_problems) - hints = [{'id': 'test_results', 'args': {'passed': n_correct, 'total': n_all}}] + passed, hints = problem_module.test(self.run, program) except AttributeError as ex: - hints = [{'id': 'test_results', 'args': {'passed': 0, 'total': 0}}] + hints = [{'id': 'system_error', 'args': {'message': 'test function does not exist'}}] self._instantiate_and_save_hints(language_module, problem_module, hints) return hints diff --git a/server/python_session.py b/server/python_session.py index 6f2c999..975af7f 100644 --- a/server/python_session.py +++ b/server/python_session.py @@ -93,10 +93,9 @@ class PythonSession(object): problem_module = problems.load_problem(language, problem_group, problem, 'common') try: - n_correct, n_all = problem_module.test(self.run, program) - hints = [{'id': 'test_results', 'args': {'passed': n_correct, 'total': n_all}}] + passed, hints = problem_module.test(self.run, program) except AttributeError as ex: - hints = [{'id': 'test_results', 'args': {'passed': 0, 'total': 0}}] + hints = [{'id': 'system_error', 'args': {'message': 'test function does not exist'}}] self._instantiate_and_save_hints(language_module, problem_module, hints) return hints -- cgit v1.2.1 From f3567827f2bb24ed8505a2725e8e7608396feeed Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Tue, 22 Sep 2015 12:18:26 +0200 Subject: Oops, overzealous copy/paste --- server/prolog_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/prolog_session.py b/server/prolog_session.py index ccfb91c..ebd53fb 100644 --- a/server/prolog_session.py +++ b/server/prolog_session.py @@ -118,7 +118,7 @@ class PrologSession(object): solved_problems = [p for p in CodeqUser.solved_problems(session.get_uid(), language) if p != (problem_group, problem)] try: - passed, hints = problem_module.test(self.run, program) + passed, hints = problem_module.test(program, solved_problems) except AttributeError as ex: hints = [{'id': 'system_error', 'args': {'message': 'test function does not exist'}}] -- cgit v1.2.1 From 2c61fec2140da5ec9a5aee8a7d6d3f0f2d3b0897 Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Tue, 22 Sep 2015 12:22:24 +0200 Subject: Fix an inconsequential typo --- server/socket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/socket.py b/server/socket.py index ce505e3..60f0704 100644 --- a/server/socket.py +++ b/server/socket.py @@ -18,7 +18,7 @@ _transactions_to_socket = {} # keyed by tid, used only when there is no sid ava def processIncomingPacket(receiving_socket, packet): - print('Decocoding JSON: {}'.format(packet)) + print('Decoding JSON: {}'.format(packet)) obj = json.loads(packet) req_type = obj.get('type') # private (meta) requests have the 'type' if req_type == 'connect': -- cgit v1.2.1