重构解耦JudgeServer判题逻辑,添加部署文档
This commit is contained in:
parent
d89bfcfc08
commit
2dd8522c18
|
@ -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 |
|
||||
|
||||
# 二、部署
|
||||
|
||||
**注意:比较适用于熟悉springboot,docker的开发人员打包部署**
|
||||
|
||||
部署文档:[https://gitee.com/himitzh0730/hoj/tree/master/docs](https://gitee.com/himitzh0730/hoj/tree/master/docs)
|
||||
|
||||
> 简略介绍
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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, "暂无数据");
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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是否为纯数字
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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" +
|
||||
|
|
|
@ -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:");
|
||||
|
||||
|
|
|
@ -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 # 连接的最小生存时间.连接保持空闲而不被驱逐的最小时间
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 # 连接的最小生存时间.连接保持空闲而不被驱逐的最小时间
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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, "判题机评测完成!");
|
||||
|
|
|
@ -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(" "));
|
||||
}
|
||||
}
|
|
@ -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+$", "");
|
||||
}
|
||||
}
|
|
@ -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+$", "");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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+$", "");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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则关闭物理连接。
|
||||
|
|
|
@ -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则关闭物理连接。
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue