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..f7110b772 --- /dev/null +++ b/dmoj/rest.py @@ -0,0 +1,150 @@ +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 +from base64 import b64decode + +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): + self.judge.graded_submissions[-1]['testCaseStatus'] = result.readable_codes() + + def compile_error_packet(self, message): + self.judge.graded_submissions[-1]['compileError'].append(message) + + def compile_message_packet(self, message): + self.judge.graded_submissions[-1]['compileMessage'].append(message) + + def internal_error_packet(self, message): + self.judge.graded_submissions[-1]['internalError'].append(message) + + def begin_grading_packet(self, is_pretested): + pass + + def grading_end_packet(self): + pass + + def batch_begin_packet(self): + pass + + def batch_end_packet(self): + pass + + def current_submission_packet(self): + pass + + def submission_terminated_packet(self): + 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.next_submission_id = 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.next_submission_id + + 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":[] + }) + + 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) + + judge.next_submission_id += 1 + + return jsonify(judge.graded_submissions[submission_id]), 200 + +# GET /submissionResult/{submissionId} +def submission_result_get(submissionId): + judge = get_judge() + return jsonify(judge.graded_submissions[submission_id]), 200 + +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..e92b0bf55 100644 --- a/setup.py +++ b/setup.py @@ -150,16 +150,18 @@ 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), - 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'],