自建 Online Judge 系统

Published by rcdfrd on 2022-05-26

自建 Online Judge 系统

前言

先稍微介绍一下什么是 Online Judge(底下简称 OJ)系统,简单来说就是像 leetcode 那样啦,可以送出程式码解题,然后让系统去批改,并且得到最后的结果。

在 leetcode 流行以前,最知名的 OJ 大概就是 UVa Online Judge,俗称 ACM

如果刚好有需求,想要自己架一个 OJ 的话,该怎么办呢?

开源 OJ 系统

在网路上搜寻一下,可以找到几个开源的 OJ 系统,其中星星数比较多看起来也比较稳定的是底下三个:

  1. DMOJ
  2. NOJ
  3. QDUOJ

DMOJ

这一套功能看起来最丰富最完整,而且支援的语言最多,可以到 60 几种!而且还支援 Google, Facebook, Github 这些第三方登入。后端是 Python 写的,而且一直持续有在维护,文件也满完整的。

唯一的缺点大概就是介面比较阳春一点,没那么讨喜。

NOJ

中国南京邮电大学开源出来的系统,是用 Laravel 写成的。介面使用 Material UI,看起来比较现代,但是文件比较不完整。

QDUOJ

中国青岛大学开源出来的,后端是 Python + Django,前端是 Vue,采用 docker 部署简单快速,支援的程式语言有:C, C++, Java 跟 Python。介面的部分则是使用 Ant Design。

想要架哪一套就是根据自己需求而定,如果 GitHub 上提供的文件完整的话,照着做就行了。若是不完整也可以透过 Issue 提问,英文不好的话也不需要太过担心,这三个 repo 用中文应该也都可以通。

而我最后选择的是最后一套,青岛大学开源出来的 OJ。会选这一套是因为介面我满喜欢的,然后是这三套里面部署最容易的一套。

部署流程在这边:https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0,因为是采用 docker 部署,所以真的容易,基本上就是把 docker-compose.yml 拉下来然后跑个指令就搞定了。

进一步研究 QDUOJ

首先我们来看一下这个系统的架构,在 GitHub 的文件上有写底下一共分成几个模组:

  1. Backend(Django): https://github.com/QingdaoU/OnlineJudge
  2. Frontend(Vue): https://github.com/QingdaoU/OnlineJudgeFE
  3. Judger Sandbox(Seccomp): https://github.com/QingdaoU/Judger
  4. JudgeServer(A wrapper for Judger): https://github.com/QingdaoU/JudgeServer

而我们最关心的问题(如何新增语言),已经有人在 Issue 里问过了:How to add more language support, such as Ruby

里面提到只要修改这个档案即可:https://github.com/QingdaoU/OnlineJudge/blob/master/judge/languages.py

这档案就是一些设定,而这边你也可以大概猜出 Judge Server 会做什么事情。在这里,每个语言的设定都会有 compile_command 跟 command,前者是来拿编译的指令,后者是拿来跑程式的指令。由于这个 OJ 的输出入都是透过 stdin/stdout,所以当你想要新增一种新的程式语言的时候,只要跟系统说该怎么去执行就好。

相反地,有些 OJ 是采用 function 的方式来填空,例如说开头提到的 leetcode,这时候若是要新增一个语言就会比较麻烦一些,因为你要额外再提供 function 的模板。

照理来说我们只要再如法泡制,加上这样的设定就行了:

js_lang_config = { "run" : { "exe_name" : "solution.js" , "command" : "/usr/bin/nodejs {exe_path}" , "seccomp_rule" : "general" ,    } }

但若是这样去跑,会发现有问题,搜了一下发现已经有人反映过:problem with addding js to language configs,解法是把 seccomp_rule 设成 None。

什么是 seccomp 呢?这就跟 OJ 的原理有关了!大家可以先仔细想想 OJ 中最重要的一个问题:

要如何安全地执行使用者提交的程式码?

若是不知道这问题是在问什么,可以想像以下情形:

  1. 有人写了一行重开机的程式码怎么办?
  2. 有人写了一个无穷回圈怎么办?
  3. 有人写了一个会把主机帐号密码传送到外部的程式怎么办?

由此可以看出,执行程式码可没有那么简单,而这块也是 OJ 最核心的一部分。

QDUOJ 的 Judger 原始码在这边:https://github.com/QingdaoU/Judger/tree/newnew/src

是用 C 写的,会 fork 一个新的 process,设定一些规则之后用 execve 来执行指令。在程式码里面也可以看出是使用 seccomp 这个东西来防止我们上面所提到的内容。

总之呢,QDUOJ 分层做得很不错,执行流程大概是这样的:

  1. 进入 Vue 做的前端页面
  2. 送出程式码,call 后端 API(Python)
  3. 后端 API 再呼叫 Judge Server API(Go)
  4. Judge Server API 呼叫 Judger 执行指令(C,execve + seccomp 执行)

所以每一个专案负责自己的事项,各司其职。

再讲回来前面提到要加上 JavaScript 这一块,尽管把 seccomp_rule 设成 None 以后,执行 JavaScript 依然会出现错误。我研究了一两天,发现问题是出在题目的记忆体限制太小,我猜测是 Node.js 要执行时本来就会吃比较多记忆体,只要把记忆体改大(例如说 1024MB)就搞定了。

不过还没结束,还有最后一个问题,那就是 ubuntu 16.04 上的 Node.js 版本满旧的,要换成新的才能使用 ES6 那些语法,解法是去改 JudgeServer 的 Dockerfile,新增一个安装 Node.js 新版的指令就好。

都改完以后,就可以来部署自己的版本了!只要先把 docker image build 好,然后更改我们最前面操作的 docker-compose 档案就可以了。

虽然说上面讲的云淡风轻,但那时候我在找 Node.js 到底为什么会一直 Runtime error 的时候找到快崩溃,因为错误讯息满不明确的,我一直以为是指令有错,是后来我才灵机一动想说:「咦,该不会是其他问题吧」,才发现是记忆体问题。

总而言之呢,如果你没有想要改东西,只是单纯想要部署的话,在这边诚心推荐 QDUOJ,部署真的简单方便,介面也好看。

自己写一个 OJ

我后来在研究资料的时候找到一些不错的开源解法,以后有人想要自己写的话可以参考考。

第一个是 IOI 开源出来的 sandbox:isolate,可以安全地执行指令。

第二个更神奇,直接给你 Judge 的 API,而且是免费的:Judge0 API。只要按照他的格式把输入传进去,就会跟你说判题结果,所以连 Judge Server 都可以不用自己做。