java 代码沙箱的实现篇
在线判题系统的核心功能模块
前言
代码沙箱其实就是java的编译执行过程,我们只需要完成这些过程即可,需要注意的是,防止写入的代码带病毒,乱写入,死循环之类的问题,我们可以用黑白名单来解决,比如把File加入黑名单
代码沙箱实现逻辑
1、编译前的准备
1.传入的参数
public class ExecuteCodeRequest {
private List<String> inputList;
private String code;
private String language;
}
- 题目需要输入的参数集合
- 代码内容
- 使用编译语言
2.传出结果参数类
public class ExecuteCodeResponse {
private List<String> outputList;
/**
* 接口信息
*/
private String message;
/**
* 执行状态
*/
private Integer status;
/**
* 判题信息
*/
private JudgeInfo judgeInfo;
}
- 我们应该都知道,每一个需要判题程序可以用很多不同的类名,所以我们固定类名,这样我们的java代码在的时候执行的时候就不会那么的麻烦,只需要写好一个字符串为javac (文件路径),就能够生成一个Main的class类,然后执行即可。
private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";
2、存放代码及编译代码
将用户代码保存为文件
/**
* 1. 把用户的代码保存为文件
* @param code 用户代码
* @return
*/
public File saveCodeToFile(String code) {
String userDir = System.getProperty("user.dir");
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
// 判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)) {
FileUtil.mkdir(globalCodePathName);
}
// 把用户的代码隔离存放
String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
return userCodeFile;
}
我们固定一个位置,来存放我们的代码,后续我们执行完成后将其删除
Process类
Process 类通常用于表示和管理操作系统中的进程。在多任务操作系统中,进程是执行中的程序实例,它包含程序计数器、寄存器、堆栈和程序执行的内存区域等。不同的编程语言或框架可能有不同的 Process 类实现,但通常它们都提供了一些共同的功能。
- 方法
-
Start()
描述:启动进程。
参数:可能包括命令行参数、工作目录等。
返回值:通常无返回值,但可能抛出异常。 -
Stop() 或 Kill()
描述:停止或终止进程。
参数:可能包括终止信号等。
返回值:通常无返回值,但可能抛出异常。 -
WaitForExit()
描述:等待进程退出。
参数:可能包括超时时间。
返回值:通常返回进程的退出代码。 -
Read() 或 GetOutput()
描述:读取进程的输出。
参数:可能包括读取的缓冲区大小等。
返回值:进程的输出内容。 -
Write() 或 Input()
描述:向进程发送输入。
参数:要发送的输入内容。
返回值:通常无返回值,但可能抛出异常。
编译代码
/**
* 2、编译代码
* @param userCodeFile
* @return
*/
public ExecuteMessage compileFile(File userCodeFile) {
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
try {
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
log.info("{},{}",executeMessage.getExitValue(),executeMessage.getErrorMessage());
if (executeMessage.getExitValue() != 0) {
executeMessage.setErrorMessage("编译错误");
}
return executeMessage;
} catch (Exception e) {
// return getErrorResponse(e);
throw new RuntimeException(e);
}
}
- 固定好我们的执行代码,确定编码格式,防止乱码
- 使用Process类,执行代码
- 构造Process的工具类,来进行编译
public static ExecuteMessage runProcessAndGetMessage(Process runProcess, String opName) {
ExecuteMessage executeMessage = new ExecuteMessage();
try {
StopWatch stopWatch = new StopWatch();
stopWatch.start();//开始计时间
// 等待程序执行,获取错误码
int exitValue = runProcess.waitFor();
executeMessage.setExitValue(exitValue);
// 正常退出
if (exitValue == 0) {
System.out.println(opName + "成功");
// 分批获取进程的正常输出
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
List<String> outputStrList = new ArrayList<>();
// 逐行读取
String compileOutputLine;
while ((compileOutputLine = bufferedReader.readLine()) != null) {
outputStrList.add(compileOutputLine);
}
executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));
} else {
// 异常退出
System.out.println(opName + "失败,错误码: " + exitValue);
// 分批获取进程的正常输出
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
List<String> outputStrList = new ArrayList<>();
// 逐行读取
String compileOutputLine;
while ((compileOutputLine = bufferedReader.readLine()) != null) {
outputStrList.add(compileOutputLine);
}
executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));
// 分批获取进程的错误输出
BufferedReader errorBufferedReader = new BufferedReader(new InputStreamReader(runProcess.getErrorStream()));
// 逐行读取
List<String> errorOutputStrList = new ArrayList<>();
// 逐行读取
String errorCompileOutputLine;
while ((errorCompileOutputLine = errorBufferedReader.readLine()) != null) {
errorOutputStrList.add(errorCompileOutputLine);
}
executeMessage.setErrorMessage(StringUtils.join(errorOutputStrList, "\n"));
}
stopWatch.stop();//停止计时
executeMessage.setTime(stopWatch.getLastTaskTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
return executeMessage;
}
在这个工具类中,我们主要做的几件事,计算时间,判断编译的退出码,然后进行比对是否正确,如果不正确,逐行读取错误信息然后设置,然后返回
3、执行代码,得到输出结果
- 执行.class代码
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
List<ExecuteMessage> executeMessageList = new ArrayList<>();
for (String inputArgs : inputList) {
// String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
try {
Process runProcess = Runtime.getRuntime().exec(runCmd);//开始运行命令
// 超时控制
new Thread(() -> {//防止死锁
try {
Thread.sleep(TIME_OUT);
System.out.println("超时了,中断");
runProcess.destroy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
System.out.println(executeMessage);
executeMessageList.add(executeMessage);
} catch (Exception e) {
ExecuteMessage executeMessage=new ExecuteMessage();
executeMessage.setMessage(e.getMessage());
executeMessageList.add(executeMessage);
return executeMessageList;
}
}
return executeMessageList;
}
这里运行了一些我们java虚拟机的只是,我们在执行的代码中加入了内存的限制-Xmx256m,限制为256M,如果内存大于该限制我们就会返回该错误信息,并且为了防止用户写一个长时间无法执行完成的程序,我们新建一个线程,让其睡眠一段时间,醒来后直接结束执行的线程(防止死锁)。
- 获取输出结果
public ExecuteCodeResponse getOutputResponse(List<ExecuteMessage> executeMessageList) {
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> outputList = new ArrayList<>();
// 取用时最大值,便于判断是否超时
long maxTime = 0;
for (ExecuteMessage executeMessage : executeMessageList) {
String errorMessage = executeMessage.getErrorMessage();
if (StrUtil.isNotBlank(errorMessage)) {
executeCodeResponse.setMessage(errorMessage);
// 用户提交的代码执行中存在错误
executeCodeResponse.setStatus(3);
break;
}
outputList.add(executeMessage.getMessage());
Long time = executeMessage.getTime();
if (time != null) {
maxTime = Math.max(maxTime, time);
}
}
// 正常运行完成
if (outputList.size() == executeMessageList.size()) {
executeCodeResponse.setStatus(1);
}
executeCodeResponse.setOutputList(outputList);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setTime(maxTime);
executeCodeResponse.setJudgeInfo(judgeInfo);
return executeCodeResponse;
}
当我们执行代码的时候,输出会在控制台中,这个时候我们读取控制台中的信息,然后拿到一个一个的输出结果,并且我们可以通过executeMessage.getErrorMessage得到是否超时的信息。