学院一直是有一个自己的oj的,但是由于最近判题崩了,需要修复一下,拿到判题代码,打开却是一手node.js,让我一个搞Java的着实懵逼,因为以前学过点js,摸清判题逻辑,一步一步console.log来调bug,最后还是太复杂,把心态调崩了。最后想了了想判题就是那个流程,还是自己写一个吧,而且以前的判题只支持python2,现在谁要用python2啊。
好吧,直接开始开发:判题需要几个步骤:
1.在linux搭建编译器环境:gcc g++ java python2 python3 pascal
2.根据源码的类型创建相应的源文件(.c .cpp .java 等)
3.编译对应的源文件
4.运行程序,使用测试用例测试得出时间消耗和内存消耗。
这里最棘手的还是第四步:怎么知道内存和时间消耗?我再网上不断的查资料,后来发现几乎没有,找了很久找到一个前辈有一篇开发oj的博客,纯用python写的。由于自己对于python仅仅是入门语法,纯用python开发对于我来讲确实有点难度。但是思路是可以借鉴的。
里面提到过一个思路:在判题脚本中运行编译之后产生的可执行文件,返回一个pid,使用过linux的小伙伴应该都知道pid(通过这个pid就能找到所运行的那个程序,在top里面就可实时知道这个进程的内存消耗),在python调用linux操作系统的api就可以持续的知道这个进程的内存消耗情况,那么在judge脚本中写个死循环,不断调用操作系统api来获取内存,当时间限制过了之后这个进程还没结束,那么就可以得出结果了(timelimit)。
博客链接:https://www.cnblogs.com/ma6174/archive/2013/05/12/3074034.html
但是后面博主推荐了一个包装这些操作的python模块lorun
。
项目地址:https://github.com/lodevil/Lo-runner
这个使用很简单:
args是一个运行命令;
fd_in是一个输入流,也就是我们判题的测试文件(.in文件);
fd_out是程序的输出流,运行结束需要这个内容来判断程序是否正确的。
timelimit、memorylimit就是我们的时间空间限制啦。
调用lorun模块的run(runcfg)方法就会知道时间空间消耗。
runcfg = {
'args':['./m'],
'fd_in':fin.fileno(),
'fd_out':ftemp.fileno(),
'timelimit':1000, #in MS
'memorylimit':20000, #in KB
}
rst = lorun.run(runcfg)
通过这个模块就解决了我棘手的问题。那么我就可以在java中编写代码来实现创建源代码、编译程序,在python中计算内存和空间消耗。
judge.py
#!/usr/bin/python
# ! -*- coding: utf8 -*-
import os
import sys
import lorun
RESULT_STR = [
'Accepted',
'Presentation Error',
'Time Limit Exceeded',
'Memory Limit Exceeded',
'Wrong Answer',
'Runtime Error',
'Output Limit Exceeded',
'Compile Error',
'System Error'
]
def runone(process, in_path, out_path, user_path, time, memory):
fin = open(in_path)
tmp = os.path.join(user_path, 'temp.out')
ftemp = open(tmp, 'w')
runcfg = {
'args': process,
'fd_in': fin.fileno(),
'fd_out': ftemp.fileno(),
'timelimit': time, # in MS
'memorylimit': memory, # in KB
}
rst = lorun.run(runcfg)
fin.close()
ftemp.close()
if rst['result'] == 0:
ftemp = open(tmp)
fout = open(out_path)
crst = lorun.check(fout.fileno(), ftemp.fileno())
fout.close()
ftemp.close()
os.remove(tmp)
rst['result'] = crst
return rst
def judge(process, data_path, user_path, time, memory):
result = {
"max_time": 0,
"max_memory": 0,
"status": 8
}
for root, dirs, files in os.walk(data_path):
for in_file in files:
if in_file.endswith('.in'):
out_file = in_file.replace('in', 'out')
fin = os.path.join(data_path, in_file)
fout = os.path.join(data_path, out_file)
if os.path.isfile(fin) and os.path.isfile(fout):
rst = runone(process, fin, fout, user_path, time, memory)
if rst['result'] == 0:
result['status'] = 0
result['max_time'] = max(result['max_time'], rst['timeused'])
result['max_memory'] = max(result['max_memory'], rst['memoryused'])
else:
result['status'], result['max_time'], result['max_memory'] = rst['result'], 0, 0
print(result)
return
print result
if __name__ == '__main__':
if len(sys.argv) != 6:
print('Usage:%s srcfile testdata_pth testdata_total'%len(sys.argv))
exit(-1)
judge(sys.argv[1].split("wzy"),sys.argv[2], sys.argv[3], long(sys.argv[4]), long(sys.argv[5]))
这个脚本是需要我在java程序中简单的调用这个脚本得出状态和时间空间消耗的;那么在java程序中需要给运行的命令,测试用例的地址,临时文件地址,还有就是时间消耗,空间消耗。这里需要说明一下,第一个参数为什么需要用特殊字符(‘wzy’)分割;给出的运行程序的命令c和c++就很简单,就是一个./a.out之类的,但是对于java、python,则是需要python3 main.py之类的有空格的命令,所以调用的时候python脚本是不知道具体的命令的,所以采用这个方式来统一调用。
在java中。通过Runtime的exec来调用这个命令,再通过这个进程的输出流来得出结果。这里我也封装了一个调用方法:
package cn.wzy.util;
import cn.wzy.vo.ExecMessage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ExecutorUtil {
public static ExecMessage exec(String cmd) {
Runtime runtime = Runtime.getRuntime();
Process exec = null;
try {
exec = runtime.exec(cmd);
} catch (IOException e) {
e.printStackTrace();
return new ExecMessage(e.getMessage(), null);
}
ExecMessage res = new ExecMessage();
res.setError(message(exec.getErrorStream()));
res.setStdout(message(exec.getInputStream()));
return res;
}
private static String message(InputStream inputStream) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
StringBuilder message = new StringBuilder();
String str;
while ((str = reader.readLine()) != null) {
message.append(str);
}
String result = message.toString();
if (result.equals("")) {
return null;
}
return result;
} catch (IOException e) {
return e.getMessage();
} finally {
try {
inputStream.close();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
ExecMessage类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExecMessage {
private String error;
private String stdout;
}
这样我调用这个方法就知道这个进程的错误输出和标准控制台输出啦。
在java中的代码就很简单了:调用'python judge.py ./m /×××/×××/×××/ /×××/×××/×××/ 1000 65535'这样的命令给出一个结果,然后判题有没有错误输出,如果有则是runtime error,否则解析标准输出的内容(json字符串),转化成java类处理。
java核心代码:
三个实体:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JudgeResult {
private Integer submitId;
private Integer status;
private Integer timeUsed;
private Integer memoryUsed;
private String errorMessage;
}
package cn.wzy.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class JudgeTask {
private String appName;
private int submitId;
private int compilerId;
private int problemId;
private String source;
private int timeLimit;
private int memoryLimit;
private boolean isSpecial;
}
package cn.wzy.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Stdout {
/**
* {'status': 0, 'max_memory': 23328L, 'max_time': 207L}
*/
private Integer status;
private Long max_memory;
private Long max_time;
}
判题代码:
@Log4j
public class Judge {
public static JudgeResult judge(JudgeTask task) {
JudgeResult result = new JudgeResult();
result.setSubmitId(task.getSubmitId());
String path = PropertiesUtil.StringValue("workspace") + "/" + task.getSubmitId();
File file = new File(path);
file.mkdirs();
try {
createFile(task.getCompilerId(), path, task.getSource());
} catch (Exception e) {
e.printStackTrace();
result.setStatus(8);
ExecutorUtil.exec("rm -rf " + path);
return result;
}
//compile the source
String message = complie(task.getCompilerId(), path);
if (message != null && task.getCompilerId() != 4) {
result.setStatus(7);
result.setErrorMessage(message);
ExecutorUtil.exec("rm -rf " + path);
return result;
}
//chmod -R 755 path
ExecutorUtil.exec("chmod -R 755 " + path);
//judge
String process = process(task.getCompilerId(), path);
String judge_data = PropertiesUtil.StringValue("judge_data") + "/" + task.getProblemId();
String cmd = "python " + PropertiesUtil.StringValue("judge_script") + " " + process + " " + judge_data + " " + path + " " + task.getTimeLimit() + " " + task.getMemoryLimit();
parseToResult(cmd, result);
ExecutorUtil.exec("rm -rf " + path);
return result;
}
private static void createFile(int compilerId, String path, String source) throws Exception {
String filename = "";
switch (compilerId) {
case 1:
filename = "main.c";
break;
case 2:
filename = "main.cpp";
break;
case 3:
filename = "Main.java";
break;
case 4:
filename = "main.pas";
break;
case 5:
filename = "main.py";
break;
}
File file = new File(path + "/" + filename);
file.createNewFile();
OutputStream output = new FileOutputStream(file);
PrintWriter writer = new PrintWriter(output);
writer.print(source);
writer.close();
output.close();
}
private static String complie(int compilerId, String path) {
/**
* '1': 'gcc','g++', '3': 'java', '4': 'pascal', '5': 'python',
*/
String cmd = "";
switch (compilerId) {
case 1:
cmd = "gcc " + path + "/main.c -o " + path + "/main";
break;
case 2:
cmd = "g++ " + path + "/main.cpp -o " + path + "/main";
break;
case 3:
cmd = "javac " + path + "/Main.java";
break;
case 4:
cmd = "fpc " + path + "/main.pas -O2 -Co -Ct -Ci";
break;
case 5:
cmd = "python3 -m py_compile " + path + "/main.py";
break;
}
return ExecutorUtil.exec(cmd).getError();
}
private static String process(int compileId, String path) {
switch (compileId) {
case 1:
return path + "/main";
case 2:
return path + "/main";
case 3:
return "javawzy-classpathwzy" + path + "wzyMain";
case 4:
return path + "/main";
case 5:
return "python3wzy" + path + "/__pycache__/" + PropertiesUtil.StringValue("python_cacheName");
}
return null;
}
private static void parseToResult(String cmd, JudgeResult result) {
ExecMessage exec = ExecutorUtil.exec(cmd);
if (exec.getError() != null) {
result.setStatus(5);
result.setErrorMessage(exec.getError());
log.error("=====error====" + result.getSubmitId() + ":" + exec.getError());
} else {
Stdout out = JSON.parseObject(exec.getStdout(), Stdout.class);
log.info("=====stdout====" + out);
result.setStatus(out.getStatus());
result.setTimeUsed(out.getMax_time().intValue());
result.setMemoryUsed(out.getMax_memory().intValue());
}
}
}
因为判题是单独的服务器,和主服务器通过kafka来通信。通过kafka传过来的用户提交记录来判题,返回结果发送回去。用到了两个topic,一个用来传输判题任务,一个传输判题结果,判题端为判题任务消费者和判题结果的生产者。
程序入口:(配置文件就不上传了,百度一大把kafka的demo)。
@Log4j
public class JudgeConsumer implements MessageListener {
@Autowired
private KafkaTemplate kafkaTemplate;
public void onMessage(ConsumerRecord record) {
log.info("==JudgeConsumer received:" + record.value());
JudgeTask judgeTask = JSON.parseObject(record.value(), JudgeTask.class);
JudgeResult result = Judge.judge(judgeTask);
kafkaTemplate.sendDefault(JSON.toJSONString(result));
}
}
github地址:https://github.com/1510460325/judge,有兴趣的可以参考参考