在线OJ项目(一)、多进程编程实现 做题代码的编译和运行
- 一、回顾线程和进程
- 二、进程比线程的优势
- 三、多进程编程样例
- 四、多进程思想 实现对代码 的编译 以及 运行两个功能
- CommandUtil 由于我们是在线oj,所以得编译用户的代码
- 不仅编译 还需要 运行(但这是两种不同的代码,具体如下)
- 五、字符串写入文件 以及 文件的读取
- FileUtil
- 六、完成读取用户代码,并且编译运行,返回结果
- Question 存储用户输入的代码
- Answer 返回代码编译运行后的结果
- Task
一、回顾线程和进程
二、进程比线程的优势
三、多进程编程样例
- runtime的exec创建进程 用Process接收
- InputStream 和 FileOutputStream 输入输出
- process.waitFor() 进程等待
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class TestExec {
public static void main(String[] args) throws IOException, InterruptedException {
// Runtime 在 JVM 中是一个单例
Runtime runtime = Runtime.getRuntime();
// Process 就表示 "进程"
Process process = runtime.exec("javac");
// 获取到子进程的标准输出和标准错误, 把这里的内容写入到两个文件中.
// 获取标准输出, 从这个文件对象中读, 就能把子进程的标准输出给读出来!
InputStream stdoutFrom = process.getInputStream();
FileOutputStream stdoutTo = new FileOutputStream("stdout.txt");
while (true) {
int ch = stdoutFrom.read();
if (ch == -1) {
break;
}
stdoutTo.write(ch);
}
stdoutFrom.close();
stdoutTo.close();
// 获取标准错误, 从这个文件对象中读, 就能把子进程的标准错误给读出来!
InputStream stderrFrom = process.getErrorStream();
FileOutputStream stderrTo = new FileOutputStream("stderr.txt");
while (true) {
int ch = stderrFrom.read();
if (ch == -1) {
break;
}
stderrTo.write(ch);
}
stderrFrom.close();
stderrTo.close();
// 通过 Process 类的 waitFor 方法来实现进程的等待.
// 父进程执行到 waitFor 的时候, 就会阻塞. 一直阻塞到子进程执行完毕为止.
// (和 Thread.join 是非常类似的)
// 这个退出码 就表示子进程的执行结果是否 ok. 如果子进程是代码执行完了正常退出, 此时返回的退出码就是 0.
// 如果子进程代码执行了一半异常退出(抛异常), 此时返回的退出码就非 0.
int exitCode = process.waitFor();
System.out.println(exitCode);
}
}
四、多进程思想 实现对代码 的编译 以及 运行两个功能
CommandUtil 由于我们是在线oj,所以得编译用户的代码
不仅编译 还需要 运行(但这是两种不同的代码,具体如下)
由于代码可能是正常运行结果,也可能是出现编译错误
所以我们在编译时,要考虑好这两种可能
- process接受的是Runtime启动进程的方法,启动时,便指明任务(runtime就类似于cmd窗口)
package compile;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class CommandUtil {
// 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
// 2. 获取到标准输出, 并写入到指定文件中.
// 3. 获取到标准错误, 并写入到指定文件中.
// 4. 等待子进程结束, 拿到子进程的状态码, 并返回.
public static int run(String cmd, String stdoutFile, String stderrFile) {
try {
// 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
Process process = Runtime.getRuntime().exec(cmd);
// 2. 获取到标准输出, 并写入到指定文件中.
if (stdoutFile != null) {
InputStream stdoutFrom = process.getInputStream();
FileOutputStream stdoutTo = new FileOutputStream(stdoutFile);
while (true) {
int ch = stdoutFrom.read();
if (ch == -1) {
break;
}
stdoutTo.write(ch);
}
stdoutFrom.close();
stdoutTo.close();
}
// 3. 获取到标准错误, 并写入到指定文件中.
if (stderrFile != null) {
InputStream stderrFrom = process.getErrorStream();
FileOutputStream stderrTo = new FileOutputStream(stderrFile);
while (true) {
int ch = stderrFrom.read();
if (ch == -1) {
break;
}
stderrTo.write(ch);
}
stderrFrom.close();
stderrTo.close();
}
// 4. 等待子进程结束, 拿到子进程的状态码, 并返回.
int exitCode = process.waitFor();
return exitCode;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return 1;
}
public static void main(String[] args) {
CommandUtil.run("javac", "stdout.txt", "stderr.txt");
}
}
五、字符串写入文件 以及 文件的读取
FileUtil
- StringBuilder读取字符串 最后toStirng返回
- fileWriter.write(content) 直接把字符串写入文件
package common;
import java.io.*;
public class FileUtil {
// 负责把 filePath 对应的文件的内容读取出来, 放到返回值中.
public static String readFile(String filePath) {
StringBuilder result = new StringBuilder();
try (FileReader fileReader = new FileReader(filePath)) {
while (true) {
int ch = fileReader.read();
if (ch == -1) {
break;
}
result.append((char)ch);
}
} catch (IOException e) {
e.printStackTrace();
}
return result.toString();
}
// 负责把 content 写入到 filePath 对应的文件中
public static void writeFile(String filePath, String content) {
try (FileWriter fileWriter = new FileWriter(filePath)) {
fileWriter.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
FileUtil.writeFile("d:/test.txt", "hello");
String content = FileUtil.readFile("d:/test.txt");
System.out.println(content);
}
}
六、完成读取用户代码,并且编译运行,返回结果
Question 存储用户输入的代码
package compile;
// 用这个类来表示一个 task 的输入内容
// 会包含要编译的代码
public class Question {
private String code;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
Answer 返回代码编译运行后的结果
package compile;
// 表示一个 compile.Task 的执行结果
public class Answer {
// 错误码. 约定 error 为 0 表示编译运行都 ok, 为 1 表示编译出错, 为 2 表示运行出错(抛异常).
private int error;
// 出错的提示信息. 如果 error 为 1, 编译出错了, reason 中就放编译的错误信息, 如果 error 为 2, 运行异常了, reason 就放异常信息
private String reason;
// 运行程序得到的标准输出的结果.
private String stdout;
// 运行程序得到的标准错误的结果.
private String stderr;
public int getError() {
return error;
}
public void setError(int error) {
this.error = error;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getStdout() {
return stdout;
}
public void setStdout(String stdout) {
this.stdout = stdout;
}
public String getStderr() {
return stderr;
}
public void setStderr(String stderr) {
this.stderr = stderr;
}
@Override
public String toString() {
return "compile.Answer{" +
"error=" + error +
", reason='" + reason + '\'' +
", stdout='" + stdout + '\'' +
", stderr='" + stderr + '\'' +
'}';
}
}
Task
- 创建目录,如果存在,创建多级目录
- 判断代码是否为恶意代码
- 把用户提交的代码Question 存储到Solution.java 文件中.
- 创建子进程, 对代码 进行编译
- 对编译返回值判断是否 出现错误 出现错误的话 返回错误值
- 创建子进程 对代码进行 运行
package compile;
import common.FileUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
// 每次的 "编译+运行" 这个过程, 就称为是一个 compile.Task
public class Task {
// 通过一组常量来约定临时文件的名字.
// 之前这里的名字都是静态常量. 但是现在要实现针对每个请求都有不同的临时目录, 就不能使用静态常量了
// 1. 这个表示所有临时文件所在的目录
private String WORK_DIR = null;
// 2. 约定代码的类名
private String CLASS = null;
// 3. 约定要编译的代码文件名.
private String CODE = null;
// 4. 约定存放编译错误信息的文件名
private String COMPILE_ERROR = null;
// 5. 约定存放运行时的标准输出的文件名
private String STDOUT = null;
// 6. 约定存放运行时的标准错误的文件名
private String STDERR = null;
public Task() {
// 在 Java 中使用 UUID 这个类就能生成一个 UUID 了
WORK_DIR = "./tmp/" + UUID.randomUUID().toString() + "/";
CLASS = "Solution";
CODE = WORK_DIR + "Solution.java";
COMPILE_ERROR = WORK_DIR + "compileError.txt";
STDOUT = WORK_DIR + "stdout.txt";
STDERR = WORK_DIR + "stderr.txt";
}
// 这个 compile.Task 类提供的核心方法, 就叫做 compileAndRun, 编译+运行 的意思.
// 参数: 要编译运行的 java 源代码.
// 返回值: 表示编译运行的结果. 编译出错/运行出错/运行正确.....
public Answer compileAndRun(Question question) {
Answer answer = new Answer();
// 0. 准备好用来存放临时文件的目录
File workDir = new File(WORK_DIR);
if (!workDir.exists()) {
// 创建多级目录.
workDir.mkdirs();
}
// 进行安全性判定
if (!checkCodeSafe(question.getCode())) {
System.out.println("用户提交了不安全的代码!");
answer.setError(3);
answer.setReason("您提交的代码可能会危害到服务器, 禁止运行!");
return answer;
}
// 1. 把 question 中的 code 写入到一个 Solution.java 文件中.
FileUtil.writeFile(CODE, question.getCode());
// 2. 创建子进程, 调用 javac 进行编译. 注意! 编译的时候, 需要有一个 .java 文件.
// 如果编译出错, javac 就会把错误信息给写入到 stderr 里. 就可以用一个专门的文件来保存. compileError.txt
// 需要先把编译命令给构造出来.
String compileCmd = String.format("javac -encoding utf8 %s -d %s", CODE, WORK_DIR);
System.out.println("编译命令: " + compileCmd);
CommandUtil.run(compileCmd, null, COMPILE_ERROR);
// 如果编译出错了, 错误信息就被记录到 COMPILE_ERROR 这个文件中了. 如果没有编译出错, 这个文件是空文件.
String compileError = FileUtil.readFile(COMPILE_ERROR);
if (!compileError.equals("")) {
// 编译出错!
// 直接返回 compile.Answer, 让 compile.Answer 里面记录编译的错误信息.
System.out.println("编译出错!");
answer.setError(1);
answer.setReason(compileError);
return answer;
}
// 编译正确! 继续往下执行运行的逻辑
// 3. 创建子进程, 调用 java 命令并执行
// 运行程序的时候, 也会把 java 子进程的标准输出和标准错误获取到. stdout.txt, stderr.txt
String runCmd = String.format("java -classpath %s %s", WORK_DIR, CLASS);
System.out.println("运行命令: " + runCmd);
CommandUtil.run(runCmd, STDOUT, STDERR);
String runError = FileUtil.readFile(STDERR);
if (!runError.equals("")) {
System.out.println("运行出错!");
answer.setError(2);
answer.setReason(runError);
return answer;
}
// 4. 父进程获取到刚才的编译执行的结果, 并打包成 compile.Answer 对象
// 编译执行的结果, 就通过刚才约定的这几个文件来进行获取即可.
answer.setError(0);
answer.setStdout(FileUtil.readFile(STDOUT));
return answer;
}
private boolean checkCodeSafe(String code) {
List<String> blackList = new ArrayList<>();
// 防止提交的代码运行恶意程序
blackList.add("Runtime");
blackList.add("exec");
// 禁止提交的代码读写文件
blackList.add("java.io");
// 禁止提交的代码访问网络
blackList.add("java.net");
for (String target : blackList) {
int pos = code.indexOf(target);
if (pos >= 0) {
// 找到任意的恶意代码特征, 返回 false 表示不安全
return false;
}
}
return true;
}
public static void main(String[] args) {
Task task = new Task();
Question question = new Question();
question.setCode("public class Solution {\n" +
" public static void main(String[] args) {\n" +
" System.out.println(\"hello world\");\n" +
" }\n" +
"}\n");
Answer answer = task.compileAndRun(question);
System.out.println(answer);
}
}