diff options
-rw-r--r-- | snekbox/api/resources/eval.py | 23 | ||||
-rw-r--r-- | snekbox/nsjail.py | 33 | ||||
-rw-r--r-- | tests/api/__init__.py | 8 | ||||
-rw-r--r-- | tests/api/test_eval.py | 5 |
4 files changed, 37 insertions, 32 deletions
diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py index b2f4260..4779557 100644 --- a/snekbox/api/resources/eval.py +++ b/snekbox/api/resources/eval.py @@ -36,7 +36,16 @@ class EvalResource: @validate(REQ_SCHEMA) def on_post(self, req, resp): """ - Evaluate Python code and return the result. + Evaluate Python code and return stdout, stderr, and the return code. + + The return codes mostly resemble those of a Unix shell. Some noteworthy cases: + + - None + The NsJail process failed to launch + - 137 (SIGKILL) + Typically because NsJail killed the Python process due to time or memory constraints + - 255 + NsJail encountered a fatal error Request body: @@ -47,8 +56,9 @@ class EvalResource: Response format: >>> { - ... "input": "print(1 + 1)", - ... "output": "2\\n" + ... "stdout": "2\\n", + ... "stderr": "", + ... "returncode": 0 ... } Status codes: @@ -63,12 +73,13 @@ class EvalResource: code = req.media["input"] try: - output = self.nsjail.python3(code) + result = self.nsjail.python3(code) except Exception: log.exception("An exception occurred while trying to process the request") raise falcon.HTTPInternalServerError resp.media = { - "input": code, - "output": output + "stdout": result.stdout, + "stderr": result.stderr, + "returncode": result.returncode } diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py index 0ebfe0c..ff12ec4 100644 --- a/snekbox/nsjail.py +++ b/snekbox/nsjail.py @@ -100,8 +100,8 @@ class NsJail: # Treat fatal as error. log.error(msg) - def python3(self, code: str) -> str: - """Execute Python 3 code in an isolated environment and return stdout or an error.""" + def python3(self, code: str) -> subprocess.CompletedProcess: + """Execute Python 3 code in an isolated environment and return the completed process.""" with NamedTemporaryFile() as nsj_log: args = ( self.nsjail_binary, "-Mo", @@ -125,29 +125,16 @@ class NsJail: self.python_binary, "-ISq", "-c", code ) - try: - msg = "Executing code..." - if DEBUG: - msg = f"{msg[:-3]}:\n{textwrap.indent(code, ' ')}" - log.info(msg) + msg = "Executing code..." + if DEBUG: + msg = f"{msg[:-3]}:\n{textwrap.indent(code, ' ')}" + log.info(msg) - proc = subprocess.run(args, capture_output=True, env=ENV, text=True) + try: + result = subprocess.run(args, capture_output=True, env=ENV, text=True) except ValueError: - return "ValueError: embedded null byte" + return subprocess.CompletedProcess(args, None, "", "ValueError: embedded null byte") self._parse_log(nsj_log) - if proc.returncode == 0: - output = proc.stdout - elif proc.returncode == 1: - output = proc.stderr - elif proc.returncode == 109: - return "timed out or memory limit exceeded" - elif proc.returncode == 255: - return "permission denied (root required)" - elif proc.returncode: - return f"unknown error, code: {proc.returncode}" - else: - return "unknown error, no error code" - - return output + return result diff --git a/tests/api/__init__.py b/tests/api/__init__.py index fd4679a..dcee5b5 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -1,3 +1,4 @@ +from subprocess import CompletedProcess from unittest import mock from falcon import testing @@ -11,7 +12,12 @@ class SnekAPITestCase(testing.TestCase): self.patcher = mock.patch("snekbox.api.resources.eval.NsJail", autospec=True) self.mock_nsjail = self.patcher.start() - self.mock_nsjail.return_value.python3.return_value = "test output" + self.mock_nsjail.return_value.python3.return_value = CompletedProcess( + args=[], + returncode=0, + stdout="output", + stderr="error" + ) self.addCleanup(self.patcher.stop) self.app = SnekAPI() diff --git a/tests/api/test_eval.py b/tests/api/test_eval.py index bcd0ec4..03f0e39 100644 --- a/tests/api/test_eval.py +++ b/tests/api/test_eval.py @@ -9,8 +9,9 @@ class TestEvalResource(SnekAPITestCase): result = self.simulate_post(self.PATH, json=body) self.assertEqual(result.status_code, 200) - self.assertEqual(body["input"], result.json["input"]) - self.assertEqual("test output", result.json["output"]) + self.assertEqual("output", result.json["stdout"]) + self.assertEqual("error", result.json["stderr"]) + self.assertEqual(0, result.json["returncode"]) def test_post_invalid_schema_400(self): body = {"stuff": "foo"} |