From 3368d2a2dcbe2a347f28d3537aab04823b44baba Mon Sep 17 00:00:00 2001 From: Simon Pettersson Date: Mon, 26 Mar 2018 08:22:52 +0200 Subject: [PATCH 1/4] Begun REST server development --- dmoj/api/api.yaml | 125 +++++++++++++++++++++++++++++++++++++ dmoj/rest.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 + 3 files changed, 281 insertions(+) create mode 100644 dmoj/api/api.yaml create mode 100644 dmoj/rest.py diff --git a/dmoj/api/api.yaml b/dmoj/api/api.yaml new file mode 100644 index 000000000..d16a3b3d2 --- /dev/null +++ b/dmoj/api/api.yaml @@ -0,0 +1,125 @@ +--- +swagger: "2.0" +info: + description: "A modern contest platform for the modern web.\n" + version: "1.0.0" + title: "DMOJ REST API" + contact: + email: "simon.v.pettersson@gmail.com" + license: + name: "GNU Affero General Public License v3.0" + url: "https://raw.githubusercontent.com/DMOJ/judge/master/LICENSE" +host: "localhost:8080" +basePath: "/simonvpe/dmoj/1.0.0" +tags: +- name: "submission" + description: "Problem Submission" +- name: "result" + description: "Submission Result" +schemes: +- "https" +- "http" +paths: + /submission: + post: + tags: + - "submission" + summary: "Submit a solution to the judge" + operationId: "add_submission" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Submission data" + required: true + schema: + $ref: "#/definitions/Submission" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/SubmissionResponse" + 405: + description: "Invalid input" + x-swagger-router-controller: "dmoj.rest" + /submissionResult/{submissionId}: + get: + tags: + - "result" + summary: "Retrieve the result of a submission" + operationId: "submission_result_get" + produces: + - "application/json" + parameters: + - name: "submissionId" + in: "path" + description: "id of submission to return" + required: true + type: "integer" + format: "int64" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/SubmissionResult" + x-swagger-router-controller: "dmoj.rest" +definitions: + Submission: + type: "object" + required: + - "languageId" + - "problemId" + - "sourceCode" + properties: + problemId: + type: "string" + description: "id of problem to grade" + languageId: + type: "string" + description: "language of problem to grade" + sourceCode: + type: "string" + format: "base64" + description: "the source code to grade" + timeLimit: + type: "number" + description: "time limit for grading, in seconds" + memoryLimit: + type: "integer" + format: "int64" + description: "memory limit for grading, in kilobytes" + default: 65536 + example: + timeLimit: 0.8008281904610115 + sourceCode: "sourceCode" + languageId: "languageId" + memoryLimit: 6 + problemId: "problemId" + SubmissionResponse: + type: "object" + required: + - "submissionId" + properties: + submissionId: + type: "integer" + format: "int64" + description: "id of the submission" + example: + submissionId: 0 + SubmissionResult: + type: "object" + required: + - "submissionId" + properties: + submissionId: + type: "integer" + format: "int64" + description: "id of the submission" + example: + submissionId: 0 +externalDocs: + description: "Find out more about Swagger" + url: "http://swagger.io" diff --git a/dmoj/rest.py b/dmoj/rest.py new file mode 100644 index 000000000..1455b22f0 --- /dev/null +++ b/dmoj/rest.py @@ -0,0 +1,154 @@ +import os, sys, connexion, six, logging +from dmoj import judgeenv, executors +from dmoj.judge import Judge +from flask import jsonify, current_app +from operator import itemgetter +from flask import g +from enum import Enum + +class JudgeState(Enum): + FAILED = 0 + SUCCESS = 1 + +class LocalPacketManager(object): + def __init__(self, judge): + self.judge = judge + + def _receive_packet(self, packet): + pass + + def supported_problems_packet(self, problems): + pass + + def test_case_status_packet(self, position, result): + print("TEST CASE STATUS %s, \"%s\"" % (str(position), result.readable_codes())) + pass + + def compile_error_packet(self, log): + self.judge.compile_error.append(log) + print("COMPILE ERROR: %s" % log) + pass + + def compile_message_packet(self, log): + self.judge.compile_message.append(log) + print("COMPILER MESSAGE: %s" % log) + + def internal_error_packet(self, message): + self.judge.internal_error.append(log) + print("INTERNAL ERROR: %s" % message) + + def begin_grading_packet(self, is_pretested): + print("BEGIN GRADING") + pass + + def grading_end_packet(self): + print("GRADING END") + pass + + def batch_begin_packet(self): + pass + + def batch_end_packet(self): + pass + + def current_submission_packet(self): + pass + + def submission_terminated_packet(self): + print("SUBMISSION TERMINATED") + pass + + def submission_acknowledged_packet(self, sub_id): + pass + + def run(self): + pass + + def close(self): + pass + + +class LocalJudge(Judge): + def __init__(self): + super(LocalJudge, self).__init__() + self.packet_manager = LocalPacketManager(self) + self.submission_id_counter = 0 + self.graded_submissions = [] + +def get_judge(): + judge = getattr(g, '_judge', None) + if judge is None: + g._judge = LocalJudge() + return g._judge + +# POST /submission +def add_submission(body): + judge = get_judge() + body = connexion.request.get_json() + problem_id = body['problemId'] + language_id = body['languageId'] + time_limit = body['timeLimit'] + memory_limit = body['memoryLimit'] + source = body['sourceCode'] + + if problem_id not in map(itemgetter(0), judgeenv.get_supported_problems()): + return jsonify({ + 'error': "unknown problem %s" % problem_id + }), 405 + + if language_id not in executors.executors: + return jsonify({'error': "unknown languae %s" % language_id}), 405 + + if time_limit <= 0: + return jsonify({'error': "timeLimit must be >= 0"}), 405 + + if memory_limit <= 0: + return jsonify({'error': "memoryLimit must be >= 0"}), 405 + + submission_id = judge.submission_id_counter + judge.submission_id_counter += 1 + + judge.graded_submissions.append({ + "submissionId": submission_id, + "problemId": problem_id, + "languageId": language_id, + "sourceCode": source, + "timeLimit": time_limit, + "memoryLimit": memory_limit, + "compileError": [], + "compileMessage": [], + "testCaseResults": [], + "internalError":[] + }) + + judge.begin_grading(submission_id, problem_id, language_id, source, time_limit, + memory_limit, False, False, blocking=True) + + return jsonify(judge.graded_submissions[submission_id]), 200 + +# GET /submissionResult/{submissionId} +def submission_result_get(submissionId): + judge = get_judge() + return 'do some magic 3!' + +def main(): + judgeenv.load_env(cli=True) + executors.load_executors() + + logging.basicConfig(filename=judgeenv.log_file, level=logging.INFO, + format='%(levelname)s %(asctime)s %(module)s %(message)s') + + for warning in judgeenv.startup_warnings: + print(ansi_style('#ansi[Warning: %s](yellow)' % warning)) + del judgeenv.startup_warnings + print() + + server = connexion.FlaskApp(__name__, specification_dir='api/') + with server.app.app_context(): + judge = get_judge() + judge.listen() + server.add_api('api.yaml') + server.run(port=8080) + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index a27ebc559..4e1d9a7f2 100644 --- a/setup.py +++ b/setup.py @@ -150,12 +150,14 @@ def unavailable(self, e): 'dmoj.cptbox': ['syscalls/aliases.list', 'syscalls/*.tbl'], 'dmoj.executors': ['csbox.exe', 'java-sandbox.jar', '*.policy'], 'dmoj.wbox': ['getaddr*.exe', 'dmsec*.dll'], + '': ['api/api.yaml'] }, entry_points={ 'console_scripts': [ 'dmoj = dmoj.judge:main', 'dmoj-cli = dmoj.cli:main', 'dmoj-autoconf = dmoj.executors.autoconfig:main', + 'dmoj-rest = dmoj.rest:main' ], }, ext_modules=cythonize(extensions), From 6093cc93e7071bd57a70540d7628a053d835f788 Mon Sep 17 00:00:00 2001 From: Simon Pettersson Date: Mon, 26 Mar 2018 08:27:29 +0200 Subject: [PATCH 2/4] Added connexion as dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4e1d9a7f2..e92b0bf55 100644 --- a/setup.py +++ b/setup.py @@ -161,7 +161,7 @@ def unavailable(self, e): ], }, ext_modules=cythonize(extensions), - install_requires=['watchdog', 'pyyaml', 'ansi2html', 'termcolor', 'pygments', 'six', 'setproctitle'], + install_requires=['watchdog', 'pyyaml', 'ansi2html', 'termcolor', 'pygments', 'six', 'setproctitle', 'connexion'], tests_require=['mock', 'requests'], extras_require={ 'test': ['mock'], From 2046cd60ae30d0bc0f86d11fb0c921d487c972c4 Mon Sep 17 00:00:00 2001 From: Simon Pettersson Date: Fri, 30 Mar 2018 21:09:31 +0200 Subject: [PATCH 3/4] Working but very simple version --- dmoj/rest.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/dmoj/rest.py b/dmoj/rest.py index 1455b22f0..413bf727a 100644 --- a/dmoj/rest.py +++ b/dmoj/rest.py @@ -21,28 +21,21 @@ def supported_problems_packet(self, problems): pass def test_case_status_packet(self, position, result): - print("TEST CASE STATUS %s, \"%s\"" % (str(position), result.readable_codes())) - pass + self.judge.graded_submissions[-1]['testCaseStatus'] = result.readable_codes() - def compile_error_packet(self, log): - self.judge.compile_error.append(log) - print("COMPILE ERROR: %s" % log) - pass + def compile_error_packet(self, message): + self.judge.graded_submissions[-1]['compileError'].append(message) - def compile_message_packet(self, log): - self.judge.compile_message.append(log) - print("COMPILER MESSAGE: %s" % log) + def compile_message_packet(self, message): + self.judge.graded_submissions[-1]['compileMessage'].append(message) def internal_error_packet(self, message): - self.judge.internal_error.append(log) - print("INTERNAL ERROR: %s" % message) + self.judge.graded_submissions[-1]['internalError'].append(message) def begin_grading_packet(self, is_pretested): - print("BEGIN GRADING") pass def grading_end_packet(self): - print("GRADING END") pass def batch_begin_packet(self): @@ -55,7 +48,6 @@ def current_submission_packet(self): pass def submission_terminated_packet(self): - print("SUBMISSION TERMINATED") pass def submission_acknowledged_packet(self, sub_id): @@ -72,7 +64,7 @@ class LocalJudge(Judge): def __init__(self): super(LocalJudge, self).__init__() self.packet_manager = LocalPacketManager(self) - self.submission_id_counter = 0 + self.next_submission_id = 0 self.graded_submissions = [] def get_judge(): @@ -105,9 +97,8 @@ def add_submission(body): if memory_limit <= 0: return jsonify({'error': "memoryLimit must be >= 0"}), 405 - submission_id = judge.submission_id_counter - judge.submission_id_counter += 1 - + submission_id = judge.next_submission_id + judge.graded_submissions.append({ "submissionId": submission_id, "problemId": problem_id, @@ -120,9 +111,11 @@ def add_submission(body): "testCaseResults": [], "internalError":[] }) - + judge.begin_grading(submission_id, problem_id, language_id, source, time_limit, memory_limit, False, False, blocking=True) + + judge.next_submission_id += 1 return jsonify(judge.graded_submissions[submission_id]), 200 From e21b1a43452c5dd327d4686263fb5b4d98b2656e Mon Sep 17 00:00:00 2001 From: Simon Pettersson Date: Sat, 31 Mar 2018 16:08:48 +0200 Subject: [PATCH 4/4] Accepting base64 encoded source --- dmoj/rest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dmoj/rest.py b/dmoj/rest.py index 413bf727a..f7110b772 100644 --- a/dmoj/rest.py +++ b/dmoj/rest.py @@ -5,6 +5,7 @@ from operator import itemgetter from flask import g from enum import Enum +from base64 import b64decode class JudgeState(Enum): FAILED = 0 @@ -111,7 +112,9 @@ def add_submission(body): "testCaseResults": [], "internalError":[] }) - + + source = b64decode(source).decode('utf-8') + print(source) judge.begin_grading(submission_id, problem_id, language_id, source, time_limit, memory_limit, False, False, blocking=True) @@ -122,7 +125,7 @@ def add_submission(body): # GET /submissionResult/{submissionId} def submission_result_get(submissionId): judge = get_judge() - return 'do some magic 3!' + return jsonify(judge.graded_submissions[submission_id]), 200 def main(): judgeenv.load_env(cli=True)