重构解耦JudgeServer判题逻辑,添加部署文档

This commit is contained in:
Himit_ZH 2021-04-16 16:57:03 +08:00
parent d89bfcfc08
commit 2dd8522c18
145 changed files with 1293 additions and 694 deletions

View File

@ -7,10 +7,17 @@
> 上线日记
| 时间 | 内容 | 更新者 |
| ---------- | ----------------------- | -------- |
| ---------- | ----------------------------------------- | -------- |
| 2020-10-26 | 正式开发 | Himit_ZH |
| 2021-04-10 | 首次上线测试 | Himit_ZH |
| 2021-04-15 | 判题调度2.0解决并发问题 | Himit_ZH |
| 2021-04-16 | 重构解耦JudgeServer判题逻辑添加部署文档 | Himit_ZH |
# 二、部署
**注意比较适用于熟悉springbootdocker的开发人员打包部署**
部署文档:[https://gitee.com/himitzh0730/hoj/tree/master/docs](https://gitee.com/himitzh0730/hoj/tree/master/docs)
> 简略介绍

428
docs/README.md Normal file
View File

@ -0,0 +1,428 @@
### 1. 后端部署
**linux下安装下载docker自行百度**
#### 1.1 安装MySQL
1. 创建自定义网络(用于容器通讯)
```shell
docker network create hoj-network
```
2. 查看网络
```shell
docker network ls
```
3. 创建挂载文件夹
```shell
//mysql配置文件
mkdir -p /data/mysql/conf
//mysql数据文件路径
mkdir p /data/mysql/data
//日志文件路径
mkdir -p /data/mysql/logs
```
4. 启动mysql
```shell
docker run -p 3306:3306 --name mysql -d \
--restart=always \
--network hoj-network \
-v /data/mysql/conf:/etc/mysql/conf.d \
-v /data/mysql/logs:/logs \
-v /data/mysql/data:/data \
-e MYSQL_ROOT_PASSWORD=admin \
mysql:5.7
```
5. 启动成功后 使用docker ps 可查看 如果正常则进行数据库创建操作
6. 创建名字叫hoj的数据库执行脚本在sqlAndSetting文件夹里面或者 [hoj.sql](https://gitee.com/himitzh0730/hoj/blob/master/sqlAndsetting/hoj.sql)、[hoj-data.sql](https://gitee.com/himitzh0730/hoj/blob/master/sqlAndsetting/hoj-data.sql)
7. 创建名字叫nacos的数据库执行脚本在sqlAndSetting文件夹里面或者 [nacos.sql](https://gitee.com/himitzh0730/hoj/blob/master/sqlAndsetting/nacos-mysql.sql)
#### 1.2 注册中心与配置中心naco
1. 执行docker命令拉取nacos镜像。
```shell
//查询nacos镜像
docker search nacos
//拉取镜像
docker pull nacos/nacos-server
//查看镜像
docker images
```
2. 启动nacos
```shell
docker run --env MODE=standalone --network hoj-network --name nacos -d -p 8848:8848 nacos/nacos-server
```
3. 查看自定义网络中各容器ip一般该network的ip应该是**172.18.0.2或172.19.0.2**
```shell
//查看网络
docker network ls
//查看网络容器
docker network inspect hoj-network
```
4. 进入nacos容器修改配置
```shell
// 进入容器
docker exec -it nacos bash
// 修改容器配置
cd conf
vi application.properties
```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200411202402562.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpZXFpbmdfeHE=,size_16,color_FFFFFF,t_70)
5. 重启容器
```shell
docker restart nacos
```
6. 连上nacos将后端服务需要的配置添加进去
```she
http://ip:8848/nacos/index.html
nacos/nacos(用户名和密码)
```
**登陆后,点击添加**
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210416154428657.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzg1MzA5Nw==,size_16,color_FFFFFF,t_70)
**依次添加后台服务的配置文件和判题服务的配置文件**
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210416154647434.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzg1MzA5Nw==,size_16,color_FFFFFF,t_70)
7. hoj-data-backup-prod.yml的配置如下请自行修改
```yaml
hoj:
jwt:
# 加密秘钥
secret: zsc-acm-hoj
# token有效时长24小时单位秒
expire: 86400
# 6小时内还有请求可进行刷新
checkRefreshExpire: 21600
header: token
judge:
# 调用判题服务器的token
token: zsc-acm-hoj-judge-server
db: # mysql数据库服务配置
host: your_mysql_host
port: your_mysql_port
name: your_mysql_database_name # 默认hoj
username: your_mysql_username
password: your_mysql_password
mail: # 邮箱服务配置
ssl: true
username: your_email_username
password: your_email_password
host: your_email_host
port: your_email_port
background-img: https://cdn.jsdelivr.net/gh/HimitZH/CDN/images/HCODE.png # 邮箱系统发送邮件模板的背景图片地址
redis: # redis服务配置
host: your_redis_host
port: 6371
password: your_redis_password
web-config:
base-url: http://www.hcode.top # 后端服务地址
name: zsc-acm-hoj # 后端服务地址
short-name: hoj # oj简写
register: true
footer: # 网站页面底部footer配置
record:
name: 浙ICP备20009096号-1 # 网站备案
url: http://www.hcode.top # 网站域名
project: # 项目
name: HOJ # 项目名字
url: https://gitee.com/himitzh0730/hoj # 项目地址
hdu:
account:
username: hdu账号1用户名,hdu账号2用户名,...
password: hdu账号1密码,hdu账号2密码,...
cf:
account:
username: cf账号1用户名,cf账号2用户名,...
password: cf账号1密码,cf账号2密码,...
```
8. 添加好后点击发布再次添加hoj-judge-server-prod.yml流程一样
```yaml
hoj:
judge:
db:
username: your_mysql_username
password: your_mysql_password
host: your_mysql_host
port: your_mysql_port
name: your_mysql_database_name # 数据库名字默认hoj
# 调用判题服务器的token与数据服务后台必须一致
token: zsc-acm-hoj-judge-server
redis:
host: your_redis_host
port: your_redis_port
password: your_redis_password
```
#### 1.3 Redis部署
依旧使用docker部署 ,**mypassword是redis的密码请使用上面配置文件中redis的password必须一致**
```shell
//查询目前可用的reids镜像
docker search redis
//选择拉取官网的镜像
docker pull redis
//查看本地是否有redis镜像
docker images
//运行redis并设置密码
docker run -d --name redis -p 6379:6379 redis --requirepass "mypassword" --restart=always
```
#### 1.4 DataBackup数据后台部署
1. 修改该路径**/hoj-springboot/DataBackup/src/main/resources/bootstrap.yml**的相关配置
```yaml
hoj-backstage:
port: 6688
nacos-url: 172.18.0.2:8848 # nacos地址,如果使用了docker network 可用使用network的ip 否则请使用服务器ip
```
2. 使用cmd打开当前JudgeServer文件夹路径然后使用mvn命令进行打包成jar包
```powershell
mvn clean package -Dmaven.test.skip=true
```
3. 上传到服务器,**使用apt安装JDK8请自行百度**,然后在当前文件夹同时创建名叫`Dockerfile`的文件,编写内容如下:
```dockerfile
FROM java:8
COPY *.jar /app.jar
CMD ["--server.port=6688"]
EXPOSE 6688
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ENTRYPOINT ["java","-Xmx512m","-Xms512m","-Xmn256m","-Djava.security.egd=file:/dev/./urandom","-jar", "/app.jar"]
```
4. 在当前文件夹使用命令打包成docker镜像
```shell
// 使用dockerfile打包成镜像
docker build -t hoj .
// 查看hoj镜像是否存在
docker images
```
5. 启动容器
```shell
docker run -d -p 6688:6688 --name hoj --network hoj-network hoj
```
6. 查看是否成功
```shell
docker ps
```
#### 1.5 JudgeServer判题服务部署
> 注意:判题服务可以部署多台云服务器,步骤一样
1. 下载本项目git clone或者download zip
2. 修改该路径**/hoj-springboot/JudgeServer/src/main/resources/bootstrap.yml**的相关配置
```yaml
hoj-judge-server:
max-task-num: -1 # -1表示最大并行任务数为cpu核心数*2
ip: 127.0.0.1 # -1表示使用默认本地ipv4若是部署其它服务器务必使用公网ip
port: 8088 # 端口号
name: hoj-judger-1 # 判题机名字 唯一不可重复!!!
nacos-url: 127.0.0.1:8848 # nacos地址
remote-judge:
open: true # 当前判题服务器是否开启远程虚拟判题功能
max-task-num: -1 # -1表示最大并行任务数为(cpu核心数*2)*2
```
3. 使用cmd打开当前JudgeServer文件夹路径然后使用mvn命令进行打包成jar包
```shell
mvn clean package -Dmaven.test.skip=true
```
4. 打包成功后在路径**/hoj-springboot/JudgeServer/target/** 文件夹内找到类似JudgeServer.jar的jar包然后将该jar包与**/judger**文件夹内的Judger-SandBox文件go打包的linux系统下可执行文件一起上传到云服务器的同一个文件夹内同时在该文件夹内创建一个JudgeServer.json的文件JVM的配置可以直接配置内容如下
```json
{
"apps" : {
"name":"hoj-judgeServer",
"script":"java",
"args":[
"-XX:+UseG1GC",
"-jar",
"JudgeServer.jar", // 注意为jar包名字
],
"error_file":"./log/err.log",
"out_file":"./log/out.log",
"merge_logs":true,
"log_date_format":"YYYY/MM/DD HH:mm:ss",
"min_uptime": "60s",
"max_restarts": 30,
"autorestart": true,
"restart_delay": "60"
}
}
```
5. 使用apt安装JDK8请自行百度。**下面的操作都使用root权限才不会出错。**
6. 接下来使用pm2启动管理Judger-SandBox和JudgeServer当然可用别的方式启动jar包nohup之类的都可以记住Judger-SandBox默认占用5050端口JudgeServer占用8088端口请确认不会被其它进程占用本次介绍使用pm2管理启动
- 更新`apt-get`
```shell
sudo apt-get update
```
- 安装`nodeJs`
```shell
sudo apt-get install nodejs
```
- 安装`npm`
```shell
sudo apt-get install npm
```
- 安装`pm2`
```shell
sudo npm install -g pm2
```
- 查看帮助,看到提示就说明成功了
```sehll
pm2 --help
```
7. 使用了第5步的就可以启动判题服务和判题安全沙盒了操作如下
- 启动沙盒确保不要出错不然无法进行自身题目判题远程虚拟判题vj无影响,Judger-SandBox为文件名即是刚刚上传的。
```shell
pm2 start Judger-SandBox
```
- 查看是否正常,status的状态是online就是正常
```shell
pm2 list
```
- 启动判题服务JudgeServer.json是我们在第四步配置创建放在与jar包同个文件夹里面的json文件启动后也使用`pm2 list`查看
```shell
pm2 start JudgeServer.json
```
- 如果两者pm2 list里面的status都是online则说明此次判题服务部署成功。
8. 最后一步下载对应编译语言的编译器HOJ默认支持 GCC,G++,Python2,Python3,Java,Golang,C#编程语言,请自行百度下载对应的编译器。
### 2. 前端部署
1. 下载本项目git clone或者download zip
2. 前提是本地有vue-cli4请自行百度下载
3. 进入/hoj-vue文件夹修改生产环境下的后端服务地址
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021041616214988.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzg1MzA5Nw==,size_16,color_FFFFFF,t_70)
4. 然后在当前路径运行打包命令
```powershell
npm run build
```
5. 打包成功会在src同文件夹内有个dist文件夹复制里面的html和css等静态文件上传到服务器的指定文件夹内/hoj/www/html内
6. 配置nginx在安装好nginx后修改nginx.conf配置
```shell
sudo vi /etc/nginx/nginx.conf
```
7. 将下面的内容复制进去
```json
server{
listen 80;
server_name www.hcode.top; # 此处填写你的域名或IP
root /hoj/www/htm; # 此处填写你的网页根目录
location /api{
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:6688; # 填写你的后端地址和端口
}
location ~ .*\.(js|json|css)$ {
gzip on;
gzip_static on; # gzip_static是nginx对于静态文件的处理模块该模块可以读取预先压缩的gz文件这样可以减少每次请求进行gzip压缩的CPU资源消耗。
gzip_min_length 1k;
gzip_http_version 1.1;
gzip_comp_level 9;
gzip_types text/css application/javascript application/json;
root /hoj/www/html; # 此处填写你的网页根目录
}
location / { # 路由重定向以适应Vue中的路由
index index.html;
try_files $uri $uri/ /index.html;
}
}
```
8. 修改后保存然后重启或者热重载nginx不出意外应该可用访问前端页面了。
```shell
sudo systemctl restart nginx
sudo nginx -s reload
```

