ACM線上判題系統(OJ)的判題實現(java+python)
學院一直是有一個自己的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<String, String> {
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
public void onMessage(ConsumerRecord<String, String> 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));
}
}