View File

@ -15,7 +15,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling // 开启定时任务
@EnableDiscoveryClient // 开启注册发现
@SpringBootApplication
@EnableFeignClients // 开启feign
@EnableAsync(proxyTargetClass=true) //开启异步注解
public class DataBackupApplication {
public static void main(String[] args) {

View File

@ -61,6 +61,7 @@ public class AdminContestController {
QueryWrapper<Contest> queryWrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(keyword)) {
keyword = keyword.trim();
queryWrapper
.like("title", keyword).or()
.like("id", keyword);

View File

@ -63,6 +63,7 @@ public class AdminProblemController {
QueryWrapper<Problem> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("gmt_create");
if (!StringUtils.isEmpty(keyword)) {
keyword = keyword.trim();
queryWrapper
.like("title", keyword).or()
.like("author", keyword).or()

View File

@ -52,6 +52,9 @@ public class AdminUserController {
@RequestParam(value = "keyword", required = false) String keyword) {
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 10;
if (keyword != null) {
keyword = keyword.trim();
}
IPage<UserRolesVo> userList = userRoleService.getUserList(limit, currentPage, keyword);
if (userList.getTotal() == 0) { // 未查询到一条数据
return CommonResult.successResponse(userList, "暂无数据");

View File

@ -89,11 +89,12 @@ public class ConfigController {
@RequestMapping("/get-email-config")
public CommonResult getEmailConfig() {
return CommonResult.successResponse(
MapUtil.builder().put("username", configVo.getEmailUsername())
.put("password", configVo.getEmailPassword())
.put("host", configVo.getEmailHost())
.put("port", configVo.getEmailPort())
.put("ssl", configVo.getEmailSsl()).map()
MapUtil.builder().put("emailUsername", configVo.getEmailUsername())
.put("emailPassword", configVo.getEmailPassword())
.put("emailHost", configVo.getEmailHost())
.put("emailPort", configVo.getEmailPort())
.put("emailBGImg", configVo.getEmailBGImg())
.put("emailSsl", configVo.getEmailSsl()).map()
);
}

View File

@ -480,7 +480,6 @@ public class ContestController {
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 30;
// 获取当前比赛的状态为ac未被校验的排在签名
IPage<ContestRecord> contestRecords = contestRecordService.getACInfo(currentPage,
limit, Constants.Contest.RECORD_AC.getCode(), cid);

View File

@ -105,7 +105,7 @@ public class JudgeController {
*/
@RequiresAuthentication
@RequestMapping(value = "/submit-problem-judge", method = RequestMethod.POST)
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
@Transactional(rollbackFor = Exception.class, isolation = Isolation.REPEATABLE_READ)
public CommonResult submitProblemJudge(@RequestBody ToJudgeDto judgeDto, HttpServletRequest request) {
// 需要获取一下该token对应用户的数据
@ -239,6 +239,7 @@ public class JudgeController {
*/
@RequiresAuthentication
@GetMapping(value = "/resubmit")
@Transactional
public CommonResult resubmit(@RequestParam("submitId") Long submitId,
HttpServletRequest request) {
@ -407,6 +408,12 @@ public class JudgeController {
}
uid = userRolesVo.getUid();
}
if (searchPid != null) {
searchPid = searchPid.trim();
}
if (searchUsername != null) {
searchUsername = searchUsername.trim();
}
IPage<JudgeVo> commonJudgeList = judgeService.getCommonJudgeList(limit, currentPage, searchPid,
searchStatus, searchUsername, uid);

View File

@ -78,6 +78,7 @@ public class ProblemController {
// 关键词查询不为空
Long pid = null;
if (!StringUtils.isEmpty(keyword)) {
keyword = keyword.trim();
Pattern pattern = Pattern.compile("[0-9]*");
Matcher isNum = pattern.matcher(keyword);
if (isNum.matches()) { // 利用正则表达式判断keyword是否为纯数字

View File

@ -4,10 +4,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import top.hcode.hoj.pojo.vo.ContestRecordVo;
import top.hcode.hoj.pojo.entity.ContestRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.hcode.hoj.pojo.vo.JudgeVo;
import java.util.Date;
import java.util.List;
@ -23,7 +21,7 @@ import java.util.List;
@Mapper
@Repository
public interface ContestRecordMapper extends BaseMapper<ContestRecord> {
IPage<ContestRecord> getACInfo(IPage iPage, @Param("status") Integer status, @Param("cid") Long cid);
List<ContestRecord> getACInfo(IPage iPage, @Param("status") Integer status, @Param("cid") Long cid);
List<ContestRecord> getOIContestRecord(@Param("cid") Long cid, @Param("isOpenSealRank") Boolean isOpenSealRank,
@Param("sealTime") Date sealTime, @Param("endTime") Date endTime);

View File

@ -3,14 +3,21 @@
<mapper namespace="top.hcode.hoj.dao.ContestRecordMapper">
<select id="getACInfo" resultType="top.hcode.hoj.pojo.entity.ContestRecord">
SELECT id,uid,username,display_id,cid,realname,pid,time,status,
SELECT id,uid,username,display_id,cid,realname,pid,time,status,checked,
MIN(submit_id) AS submit_id,
MIN(submit_time) AS submit_time,
MAX(first_blood) AS first_blood,
checked FROM contest_record
WHERE status=#{status} and cid = #{cid}
MAX(first_blood) AS first_blood
FROM contest_record
<where>
<if test="status!=null">
status=#{status}
</if>
<if test="cid!=null">
and cid = #{cid}
</if>
</where>
GROUP BY status,uid,pid,cpid
ORDER BY checked DESC,submit_time DESC
ORDER BY checked ASC,submit_time DESC
</select>
<select id="getOIContestRecord" resultType="top.hcode.hoj.pojo.entity.ContestRecord">

View File

@ -73,6 +73,9 @@ public class ConfigVo {
@Value("${hoj.mail.ssl}")
private Boolean emailSsl;
@Value("${hoj.mail.background-img}")
private String emailBGImg;
// 网站前端显示配置
@Value("${hoj.web-config.base-url}")
private String baseUrl;

View File

@ -123,6 +123,10 @@ public class ConfigServiceImpl implements ConfigService {
configVo.setEmailUsername((String) params.get("emailUsername"));
}
if (!StringUtils.isEmpty(params.get("emailBGImg"))) {
configVo.setEmailBGImg((String) params.get("emailBGImg"));
}
if (params.get("emailSsl") != null) {
configVo.setEmailSsl((Boolean) params.get("emailSsl"));
}

View File

@ -34,7 +34,7 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
@Override
public IPage<ContestRecord> getACInfo(Integer currentPage, Integer limit, Integer status, Long cid) {
Page<ContestRecord> page = new Page<>(currentPage, limit);
return contestRecordMapper.getACInfo(page, status, cid);
return page.setRecords(contestRecordMapper.getACInfo(page, status, cid));
}

View File

@ -4,6 +4,8 @@ import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
@ -25,6 +27,7 @@ import java.util.Date;
* @Description: 异步发送邮件的任务
*/
@Service
@RefreshScope
@Async
@Slf4j
public class EmailServiceImpl implements EmailService {
@ -35,6 +38,21 @@ public class EmailServiceImpl implements EmailService {
@Autowired
private TemplateEngine templateEngine;
@Value("${hoj.web-config.base-url}")
public String ojAddr;
@Value("${hoj.web-config.name}")
public String ojName;
@Value("${hoj.web-config.short-name}")
public String ojShortName;
@Value("${hoj.mail.background-img}")
public String ojEmailBg;
@Value("${hoj.mail.username}")
public String ojEmailFrom;
/**
* @param email 用户邮箱
* @param code 生成的六位随机数字验证码
@ -52,10 +70,10 @@ public class EmailServiceImpl implements EmailService {
true);
// 设置渲染到html页面对应的值
Context context = new Context();
context.setVariable(Constants.Email.OJ_NAME.name(), Constants.Email.OJ_NAME.getValue());
context.setVariable(Constants.Email.OJ_SHORT_NAME.name(), Constants.Email.OJ_SHORT_NAME.getValue());
context.setVariable(Constants.Email.OJ_URL.name(), Constants.Email.OJ_URL.getValue());
context.setVariable(Constants.Email.EMAIL_BACKGROUND_IMG.name(), Constants.Email.EMAIL_BACKGROUND_IMG.getValue());
context.setVariable(Constants.Email.OJ_NAME.name(), ojName);
context.setVariable(Constants.Email.OJ_SHORT_NAME.name(), ojShortName.toUpperCase());
context.setVariable(Constants.Email.OJ_URL.name(), ojAddr);
context.setVariable(Constants.Email.EMAIL_BACKGROUND_IMG.name(), ojEmailBg);
context.setVariable("CODE", code);
context.setVariable("EXPIRE_TIME", expireTime.toString());
@ -67,7 +85,7 @@ public class EmailServiceImpl implements EmailService {
// 收件人
mimeMessageHelper.setTo(email);
// 发送人
mimeMessageHelper.setFrom(Constants.Email.EMAIL_FROM.getValue());
mimeMessageHelper.setFrom(ojEmailFrom);
mailSender.send(mimeMessage);
} catch (MessagingException e) {
log.error("用户注册的邮件任务发生异常------------>{}", e.getMessage());
@ -93,10 +111,10 @@ public class EmailServiceImpl implements EmailService {
true);
// 设置渲染到html页面对应的值
Context context = new Context();
context.setVariable(Constants.Email.OJ_NAME.name(), Constants.Email.OJ_NAME.getValue());
context.setVariable(Constants.Email.OJ_SHORT_NAME.name(), Constants.Email.OJ_SHORT_NAME.getValue());
context.setVariable(Constants.Email.OJ_URL.name(), Constants.Email.OJ_URL.getValue());
context.setVariable(Constants.Email.EMAIL_BACKGROUND_IMG.name(), Constants.Email.EMAIL_BACKGROUND_IMG.getValue());
context.setVariable(Constants.Email.OJ_NAME.name(), ojName);
context.setVariable(Constants.Email.OJ_SHORT_NAME.name(), ojShortName.toUpperCase());
context.setVariable(Constants.Email.OJ_URL.name(), ojAddr);
context.setVariable(Constants.Email.EMAIL_BACKGROUND_IMG.name(), ojEmailBg);
context.setVariable("RESET_URL", Constants.Email.OJ_URL.getValue() + "/reset-password?username=" + username + "&code=" + code);
context.setVariable("EXPIRE_TIME", expireTime.toString());
context.setVariable("USERNAME", username);
@ -110,7 +128,7 @@ public class EmailServiceImpl implements EmailService {
// 收件人
mimeMessageHelper.setTo(email);
// 发送人
mimeMessageHelper.setFrom(Constants.Email.EMAIL_FROM.getValue());
mimeMessageHelper.setFrom(ojEmailFrom);
mailSender.send(mimeMessage);
} catch (MessagingException e) {
log.error("用户重置密码的邮件任务发生异常------------>{}", e.getMessage());
@ -133,11 +151,10 @@ public class EmailServiceImpl implements EmailService {
true);
// 设置渲染到html页面对应的值
Context context = new Context();
context.setVariable(Constants.Email.OJ_NAME.name(), Constants.Email.OJ_NAME.getValue());
context.setVariable(Constants.Email.OJ_SHORT_NAME.name(), Constants.Email.OJ_SHORT_NAME.getValue());
context.setVariable(Constants.Email.OJ_URL.name(), Constants.Email.OJ_URL.getValue());
context.setVariable(Constants.Email.EMAIL_BACKGROUND_IMG.name(), Constants.Email.EMAIL_BACKGROUND_IMG.getValue());
context.setVariable(Constants.Email.OJ_NAME.name(), ojName);
context.setVariable(Constants.Email.OJ_SHORT_NAME.name(), ojShortName.toUpperCase());
context.setVariable(Constants.Email.OJ_URL.name(), ojAddr);
context.setVariable(Constants.Email.EMAIL_BACKGROUND_IMG.name(), ojEmailBg);
//利用模板引擎加载html文件进行渲染并生成对应的字符串
String emailContent = templateEngine.process("emailTemplate_testEmail", context);
@ -147,7 +164,7 @@ public class EmailServiceImpl implements EmailService {
// 收件人
mimeMessageHelper.setTo(email);
// 发送人
mimeMessageHelper.setFrom(Constants.Email.EMAIL_FROM.getValue());
mimeMessageHelper.setFrom(ojEmailFrom);
mailSender.send(mimeMessage);
} catch (MessagingException e) {
log.error("超级管理员重置邮件系统配置的测试邮箱可用性的任务发生异常------------>{}", e.getMessage());

View File

@ -41,6 +41,7 @@ public class ConfigUtils {
" password: " + configVo.getEmailPassword() + "\n" +
" host: " + configVo.getEmailHost() + "\n" +
" port: " + configVo.getEmailPort() + "\n" +
" background-img: " + configVo.getEmailBGImg() + "\n" +
" redis:\n" +
" host: " + configVo.getRedisHost() + "\n" +
" port: " + configVo.getRedisPort() + "\n" +

View File

@ -1,6 +1,7 @@
package top.hcode.hoj.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
@ -8,45 +9,15 @@ import org.springframework.stereotype.Component;
* @Date: 2021/1/1 13:00
* @Description: 常量枚举类
*/
@Component
public class Constants {
public static String ojAddr;
public static String ojName;
public static String ojShortName;
public static String ojEmailBg;
public static String ojEmailFrom;
@Value("${hoj-backstage.addr}")
@Value("${hoj.web-config.base-url}")
public void setOjAddr(String ojAddr) {
Constants.ojAddr = ojAddr;
}
@Value("${hoj-backstage.name}")
public void setOjName(String ojName) {
Constants.ojName = ojName;
}
@Value("${hoj-backstage.short-name}")
public void setOjShortName(String ojShortName) {
Constants.ojShortName = ojShortName;
}
@Value("${hoj-backstage.email-bg}")
public void setOjEmailBg(String ojEmailBg) {
Constants.ojEmailBg = ojEmailBg;
}
@Value("${hoj.mail.username}")
public void setOjEmailFrom(String ojEmailFrom) {
Constants.ojEmailFrom = ojEmailFrom;
}
/**
* @Description 提交评测结果的状态码
* @Since 2021/1/1
@ -217,11 +188,11 @@ public class Constants {
public enum Email {
OJ_URL(ojAddr),
OJ_NAME(ojName),
OJ_SHORT_NAME(ojShortName),
EMAIL_FROM(ojEmailFrom),
EMAIL_BACKGROUND_IMG(ojEmailBg),
OJ_URL("OJ_UR"),
OJ_NAME("OJ_NAME"),
OJ_SHORT_NAME("OJ_SHORT_NAME"),
EMAIL_FROM("EMAIL_FROM"),
EMAIL_BACKGROUND_IMG("EMAIL_BACKGROUND_IMG"),
REGISTER_KEY_PREFIX("register-user:"),
RESET_PASSWORD_KEY_PREFIX("reset-password:");

View File

@ -40,9 +40,9 @@ spring:
url: jdbc:mysql://${hoj.db.host}:${hoj.db.port}/${hoj.db.name}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initial-size: 10 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
min-idle: 10 # 最小连接池数量
maxActive: 200 # 最大连接池数量
initial-size: 20 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
min-idle: 50 # 最小连接池数量
maxActive: 400 # 最大连接池数量
maxWait: 60000 # 获取连接时最大等待时间单位毫秒。配置了maxWait之后缺省启用公平锁并发效率会有所下降如果需要可以通过配置
timeBetweenEvictionRunsMillis: 60000 # 关闭空闲连接的检测时间间隔.Destroy线程会检测连接的间隔时间如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
minEvictableIdleTimeMillis: 300000 # 连接的最小生存时间.连接保持空闲而不被驱逐的最小时间

View File

@ -1,10 +1,6 @@
hoj-backstage:
port: 6688
nacos-url: http://172.18.0.2:8848
addr: http://oj.hcode.top
name: Hcode Online Judge
short-name: HOJ
email-bg: https://cdn.jsdelivr.net/gh/HimitZH/CDN/images/HCODE.png # 邮箱系统发生邮件模板的背景图片地址
nacos-url: 172.18.0.2:8848
server:
port: ${hoj-backstage.port}

View File

@ -40,9 +40,9 @@ spring:
url: jdbc:mysql://${hoj.db.host}:${hoj.db.port}/${hoj.db.name}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initial-size: 10 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
min-idle: 10 # 最小连接池数量
maxActive: 200 # 最大连接池数量
initial-size: 20 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
min-idle: 50 # 最小连接池数量
maxActive: 400 # 最大连接池数量
maxWait: 60000 # 获取连接时最大等待时间单位毫秒。配置了maxWait之后缺省启用公平锁并发效率会有所下降如果需要可以通过配置
timeBetweenEvictionRunsMillis: 60000 # 关闭空闲连接的检测时间间隔.Destroy线程会检测连接的间隔时间如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
minEvictableIdleTimeMillis: 300000 # 连接的最小生存时间.连接保持空闲而不被驱逐的最小时间

View File

@ -1,10 +1,6 @@
hoj-backstage:
port: 6688
nacos-url: http://172.18.0.2:8848
addr: http://oj.hcode.top
name: Hcode Online Judge
short-name: HOJ
email-bg: https://cdn.jsdelivr.net/gh/HimitZH/CDN/images/HCODE.png # 邮箱系统发生邮件模板的背景图片地址
nacos-url: 172.18.0.2:8848
server:
port: ${hoj-backstage.port}

View File

@ -3,14 +3,21 @@
<mapper namespace="top.hcode.hoj.dao.ContestRecordMapper">
<select id="getACInfo" resultType="top.hcode.hoj.pojo.entity.ContestRecord">
SELECT id,uid,username,display_id,cid,realname,pid,time,status,
SELECT id,uid,username,display_id,cid,realname,pid,time,status,checked,
MIN(submit_id) AS submit_id,
MIN(submit_time) AS submit_time,
MAX(first_blood) AS first_blood,
checked FROM contest_record
WHERE status=#{status} and cid = #{cid}
MAX(first_blood) AS first_blood
FROM contest_record
<where>
<if test="status!=null">
status=#{status}
</if>
<if test="cid!=null">
and cid = #{cid}
</if>
</where>
GROUP BY status,uid,pid,cpid
ORDER BY checked DESC,submit_time DESC
ORDER BY checked ASC,submit_time DESC
</select>
<select id="getOIContestRecord" resultType="top.hcode.hoj.pojo.entity.ContestRecord">

View File

@ -0,0 +1,20 @@
package top.hcode.hoj.common.exception;
import lombok.Data;
/**
* @Author: Himit_ZH
* @Date: 2021/4/16 13:52
* @Description:
*/
@Data
public class SubmitError extends Exception {
private String stdout;
private String stderr;
public SubmitError(String message, String stdout, String stderr) {
super(message);
this.stdout = stdout;
this.stderr = stderr;
}
}

View File

@ -93,6 +93,7 @@ public class JudgeController {
// 更新该次提交
judgeService.updateById(finalJudge);
if (finalJudge.getStatus().intValue() != Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus()) {
// 更新其它表
judgeService.updateOtherTable(finalJudge.getSubmitId(),
finalJudge.getStatus(),
@ -100,6 +101,7 @@ public class JudgeController {
finalJudge.getUid(),
finalJudge.getPid(),
finalJudge.getScore());
}
return CommonResult.successResponse(finalJudge, "判题机评测完成!");

View File

@ -0,0 +1,96 @@
package top.hcode.hoj.judge;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import org.springframework.util.StringUtils;
import top.hcode.hoj.common.exception.CompileError;
import top.hcode.hoj.common.exception.SubmitError;
import top.hcode.hoj.common.exception.SystemError;
import top.hcode.hoj.util.Constants;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2021/4/16 12:14
* @Description: 判题流程解耦重构2.0该类只负责编译
*/
public class Compiler {
public static String compile(Constants.CompileConfig compileConfig, String code, String language) throws SystemError, CompileError, SubmitError {
if (compileConfig == null) {
throw new CompileError("Unsupported language " + language, null, null);
}
// 调用安全沙箱进行编译
JSONArray result = SandboxRun.compile(compileConfig.getMaxCpuTime(),
compileConfig.getMaxRealTime(),
compileConfig.getMaxMemory(),
128 * 1024 * 1024L,
compileConfig.getSrcName(),
compileConfig.getExeName(),
parseCompileCommand(compileConfig.getCommand(), compileConfig),
compileConfig.getEnvs(),
code,
true,
false,
null
);
JSONObject compileResult = (JSONObject) result.get(0);
if (compileResult.getInt("status").intValue() != Constants.Judge.STATUS_ACCEPTED.getStatus()) {
throw new CompileError("Compile Error.", ((JSONObject) compileResult.get("files")).getStr("stderr"),
((JSONObject) compileResult.get("files")).getStr("stderr"));
}
String fileId = ((JSONObject) compileResult.get("fileIds")).getStr(compileConfig.getExeName());
if (StringUtils.isEmpty(fileId)) {
throw new SubmitError("Executable file not found.", ((JSONObject) compileResult.get("files")).getStr("stderr"),
((JSONObject) compileResult.get("files")).getStr("stderr"));
}
return fileId;
}
public static Boolean compileSpj(String code, Long pid, String spjLanguage) throws SystemError, CompileError {
Constants.CompileConfig spjCompiler = Constants.CompileConfig.getCompilerByLanguage("SPJ-" + spjLanguage);
if (spjCompiler == null) {
throw new CompileError("Unsupported language " + spjLanguage, null, null);
}
boolean copyOutExe = true;
if (pid == null) { // 题目id为空则不进行本地存储可能为新建题目时测试特判程序是否正常的判断而已
copyOutExe = false;
}
// 调用安全沙箱对特别判题程序进行编译
JSONArray res = SandboxRun.compile(spjCompiler.getMaxCpuTime(),
spjCompiler.getMaxRealTime(),
spjCompiler.getMaxMemory(),
128 * 1024 * 1024L,
spjCompiler.getSrcName(),
spjCompiler.getExeName(),
parseCompileCommand(spjCompiler.getCommand(), spjCompiler),
spjCompiler.getEnvs(),
code,
false,
copyOutExe,
Constants.JudgeDir.SPJ_WORKPLACE_DIR.getContent() + "/" + pid
);
JSONObject compileResult = (JSONObject) res.get(0);
if (compileResult.getInt("status").intValue() != Constants.Judge.STATUS_ACCEPTED.getStatus()) {
throw new SystemError("Special Judge Code Compile Error.", ((JSONObject) compileResult.get("files")).getStr("stderr"),
((JSONObject) compileResult.get("files")).getStr("stderr"));
}
return true;
}
private static List<String> parseCompileCommand(String command, Constants.CompileConfig compileConfig) {
command = MessageFormat.format(command, Constants.JudgeDir.TMPFS_DIR.getContent(),
compileConfig.getSrcName(), compileConfig.getExeName());
return Arrays.asList(command.split(" "));
}
}

View File

@ -0,0 +1,398 @@
package top.hcode.hoj.judge;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import top.hcode.hoj.common.exception.SystemError;
import top.hcode.hoj.util.Constants;
import java.io.File;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.*;
/**
* @Author: Himit_ZH
* @Date: 2021/4/16 12:15
* @Description: 判题流程解耦重构2.0该类负责输入数据进入程序进行测评
*/
@Slf4j
public class JudgeRun {
private static final int cpuNum = Runtime.getRuntime().availableProcessors();
private static final int SPJ_WA = 101;
private static final int SPJ_AC = 100;
private static final int SPJ_ERROR = 102;
private Long submitId;
private Long problemId;
private String testCasesDir;
private JSONObject testCasesInfo;
private Constants.RunConfig runConfig;
private Constants.RunConfig spjRunConfig;
public JudgeRun(Long submitId, Long problemId, String testCasesDir, JSONObject testCasesInfo, Constants.RunConfig runConfig, Constants.RunConfig spjRunConfig) {
this.submitId = submitId;
this.problemId = problemId;
this.testCasesDir = testCasesDir;
this.testCasesInfo = testCasesInfo;
this.runConfig = runConfig;
this.spjRunConfig = spjRunConfig;
}
public List<JSONObject> judgeAllCase(String userFileId,
Long maxTime,
Long maxMemory,
Boolean getUserOutput,
Boolean isRemoveEOFBlank,
String spjExeName)
throws SystemError, ExecutionException, InterruptedException {
if (testCasesInfo == null) {
throw new SystemError("The evaluation data of the problem does not exist", null, null);
}
// 使用线程池开启多线程测试每一测试输入数据
ExecutorService threadPool = new ThreadPoolExecutor(
cpuNum, // 核心线程数
cpuNum * 2, // 最大线程数最多几个线程并发
3,//当非核心线程无任务时几秒后结束该线程
TimeUnit.SECONDS,// 结束线程时间单位
new LinkedBlockingDeque<>(200), //阻塞队列限制等候线程数
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());//队列满了尝试去和最早的竞争也不会抛出异常
List<FutureTask<JSONObject>> futureTasks = new ArrayList<>();
JSONArray testcaseList = (JSONArray) testCasesInfo.get("testCases");
Boolean isSpj = testCasesInfo.getBool("isSpj");
// 默认给1.5倍题目限制时间用来测评
Double time = maxTime * 1.5;
final Long testTime = time.longValue();
// 用户输出的文件夹
String runDir = Constants.JudgeDir.RUN_WORKPLACE_DIR.getContent() + "/" + submitId;
for (int index = 0; index < testcaseList.size(); index++) {
// 将每个需要测试的线程任务加入任务列表中
final int testCaseId = index;
// 测试样例的路径
final String testCaseInputPath = testCasesDir + "/" + ((JSONObject) testcaseList.get(index)).getStr("inputName");
// 数据库表的测试样例id
final Long caseId = ((JSONObject) testcaseList.get(index)).getLong("caseId");
// 该测试点的满分
final Integer score = ((JSONObject) testcaseList.get(index)).getInt("score", 0);
final Long maxOutputSize = Math.max(((JSONObject) testcaseList.get(index)).getLong("outputSize", 0L) * 2, 16 * 1024 * 1024L);
if (!isSpj) {
futureTasks.add(new FutureTask<>(new Callable<JSONObject>() {
@Override
public JSONObject call() throws SystemError {
JSONObject result = judgeOneCase(userFileId,
testCaseId,
runDir,
testCaseInputPath,
testTime,// 默认给1.5倍题目限制时间用来测评
maxMemory,
maxOutputSize,
getUserOutput,
isRemoveEOFBlank);
result.set("caseId", caseId);
result.set("score", score);
return result;
}
}));
} else {
final String testCaseOutputPath = testCasesDir + "/" + ((JSONObject) testcaseList.get(index)).getStr("outputName");
futureTasks.add(new FutureTask<>(new Callable<JSONObject>() {
@Override
public JSONObject call() throws SystemError {
JSONObject result = spjJudgeOneCase(userFileId,
testCaseId,
runDir,
testCaseInputPath,
testCaseOutputPath,
testTime,// 默认给1.5倍题目限制时间用来测评
maxMemory,
maxOutputSize,
runConfig.getExeName(),
spjExeName,
getUserOutput);
result.set("caseId", caseId);
result.set("score", score);
return result;
}
}));
}
}
// 提交到线程池进行执行
for (FutureTask<JSONObject> futureTask : futureTasks) {
threadPool.submit(futureTask);
}
// 所有任务执行完成且等待队列中也无任务关闭线程池
if (!threadPool.isShutdown()) {
threadPool.shutdown();
}
// 阻塞主线程, 直至线程池关闭
try {
threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
log.error("判题线程池异常--------------->{}", e.getMessage());
}
List<JSONObject> result = new LinkedList<>();
// 获取线程返回结果
for (int i = 0; i < futureTasks.size(); i++) {
JSONObject tmp = futureTasks.get(i).get();
result.add(tmp);
}
return result;
}
private JSONObject spjJudgeOneCase(String userFileId,
Integer testCaseId,
String runDir,
String testCaseInputFilePath,
String testCaseOutputFilePath,
Long maxTime,
Long maxMemory,
Long maxOutputSize,
String userExeName,
String spjExeName,
Boolean getUserOutput) throws SystemError {
// 对于当前测试样例用户程序的输出对应生成的文件正常就输出数据错误就是输出错误信息
String realUserOutputFile = runDir + "/" + testCaseId + ".out";
// 特判程序的路径
String spjExeSrc = Constants.JudgeDir.SPJ_WORKPLACE_DIR.getContent() + "/" + problemId + "/" + spjExeName;
JSONArray judgeResultList = SandboxRun.spjTestCase(
parseRunCommand(runConfig.getCommand(), runConfig, null),
runConfig.getEnvs(),
userExeName,
userFileId,
testCaseInputFilePath,
maxTime,
maxOutputSize,
parseRunCommand(spjRunConfig.getCommand(), spjRunConfig, "tmp"),
spjRunConfig.getEnvs(),
spjExeSrc,
testCaseOutputFilePath,
spjExeName);
JSONObject result = new JSONObject();
// 用户程序输出写入文件
FileWriter fileWriter = new FileWriter(realUserOutputFile);
JSONObject userJudgeResult = (JSONObject) judgeResultList.get(0);
JSONObject spjJudgeResult = (JSONObject) judgeResultList.get(1);
// 特判程序输出或错误输出
String spjStdOut = ((JSONObject) spjJudgeResult.get("files")).getStr("stdout");
String spjErrOut = ((JSONObject) spjJudgeResult.get("files")).getStr("stderr");
// 获取用户程序运行内存 b-->kb
long memory = userJudgeResult.getLong("memory") / 1024;
// 获取用户程序运行时间 ns->ms
long time = userJudgeResult.getLong("time") / 1000000;
// 用户程序的退出状态码
int userExitCode = userJudgeResult.getInt("exitStatus");
// 记录错误信息
StringBuffer errMsg = new StringBuffer();
// 如果用户提交的代码运行无误
if (userJudgeResult.getInt("status").intValue() == Constants.Judge.STATUS_ACCEPTED.getStatus()) {
// 如果运行超过题目限制时间直接TLE
if (time >= maxTime) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (memory >= maxMemory) { // 如果运行超过题目限制空间直接MLE
result.set("status", Constants.Judge.STATUS_MEMORY_LIMIT_EXCEEDED.getStatus());
} else { // 校验特判程序的输出
// 根据特判程序的退出状态码进行判断
if (spjJudgeResult.getInt("exitStatus") == SPJ_AC) {
result.set("status", Constants.Judge.STATUS_ACCEPTED.getStatus());
} else if (spjJudgeResult.getInt("exitStatus") == SPJ_WA) {
result.set("status", Constants.Judge.STATUS_WRONG_ANSWER.getStatus());
} else {
throw new SystemError(spjErrOut, spjStdOut, spjErrOut);
}
}
} else if (userJudgeResult.getInt("status").intValue() == Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus()) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (userExitCode != 0) {
if (userExitCode < 32) {
result.set("status", Constants.Judge.STATUS_RUNTIME_ERROR.getStatus());
errMsg.append(String.format("ExitCode: %s (%s)\n", userExitCode, SandboxRun.signals.get(userExitCode)));
} else {
errMsg.append(String.format("ExitCode: %s\n", userExitCode));
}
String err = ((JSONObject) userJudgeResult.get("files")).getStr("stderr", null);
errMsg.append(err);
result.set("errMsg", errMsg.toString());
fileWriter.write(err);
} else {
result.set("status", userJudgeResult.getInt("status"));
}
// kb
result.set("memory", memory);
// ms
result.set("time", time);
if (getUserOutput) { // 如果需要获取用户对于该题目的输出,只提供特判程序输出
result.set("output", spjStdOut);
}
return result;
}
private JSONObject judgeOneCase(String userFileId,
Integer testCaseId,
String runDir,
String testCasePath,
Long maxTime,
Long maxMemory,
Long maxOutputSize,
Boolean getUserOutput,
Boolean isRemoveEOFBlank) throws SystemError {
// 调用安全沙箱使用测试点对程序进行测试
JSONArray judgeResultList = SandboxRun.testCase(parseRunCommand(runConfig.getCommand(), runConfig, null),
runConfig.getEnvs(),
testCasePath,
maxTime,
maxOutputSize,
runConfig.getExeName(),
userFileId);
JSONObject result = new JSONObject();
JSONObject judgeResult = (JSONObject) judgeResultList.get(0);
// 获取跑题用户输出或错误输出
String userStdOut = ((JSONObject) judgeResult.get("files")).getStr("stdout");
String userErrOut = ((JSONObject) judgeResult.get("files")).getStr("stderr");
StringBuffer errMsg = new StringBuffer();
// 获取程序运行内存 b-->kb
long memory = judgeResult.getLong("memory") / 1024;
// 获取程序运行时间 ns->ms
long time = judgeResult.getLong("time") / 1000000;
// 异常退出的状态码
int exitCode = judgeResult.getInt("exitStatus");
// 如果测试跑题无异常
if (judgeResult.getInt("status").intValue() == Constants.Judge.STATUS_ACCEPTED.getStatus()) {
// 对结果的时间损耗和空间损耗与题目限制做比较判断是否mle和tle
if (time >= maxTime) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (memory >= maxMemory) {
result.set("status", Constants.Judge.STATUS_MEMORY_LIMIT_EXCEEDED.getStatus());
} else {
// 与原测试数据输出的md5进行对比 AC或者是WA
result.set("status", compareOutput(testCaseId, userStdOut, isRemoveEOFBlank));
}
} else if (judgeResult.getInt("status") == Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus()) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (exitCode != 0) {
result.set("status", Constants.Judge.STATUS_RUNTIME_ERROR.getStatus());
if (exitCode < 32) {
errMsg.append(String.format("ExitCode: %s (%s)\n", exitCode, SandboxRun.signals.get(exitCode)));
} else {
errMsg.append(String.format("ExitCode: %s\n", exitCode));
}
} else {
result.set("status", judgeResult.getInt("status"));
}
// b
result.set("memory", memory);
// ns->ms
result.set("time", time);
if (!StringUtils.isEmpty(userStdOut)) {
// 对于当前测试样例用户程序的输出对应生成的文件
FileWriter stdWriter = new FileWriter(runDir + "/" + testCaseId + ".out");
stdWriter.write(userStdOut);
}
if (!StringUtils.isEmpty(userErrOut)) {
// 对于当前测试样例用户的错误提示生成对应文件
FileWriter errWriter = new FileWriter(runDir + "/" + testCaseId + ".err");
errWriter.write(userErrOut);
// 同时记录错误信息
errMsg.append(userErrOut);
}
// 记录该测试点的错误信息
if (!StringUtils.isEmpty(errMsg.toString())) {
result.set("errMsg", errMsg.toString());
}
if (getUserOutput) { // 如果需要获取用户对于该题目的输出
result.set("output", userStdOut);
}
return result;
}
private static List<String> parseRunCommand(String command, Constants.RunConfig runConfig, String testCaseTmpName) {
command = MessageFormat.format(command, Constants.JudgeDir.TMPFS_DIR.getContent(),
runConfig.getExeName(), Constants.JudgeDir.TMPFS_DIR.getContent() + File.separator + testCaseTmpName);
return Arrays.asList(command.split(" "));
}
public JSONObject getTestCasesInfo(int testCaseId) {
return (JSONObject) ((JSONArray) testCasesInfo.get("testCases")).get(testCaseId);
}
// 根据评测结果与用户程序输出的字符串MD5进行对比
private Integer compareOutput(int testCaseId, String userOutput, Boolean isRemoveEOFBlank) {
// 如果当前题目选择默认去掉字符串末位空格
if (isRemoveEOFBlank) {
String userOutputMd5 = DigestUtils.md5DigestAsHex(rtrim(userOutput).getBytes());
if (userOutputMd5.equals(getTestCasesInfo(testCaseId).getStr("EOFStrippedOutputMd5"))) {
return Constants.Judge.STATUS_ACCEPTED.getStatus();
}
} else { // 不选择默认去掉文末空格 与原数据进行对比
String userOutputMd5 = DigestUtils.md5DigestAsHex(userOutput.getBytes());
if (userOutputMd5.equals(getTestCasesInfo(testCaseId).getStr("outputMd5"))) {
return Constants.Judge.STATUS_ACCEPTED.getStatus();
}
}
// 如果不AC,进行PE判断否则为WA
String userOutputMd5 = DigestUtils.md5DigestAsHex(userOutput.replaceAll("\\s+", "").getBytes());
if (userOutputMd5.equals(getTestCasesInfo(testCaseId).getStr("allStrippedOutputMd5"))) {
return Constants.Judge.STATUS_PRESENTATION_ERROR.getStatus();
} else {
return Constants.Judge.STATUS_WRONG_ANSWER.getStatus();
}
}
// 去除所有的空格换行等空白符
public static String rtrim(String value) {
if (value == null) return null;
return value.replaceAll("\\s+$", "");
}
}

View File

@ -15,6 +15,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import top.hcode.hoj.common.exception.CompileError;
import top.hcode.hoj.common.exception.SubmitError;
import top.hcode.hoj.common.exception.SystemError;
import top.hcode.hoj.pojo.entity.Judge;
import top.hcode.hoj.pojo.entity.JudgeCase;
@ -35,68 +36,28 @@ import java.util.concurrent.*;
@Component
public class JudgeStrategy {
private static final int SPJ_WA = 101;
private static final int SPJ_AC = 100;
private static final int SPJ_ERROR = 102;
private static final int cpuNum = Runtime.getRuntime().availableProcessors();
private JSONObject testCasesInfo;
private String testCasesDir;
private Constants.CompileConfig compileConfig;
private Constants.RunConfig runConfig;
private Constants.RunConfig spjRunConfig;
private String code;
private String Language;
private Long pid;
private Problem problem;
private Judge judge;
@Autowired
private JudgeServiceImpl judgeService;
@Autowired
private ProblemCaseServiceImpl problemCaseService;
private ProblemTestCaseUtils problemTestCaseUtils;
@Autowired
private JudgeCaseServiceImpl judgeCaseService;
public void init(Problem problem, Judge judge) {
this.testCasesDir = Constants.JudgeDir.TEST_CASE_DIR.getContent() + "/problem_" + problem.getId();
this.runConfig = Constants.RunConfig.getRunnerByLanguage(judge.getLanguage());
this.spjRunConfig = Constants.RunConfig.getRunnerByLanguage("SPJ-" + problem.getSpjLanguage());
this.compileConfig = Constants.CompileConfig.getCompilerByLanguage(judge.getLanguage());
this.code = judge.getCode();
this.Language = judge.getLanguage();
this.problem = problem;
this.pid = problem.getId();
this.judge = judge;
}
public HashMap<String, Object> judge(Problem problem, Judge judge) {
// 初始化环境
init(problem, judge);
HashMap<String, Object> result = new HashMap<>();
// 编译好的临时代码文件id
String userFileId = null;
try {
// 对用户源代码进行编译 获取tmpfs中的fileId
userFileId = compile();
// 加载测试数据
this.testCasesInfo = loadTestCaseInfo(problem.getId(), problem.getCaseVersion(), !StringUtils.isEmpty(problem.getSpjCode()));
userFileId = Compiler.compile(Constants.CompileConfig.getCompilerByLanguage(judge.getLanguage()), judge.getCode(), judge.getLanguage());
// 测试数据文件所在文件夹
String testCasesDir = Constants.JudgeDir.TEST_CASE_DIR.getContent() + "/problem_" + problem.getId();
// 从文件中加载测试数据json
JSONObject testCasesInfo = problemTestCaseUtils.loadTestCaseInfo(problem.getId(), testCasesDir, problem.getCaseVersion(), !StringUtils.isEmpty(problem.getSpjCode()));
// 检查是否为spj同时是否有spj编译完成的文件若不存在就先编译生成该spj文件
Boolean hasSpjOrNotSpj = checkOrCompileSpj(problem);
// 如果该题为spj但是没有spj程序
@ -116,13 +77,24 @@ public class JudgeStrategy {
if (!StringUtils.isEmpty(problem.getSpjCode())) {
spjExeName = Constants.RunConfig.getRunnerByLanguage("SPJ-" + problem.getSpjLanguage()).getExeName();
}
JudgeRun judgeRun = new JudgeRun(
judge.getSubmitId(),
problem.getId(),
testCasesDir,
testCasesInfo,
Constants.RunConfig.getRunnerByLanguage(judge.getLanguage()),
Constants.RunConfig.getRunnerByLanguage("SPJ-" + problem.getSpjLanguage())
);
// 开始测试每个测试点
List<JSONObject> allCaseResultList = judgeAllCase(userFileId, problem.getTimeLimit() * 1L,
List<JSONObject> allCaseResultList = judgeRun.judgeAllCase(
userFileId,
problem.getTimeLimit() * 1L,
problem.getMemoryLimit() * 1024L,
runConfig.getExeName(), false, problem.getIsRemoveEndBlank(), spjExeName);
false,
problem.getIsRemoveEndBlank(),
spjExeName);
// 对全部测试点结果进行评判,获取最终评判结果
HashMap<String, Object> judgeInfo = getJudgeInfo(allCaseResultList, problem, judge);
return judgeInfo;
} catch (SystemError systemError) {
result.put("code", Constants.Judge.STATUS_SYSTEM_ERROR.getStatus());
@ -131,6 +103,11 @@ public class JudgeStrategy {
result.put("memory", 0L);
log.error("题号为:" + problem.getId() + "的题目提交id为" + judge.getSubmitId() + "在评测过程中发生系统性的异常------------------->{}",
systemError.getMessage() + "\n" + systemError.getStderr());
} catch (SubmitError submitError) {
result.put("code", Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus());
result.put("errMsg", submitError.getMessage() + ":" + submitError.getStderr());
result.put("time", 0L);
result.put("memory", 0L);
} catch (CompileError compileError) {
result.put("code", Constants.Judge.STATUS_COMPILE_ERROR.getStatus());
result.put("errMsg", compileError.getStderr());
@ -152,88 +129,6 @@ public class JudgeStrategy {
return result;
}
public static List<String> parseCompileCommand(String command, Constants.CompileConfig compileConfig) {
command = MessageFormat.format(command, Constants.JudgeDir.TMPFS_DIR.getContent(),
compileConfig.getSrcName(), compileConfig.getExeName());
return Arrays.asList(command.split(" "));
}
public static List<String> parseRunCommand(String command, Constants.RunConfig runConfig, String testCaseTmpName) {
command = MessageFormat.format(command, Constants.JudgeDir.TMPFS_DIR.getContent(),
runConfig.getExeName(), Constants.JudgeDir.TMPFS_DIR.getContent() + File.separator + testCaseTmpName);
return Arrays.asList(command.split(" "));
}
public String compile() throws SystemError, CompileError {
if (compileConfig == null) {
throw new CompileError("Unsupported language " + Language, null, null);
}
// 调用安全沙箱进行编译
JSONArray result = SandboxRun.compile(compileConfig.getMaxCpuTime(),
compileConfig.getMaxRealTime(),
compileConfig.getMaxMemory(),
128 * 1024 * 1024L,
compileConfig.getSrcName(),
compileConfig.getExeName(),
parseCompileCommand(compileConfig.getCommand(), compileConfig),
compileConfig.getEnvs(),
code,
true,
false,
null
);
JSONObject compileResult = (JSONObject) result.get(0);
if (compileResult.getInt("status") != Constants.Judge.STATUS_ACCEPTED.getStatus()) {
throw new CompileError("Compile Error.", ((JSONObject) compileResult.get("files")).getStr("stderr"), ((JSONObject) compileResult.get("files")).getStr("stderr"));
}
String fileId = ((JSONObject) compileResult.get("fileIds")).getStr(compileConfig.getExeName());
if (StringUtils.isEmpty(fileId)) {
throw new CompileError("Executable file not found.", null, null);
}
return fileId;
}
public Boolean compileSpj(String code, Long pid, String spjLanguage) throws SystemError, CompileError {
Constants.CompileConfig spjCompiler = Constants.CompileConfig.getCompilerByLanguage("SPJ-" + spjLanguage);
if (spjCompiler == null) {
throw new CompileError("Unsupported language " + spjLanguage, null, null);
}
Boolean copyOutExe = true;
if (pid == null) { // 题目id为空则不进行本地存储可能为新建题目时测试特判程序是否正常的判断而已
copyOutExe = false;
}
// 调用安全沙箱对特别判题程序进行编译
JSONArray res = SandboxRun.compile(spjCompiler.getMaxCpuTime(),
spjCompiler.getMaxRealTime(),
spjCompiler.getMaxMemory(),
128 * 1024 * 1024L,
spjCompiler.getSrcName(),
spjCompiler.getExeName(),
parseCompileCommand(spjCompiler.getCommand(), spjCompiler),
spjCompiler.getEnvs(),
code,
false,
copyOutExe,
Constants.JudgeDir.SPJ_WORKPLACE_DIR.getContent() + "/" + pid
);
JSONObject compileResult = (JSONObject) res.get(0);
if (compileResult.getInt("status") != Constants.Judge.STATUS_ACCEPTED.getStatus()) {
throw new SystemError("Special Judge Code Compile Error.", ((JSONObject) compileResult.get("files")).getStr("stderr"),
((JSONObject) compileResult.get("files")).getStr("stderr"));
}
return true;
}
public Boolean checkOrCompileSpj(Problem problem) throws CompileError, SystemError {
// 如果是需要特判的题目则需要检测特批程序是否已经编译否则进行编译
@ -241,318 +136,12 @@ public class JudgeStrategy {
Constants.CompileConfig spjCompiler = Constants.CompileConfig.getCompilerByLanguage(problem.getSpjLanguage());
// 如果不存在该已经编译好的特批程序则需要再次进行编译
if (!FileUtil.exist(Constants.JudgeDir.SPJ_WORKPLACE_DIR.getContent() + "/" + problem.getId() + "/" + spjCompiler.getExeName())) {
return compileSpj(problem.getSpjCode(), problem.getId(), problem.getSpjLanguage());
return Compiler.compileSpj(problem.getSpjCode(), problem.getId(), problem.getSpjLanguage());
}
}
return true;
}
public JSONObject spjJudgeOneCase(String userFileId, Integer testCaseId, String runDir, String testCaseInputFilePath,
String testCaseOutputFilePath, Long maxTime, Long maxMemory, Long maxOutputSize, String userExeName,
String spjExeName, Boolean getUserOutput) throws SystemError {
// 对于当前测试样例用户程序的输出对应生成的文件正常就输出数据错误就是输出错误信息
String realUserOutputFile = runDir + "/" + testCaseId + ".out";
// 特判程序的路径
String spjExeSrc = Constants.JudgeDir.SPJ_WORKPLACE_DIR.getContent() + "/" + pid + "/" + spjExeName;
JSONArray judgeResultList = SandboxRun.spjTestCase(parseRunCommand(runConfig.getCommand(), runConfig, null),
runConfig.getEnvs(),
userExeName,
userFileId,
testCaseInputFilePath,
maxTime,
maxOutputSize,
parseRunCommand(spjRunConfig.getCommand(), spjRunConfig, "tmp"),
spjRunConfig.getEnvs(),
spjExeSrc,
testCaseOutputFilePath,
spjExeName);
JSONObject result = new JSONObject();
// 用户程序输出写入文件
FileWriter fileWriter = new FileWriter(realUserOutputFile);
JSONObject userJudgeResult = (JSONObject) judgeResultList.get(0);
JSONObject spjJudgeResult = (JSONObject) judgeResultList.get(1);
// 特判程序输出或错误输出
String spjStdOut = ((JSONObject) spjJudgeResult.get("files")).getStr("stdout");
String spjErrOut = ((JSONObject) spjJudgeResult.get("files")).getStr("stderr");
// 获取用户程序运行内存 b-->kb
long memory = userJudgeResult.getLong("memory") / 1024;
// 获取用户程序运行时间 ns->ms
long time = userJudgeResult.getLong("time") / 1000000;
// 用户程序的退出状态码
int userExitCode = userJudgeResult.getInt("exitStatus");
// 记录错误信息
StringBuffer errMsg = new StringBuffer();
// 如果用户提交的代码运行无误
if (userJudgeResult.getInt("status") == Constants.Judge.STATUS_ACCEPTED.getStatus()) {
// 如果运行超过题目限制时间直接TLE
if (time >= maxTime) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (memory >= maxMemory) { // 如果运行超过题目限制空间直接MLE
result.set("status", Constants.Judge.STATUS_MEMORY_LIMIT_EXCEEDED.getStatus());
} else { // 校验特判程序的输出
// 根据特判程序的退出状态码进行判断
if (spjJudgeResult.getInt("exitStatus") == SPJ_AC) {
result.set("status", Constants.Judge.STATUS_ACCEPTED.getStatus());
} else if (spjJudgeResult.getInt("exitStatus") == SPJ_WA) {
result.set("status", Constants.Judge.STATUS_WRONG_ANSWER.getStatus());
} else {
throw new SystemError(spjErrOut, spjStdOut, spjErrOut);
}
}
} else if (userJudgeResult.getInt("status") == Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus()) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (userExitCode != 0) {
if (userExitCode < 32) {
result.set("status", Constants.Judge.STATUS_RUNTIME_ERROR.getStatus());
errMsg.append(String.format("ExitCode: %s (%s)\n", userExitCode, SandboxRun.signals.get(userExitCode)));
} else {
errMsg.append(String.format("ExitCode: %s\n", userExitCode));
}
String err = ((JSONObject) userJudgeResult.get("files")).getStr("stderr", null);
errMsg.append(err);
result.set("errMsg", errMsg.toString());
fileWriter.write(err);
} else {
result.set("status", userJudgeResult.getInt("status"));
}
// kb
result.set("memory", memory);
// ms
result.set("time", time);
if (getUserOutput) { // 如果需要获取用户对于该题目的输出,只提供特判程序输出
result.set("output", spjStdOut);
}
return result;
}
public JSONObject judgeOneCase(String userFileId, Integer testCaseId, String runDir, String testCasePath, Long maxTime,
Long maxMemory, Long maxOutputSize, Boolean getUserOutput, Boolean isRemoveEOFBlank) throws SystemError {
// 调用安全沙箱使用测试点对程序进行测试
JSONArray judgeResultList = SandboxRun.testCase(parseRunCommand(runConfig.getCommand(), runConfig, null),
runConfig.getEnvs(),
testCasePath,
maxTime,
maxOutputSize,
runConfig.getExeName(),
userFileId);
JSONObject result = new JSONObject();
JSONObject judgeResult = (JSONObject) judgeResultList.get(0);
// 获取跑题用户输出或错误输出
String userStdOut = ((JSONObject) judgeResult.get("files")).getStr("stdout");
String userErrOut = ((JSONObject) judgeResult.get("files")).getStr("stderr");
StringBuffer errMsg = new StringBuffer();
// 获取程序运行内存 b-->kb
long memory = judgeResult.getLong("memory") / 1024;
// 获取程序运行时间 ns->ms
long time = judgeResult.getLong("time") / 1000000;
// 异常退出的状态码
int exitCode = judgeResult.getInt("exitStatus");
// 如果测试跑题无异常
if (judgeResult.getInt("status") == Constants.Judge.STATUS_ACCEPTED.getStatus()) {
// 对结果的时间损耗和空间损耗与题目限制做比较判断是否mle和tle
if (time >= maxTime) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (memory >= maxMemory) {
result.set("status", Constants.Judge.STATUS_MEMORY_LIMIT_EXCEEDED.getStatus());
} else {
// 与原测试数据输出的md5进行对比 AC或者是WA
result.set("status", compareOutput(testCaseId, userStdOut, isRemoveEOFBlank));
}
} else if (judgeResult.getInt("status") == Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus()) {
result.set("status", Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED.getStatus());
} else if (exitCode != 0) {
result.set("status", Constants.Judge.STATUS_RUNTIME_ERROR.getStatus());
if (exitCode < 32) {
errMsg.append(String.format("ExitCode: %s (%s)\n", exitCode, SandboxRun.signals.get(exitCode)));
} else {
errMsg.append(String.format("ExitCode: %s\n", exitCode));
}
} else {
result.set("status", judgeResult.getInt("status"));
}
// b
result.set("memory", memory);
// ns->ms
result.set("time", time);
if (!StringUtils.isEmpty(userStdOut)) {
// 对于当前测试样例用户程序的输出对应生成的文件
FileWriter stdWriter = new FileWriter(runDir + "/" + testCaseId + ".out");
stdWriter.write(userStdOut);
}
if (!StringUtils.isEmpty(userErrOut)) {
// 对于当前测试样例用户的错误提示生成对应文件
FileWriter errWriter = new FileWriter(runDir + "/" + testCaseId + ".err");
errWriter.write(userErrOut);
// 同时记录错误信息
errMsg.append(userErrOut);
}
// 记录该测试点的错误信息
if (!StringUtils.isEmpty(errMsg.toString())) {
result.set("errMsg", errMsg.toString());
}
if (getUserOutput) { // 如果需要获取用户对于该题目的输出
result.set("output", userStdOut);
}
return result;
}
public List<JSONObject> judgeAllCase(String userFileId, Long maxTime, Long maxMemory, @Nullable String userExeName,
Boolean getUserOutput, Boolean isRemoveEOFBlank, String spjExeName) throws SystemError, ExecutionException, InterruptedException {
if (testCasesInfo == null) {
throw new SystemError("The evaluation data of the problem does not exist", null, null);
}
// 使用线程池开启多线程测试每一测试输入数据
ExecutorService threadPool = new ThreadPoolExecutor(
cpuNum, // 核心线程数
cpuNum * 2, // 最大线程数最多几个线程并发
3,//当非核心线程无任务时几秒后结束该线程
TimeUnit.SECONDS,// 结束线程时间单位
new LinkedBlockingDeque<>(200), //阻塞队列限制等候线程数
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());//队列满了尝试去和最早的竞争也不会抛出异常
List<FutureTask<JSONObject>> futureTasks = new ArrayList<>();
JSONArray testcaseList = (JSONArray) testCasesInfo.get("testCases");
Boolean isSpj = testCasesInfo.getBool("isSpj");
// 默认给1.5倍题目限制时间用来测评
Double time = maxTime * 1.5;
final Long testTime = time.longValue();
// 用户输出的文件夹
String runDir = Constants.JudgeDir.RUN_WORKPLACE_DIR.getContent() + "/" + judge.getSubmitId();
for (int index = 0; index < testcaseList.size(); index++) {
// 将每个需要测试的线程任务加入任务列表中
final int testCaseId = index;
// 测试样例的路径
final String testCaseInputPath = testCasesDir + "/" + ((JSONObject) testcaseList.get(index)).getStr("inputName");
// 数据库表的测试样例id
final Long caseId = ((JSONObject) testcaseList.get(index)).getLong("caseId");
// 该测试点的满分
final Integer score = ((JSONObject) testcaseList.get(index)).getInt("score", 0);
final Long maxOutputSize = Math.max(((JSONObject) testcaseList.get(index)).getLong("outputSize", 0L) * 2, 16 * 1024 * 1024L);
if (!isSpj) {
futureTasks.add(new FutureTask<>(new Callable<JSONObject>() {
@Override
public JSONObject call() throws SystemError {
JSONObject result = judgeOneCase(userFileId,
testCaseId,
runDir,
testCaseInputPath,
testTime,// 默认给1.5倍题目限制时间用来测评
maxMemory,
maxOutputSize,
getUserOutput,
isRemoveEOFBlank);
result.set("caseId", caseId);
result.set("score", score);
return result;
}
}));
} else {
final String testCaseOutputPath = testCasesDir + "/" + ((JSONObject) testcaseList.get(index)).getStr("outputName");
futureTasks.add(new FutureTask<>(new Callable<JSONObject>() {
@Override
public JSONObject call() throws SystemError {
JSONObject result = spjJudgeOneCase(userFileId,
testCaseId,
runDir,
testCaseInputPath,
testCaseOutputPath,
testTime,// 默认给1.5倍题目限制时间用来测评
maxMemory,
maxOutputSize,
userExeName,
spjExeName,
getUserOutput);
result.set("caseId", caseId);
result.set("score", score);
return result;
}
}));
}
}
// 提交到线程池进行执行
for (FutureTask<JSONObject> futureTask : futureTasks) {
threadPool.submit(futureTask);
}
// 所有任务执行完成且等待队列中也无任务关闭线程池
if (!threadPool.isShutdown()) {
threadPool.shutdown();
}
// 阻塞主线程, 直至线程池关闭
try {
threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
log.error("判题线程池异常--------------->{}", e.getMessage());
}
List<JSONObject> result = new LinkedList<>();
// 获取线程返回结果
for (int i = 0; i < futureTasks.size(); i++) {
JSONObject tmp = futureTasks.get(i).get();
result.add(tmp);
}
return result;
}
// 根据评测结果与用户程序输出的字符串MD5进行对比
public Integer compareOutput(int testCaseId, String userOutput, Boolean isRemoveEOFBlank) {
JSONObject result = new JSONObject();
// 如果当前题目选择默认去掉字符串末位空格
if (isRemoveEOFBlank) {
String userOutputMd5 = DigestUtils.md5DigestAsHex(rtrim(userOutput).getBytes());
if (userOutputMd5.equals(getTestCasesInfo(testCaseId).getStr("EOFStrippedOutputMd5"))) {
return Constants.Judge.STATUS_ACCEPTED.getStatus();
}
} else { // 不选择默认去掉文末空格 与原数据进行对比
String userOutputMd5 = DigestUtils.md5DigestAsHex(userOutput.getBytes());
if (userOutputMd5.equals(getTestCasesInfo(testCaseId).getStr("outputMd5"))) {
return Constants.Judge.STATUS_ACCEPTED.getStatus();
}
}
// 如果不AC,进行PE判断否则为WA
String userOutputMd5 = DigestUtils.md5DigestAsHex(userOutput.replaceAll("\\s+", "").getBytes());
if (userOutputMd5.equals(getTestCasesInfo(testCaseId).getStr("allStrippedOutputMd5"))) {
return Constants.Judge.STATUS_PRESENTATION_ERROR.getStatus();
} else {
return Constants.Judge.STATUS_WRONG_ANSWER.getStatus();
}
}
// 获取判题的运行时间运行空间OI得分
public HashMap<String, Object> computeResultInfo(List<JSONObject> testCaseResultList, Boolean isACM, Integer errorCaseNum, Integer totalScore) {
HashMap<String, Object> result = new HashMap<>();
@ -637,107 +226,4 @@ public class JudgeStrategy {
return result;
}
public JSONObject getTestCasesInfo(int testCaseId) {
return (JSONObject) ((JSONArray) testCasesInfo.get("testCases")).get(testCaseId);
}
// 初始化测试数据写成json文件
public JSONObject initTestCase(List<HashMap<String, Object>> testCases, Long problemId, String version, Boolean isSpj) throws SystemError, UnsupportedEncodingException {
if (testCases == null || testCases.size() == 0) {
throw new SystemError("题号为:" + problemId + "的评测数据为空!", null, "The test cases does not exist.");
}
JSONObject result = new JSONObject();
result.set("isSpj", isSpj);
result.set("version", version);
result.set("testCasesSize", testCases.size());
result.set("testCases", new JSONArray());
String testCasesDir = Constants.JudgeDir.TEST_CASE_DIR.getContent() + "/problem_" + problemId;
// 无论有没有测试数据一旦执行该函数一律清空重新生成该题目对应的测试数据文件
FileUtil.del(testCasesDir);
for (int index = 0; index < testCases.size(); index++) {
JSONObject jsonObject = new JSONObject();
String inputName = (index + 1) + ".in";
jsonObject.set("caseId", (long) testCases.get(index).get("caseId"));
jsonObject.set("score", testCases.get(index).getOrDefault("score", null));
jsonObject.set("inputName", inputName);
// 生成对应文件
FileWriter infileWriter = new FileWriter(testCasesDir + "/" + inputName, CharsetUtil.UTF_8);
// 将该测试数据的输入写入到文件
infileWriter.write((String) testCases.get(index).get("input"));
String outputName = (index + 1) + ".out";
jsonObject.set("outputName", outputName);
// 生成对应文件
String outputData = (String) testCases.get(index).get("output");
FileWriter outFile = new FileWriter(testCasesDir + "/" + outputName, CharsetUtil.UTF_8);
outFile.write(outputData);
// spj是根据特判程序输出判断结果所以无需初始化测试数据
if (!isSpj) {
// 原数据MD5
jsonObject.set("outputMd5", DigestUtils.md5DigestAsHex(outputData.getBytes()));
// 原数据大小
jsonObject.set("outputSize", outputData.getBytes("utf-8").length);
// 去掉全部空格的MD5用来判断pe
jsonObject.set("allStrippedOutputMd5", DigestUtils.md5DigestAsHex(outputData.replaceAll("\\s+", "").getBytes()));
// 默认去掉文末空格的MD5
jsonObject.set("EOFStrippedOutputMd5", DigestUtils.md5DigestAsHex(rtrim(outputData).getBytes()));
}
((JSONArray) result.get("testCases")).put(index, jsonObject);
}
FileWriter infoFile = new FileWriter(testCasesDir + "/info", CharsetUtil.UTF_8);
// 写入记录文件
infoFile.write(JSONUtil.toJsonStr(result));
return result;
}
// 获取指定题目的info数据
public JSONObject loadTestCaseInfo(Long problemId, String version, Boolean isSpj) throws SystemError, UnsupportedEncodingException {
String testCasesDir = Constants.JudgeDir.TEST_CASE_DIR.getContent() + "/problem_" + problemId;
if (FileUtil.exist(testCasesDir + "/info")) {
FileReader fileReader = new FileReader(testCasesDir + "/info", CharsetUtil.UTF_8);
String infoStr = fileReader.readString();
JSONObject testcaseInfo = JSONUtil.parseObj(infoStr);
// 测试样例被改动需要重新生成
if (!testcaseInfo.getStr("version", null).equals(version)) {
return tryInitTestCaseInfo(problemId, version, isSpj);
}
return testcaseInfo;
} else {
return tryInitTestCaseInfo(problemId, version, isSpj);
}
}
// 若没有测试数据则尝试从数据库获取并且初始化到本地
public JSONObject tryInitTestCaseInfo(Long problemId, String version, Boolean isSpj) throws SystemError, UnsupportedEncodingException {
QueryWrapper<ProblemCase> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("pid", problemId);
List<ProblemCase> problemCases = problemCaseService.list(queryWrapper);
List<HashMap<String, Object>> testCases = new LinkedList<>();
for (ProblemCase problemCase : problemCases) {
HashMap<String, Object> tmp = new HashMap<>();
tmp.put("input", problemCase.getInput());
tmp.put("output", problemCase.getOutput());
tmp.put("caseId", problemCase.getId());
tmp.put("score", problemCase.getScore());
testCases.add(tmp);
}
return initTestCase(testCases, problemId, version, isSpj);
}
public static String rtrim(String value) {
if (value == null) return null;
return value.replaceAll("\\s+$", "");
}
}

View File

@ -0,0 +1,134 @@
package top.hcode.hoj.judge;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import top.hcode.hoj.common.exception.SystemError;
import top.hcode.hoj.pojo.entity.ProblemCase;
import top.hcode.hoj.service.impl.ProblemCaseServiceImpl;
import top.hcode.hoj.util.Constants;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2021/4/16 13:21
* @Description: 判题流程解耦重构2.0该类只负责题目测试数据的检查与初始化
*/
@Component
public class ProblemTestCaseUtils {
@Autowired
ProblemCaseServiceImpl problemCaseService;
// 初始化测试数据写成json文件
public JSONObject initTestCase(List<HashMap<String, Object>> testCases,
Long problemId,
String version,
Boolean isSpj) throws SystemError, UnsupportedEncodingException {
if (testCases == null || testCases.size() == 0) {
throw new SystemError("题号为:" + problemId + "的评测数据为空!", null, "The test cases does not exist.");
}
JSONObject result = new JSONObject();
result.set("isSpj", isSpj);
result.set("version", version);
result.set("testCasesSize", testCases.size());
result.set("testCases", new JSONArray());
String testCasesDir = Constants.JudgeDir.TEST_CASE_DIR.getContent() + "/problem_" + problemId;
// 无论有没有测试数据一旦执行该函数一律清空重新生成该题目对应的测试数据文件
FileUtil.del(testCasesDir);
for (int index = 0; index < testCases.size(); index++) {
JSONObject jsonObject = new JSONObject();
String inputName = (index + 1) + ".in";
jsonObject.set("caseId", (long) testCases.get(index).get("caseId"));
jsonObject.set("score", testCases.get(index).getOrDefault("score", null));
jsonObject.set("inputName", inputName);
// 生成对应文件
FileWriter infileWriter = new FileWriter(testCasesDir + "/" + inputName, CharsetUtil.UTF_8);
// 将该测试数据的输入写入到文件
infileWriter.write((String) testCases.get(index).get("input"));
String outputName = (index + 1) + ".out";
jsonObject.set("outputName", outputName);
// 生成对应文件
String outputData = (String) testCases.get(index).get("output");
FileWriter outFile = new FileWriter(testCasesDir + "/" + outputName, CharsetUtil.UTF_8);
outFile.write(outputData);
// spj是根据特判程序输出判断结果所以无需初始化测试数据
if (!isSpj) {
// 原数据MD5
jsonObject.set("outputMd5", DigestUtils.md5DigestAsHex(outputData.getBytes()));
// 原数据大小
jsonObject.set("outputSize", outputData.getBytes("utf-8").length);
// 去掉全部空格的MD5用来判断pe
jsonObject.set("allStrippedOutputMd5", DigestUtils.md5DigestAsHex(outputData.replaceAll("\\s+", "").getBytes()));
// 默认去掉文末空格的MD5
jsonObject.set("EOFStrippedOutputMd5", DigestUtils.md5DigestAsHex(rtrim(outputData).getBytes()));
}
((JSONArray) result.get("testCases")).put(index, jsonObject);
}
FileWriter infoFile = new FileWriter(testCasesDir + "/info", CharsetUtil.UTF_8);
// 写入记录文件
infoFile.write(JSONUtil.toJsonStr(result));
return result;
}
// 获取指定题目的info数据
public JSONObject loadTestCaseInfo(Long problemId, String testCasesDir, String version, Boolean isSpj) throws SystemError, UnsupportedEncodingException {
if (FileUtil.exist(testCasesDir + "/info")) {
FileReader fileReader = new FileReader(testCasesDir + "/info", CharsetUtil.UTF_8);
String infoStr = fileReader.readString();
JSONObject testcaseInfo = JSONUtil.parseObj(infoStr);
// 测试样例被改动需要重新生成
if (!testcaseInfo.getStr("version", null).equals(version)) {
return tryInitTestCaseInfo(problemId, version, isSpj);
}
return testcaseInfo;
} else {
return tryInitTestCaseInfo(problemId, version, isSpj);
}
}
// 若没有测试数据则尝试从数据库获取并且初始化到本地
public JSONObject tryInitTestCaseInfo(Long problemId, String version, Boolean isSpj) throws SystemError, UnsupportedEncodingException {
QueryWrapper<ProblemCase> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("pid", problemId);
List<ProblemCase> problemCases = problemCaseService.list(queryWrapper);
List<HashMap<String, Object>> testCases = new LinkedList<>();
for (ProblemCase problemCase : problemCases) {
HashMap<String, Object> tmp = new HashMap<>();
tmp.put("input", problemCase.getInput());
tmp.put("output", problemCase.getOutput());
tmp.put("caseId", problemCase.getId());
tmp.put("score", problemCase.getScore());
testCases.add(tmp);
}
return initTestCase(testCases, problemId, version, isSpj);
}
// 去除所有的空格换行等空白符
public static String rtrim(String value) {
if (value == null) return null;
return value.replaceAll("\\s+$", "");
}
}

View File

@ -68,7 +68,7 @@ public class SandboxRun {
public static final HashMap<String, Integer> RESULT_MAP_STATUS = new HashMap<>();
private static final int maxProcessNumber = 64;
private static final int maxProcessNumber = 128;
private static final int TIME_LIMIT_MS = 8000;
@ -144,6 +144,8 @@ public class SandboxRun {
if (ex.getRawStatusCode() != 200) {
throw new SystemError("Cannot connect to sandbox service.", null, ex.getResponseBodyAsString());
}
} catch (Exception e){
throw new SystemError("Call SandBox Error.", null, e.getMessage());
}
return null;
}

View File

@ -10,6 +10,7 @@ import top.hcode.hoj.common.exception.CompileError;
import top.hcode.hoj.common.exception.SystemError;
import top.hcode.hoj.dao.JudgeMapper;
import top.hcode.hoj.judge.*;
import top.hcode.hoj.judge.Compiler;
import top.hcode.hoj.pojo.entity.Contest;
import top.hcode.hoj.pojo.entity.Judge;
import top.hcode.hoj.pojo.entity.Problem;
@ -64,10 +65,11 @@ public class JudgeServiceImpl extends ServiceImpl<JudgeMapper, Judge> implements
HashMap<String, Object> judgeResult = judgeStrategy.judge(problem, judge);
// 如果是编译失败或者系统错误就有错误提醒
// 如果是编译失败提交错误或者系统错误就有错误提醒
if (judgeResult.get("code") == Constants.Judge.STATUS_COMPILE_ERROR.getStatus() ||
judgeResult.get("code") == Constants.Judge.STATUS_SYSTEM_ERROR.getStatus() ||
judgeResult.get("code") == Constants.Judge.STATUS_RUNTIME_ERROR.getStatus()) {
judgeResult.get("code") == Constants.Judge.STATUS_RUNTIME_ERROR.getStatus() ||
judgeResult.get("code") == Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus()) {
judge.setErrorMessage((String) judgeResult.getOrDefault("errMsg", ""));
}
// 设置最终结果状态码
@ -86,7 +88,7 @@ public class JudgeServiceImpl extends ServiceImpl<JudgeMapper, Judge> implements
}
public Boolean compileSpj(String code, Long pid, String spjLanguage) throws CompileError, SystemError {
return judgeStrategy.compileSpj(code, pid, spjLanguage);
return Compiler.compileSpj(code, pid, spjLanguage);
}
@Override

View File

@ -7,7 +7,7 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initial-size: 10 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
min-idle: 10 # 最小连接池数量
min-idle: 20 # 最小连接池数量
maxActive: 200 # 最大连接池数量
maxWait: 60000 # 获取连接时最大等待时间单位毫秒。配置了maxWait之后缺省启用公平锁并发效率会有所下降如果需要可以通过配置
timeBetweenEvictionRunsMillis: 60000 # 关闭空闲连接的检测时间间隔.Destroy线程会检测连接的间隔时间如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。

View File

@ -7,7 +7,7 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initial-size: 10 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
min-idle: 10 # 最小连接池数量
min-idle: 20 # 最小连接池数量
maxActive: 200 # 最大连接池数量
maxWait: 60000 # 获取连接时最大等待时间单位毫秒。配置了maxWait之后缺省启用公平锁并发效率会有所下降如果需要可以通过配置
timeBetweenEvictionRunsMillis: 60000 # 关闭空闲连接的检测时间间隔.Destroy线程会检测连接的间隔时间如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。

Some files were not shown because too many files have changed in this diff Show More