feat(remote_judger): add codeforces

This commit is contained in:
Baoshuo Ren 2023-01-20 16:35:02 +08:00
parent 3a2e3ce1db
commit 3d6102a3f9
Signed by: baoshuo
GPG Key ID: 00CB9680AB29F51A
15 changed files with 835 additions and 101 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ docker-compose.local.yml
.config.php
.config.development.php
.config.local.php
*.development.env
*.local.env

View File

@ -52,9 +52,14 @@ services:
restart: always
volumes:
- ./uoj_data/judger/log:/opt/uoj_judger/log
env_file:
- remote-judger.development.env
environment:
- DEV=true
- UOJ_PROTOCOL=http
- UOJ_HOST=uoj-web
- UOJ_JUDGER_NAME=remote_judger
- UOJ_JUDGER_PASSWORD=_judger_password_
uoj-web:
build:

View File

@ -9,4 +9,4 @@ COPY . .
RUN npm run build
CMD [ "node", "dist/entrypoint.js" ]
CMD [ "node", "--experimental-specifier-resolution=node", "dist/entrypoint.js" ]

5
remote_judger/README Normal file
View File

@ -0,0 +1,5 @@
本模块借鉴了以下项目的源码:
- https://github.com/hydro-dev/Hydro/blob/feb51804766e35dbd13f7cb74fda95c0b783c49d/packages/vjudge/
在此表示感谢。

View File

@ -0,0 +1,2 @@
USE `app_uoj233`;
insert into judger_info (judger_name, password, ip) values ('remote_judger', '_judger_password_', 'uoj-remote-judger');

View File

@ -9,15 +9,15 @@
"version": "0.0.0",
"license": "AGPL-3.0",
"dependencies": {
"fs-extra": "^11.1.0",
"jsdom": "^21.0.0",
"math-sum": "^2.0.0",
"reggol": "^1.3.4",
"superagent": "^8.0.6",
"superagent-prefix": "^0.0.2",
"superagent-proxy": "^3.0.0",
"xml-js": "^1.6.11"
"superagent-proxy": "^3.0.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.18",
@ -41,6 +41,16 @@
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
"dev": true
},
"node_modules/@types/fs-extra": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz",
"integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==",
"dev": true,
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
@ -58,6 +68,15 @@
"parse5": "^7.0.0"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
"integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
@ -464,16 +483,16 @@
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz",
"integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=6 <7 || >=8"
"node": ">=14.14"
}
},
"node_modules/ftp": {
@ -530,6 +549,35 @@
"node": ">= 6"
}
},
"node_modules/get-uri/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/get-uri/node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/get-uri/node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@ -712,9 +760,12 @@
}
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@ -1047,11 +1098,6 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@ -1177,11 +1223,6 @@
"node": ">=6.4.0 <13 || >=14"
}
},
"node_modules/superagent-prefix": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/superagent-prefix/-/superagent-prefix-0.0.2.tgz",
"integrity": "sha512-LssjLYPklgE/ZlaowG+DZjSvXkiwyT47U3D1tvgDlAeG7w7ew5MowVXwepIt+yoVql2xQmgR/sJr2sivkRyN7g=="
},
"node_modules/superagent-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-3.0.0.tgz",
@ -1286,11 +1327,11 @@
}
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"engines": {
"node": ">= 4.0.0"
"node": ">= 10.0.0"
}
},
"node_modules/unpipe": {
@ -1419,17 +1460,6 @@
}
}
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@ -1469,6 +1499,16 @@
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
"dev": true
},
"@types/fs-extra": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz",
"integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==",
"dev": true,
"requires": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"@types/js-yaml": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
@ -1486,6 +1526,15 @@
"parse5": "^7.0.0"
}
},
"@types/jsonfile": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
"integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
@ -1794,13 +1843,13 @@
}
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz",
"integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"ftp": {
@ -1844,6 +1893,29 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"requires": {
"graceful-fs": "^4.1.6"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
}
}
},
@ -1985,11 +2057,12 @@
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
"graceful-fs": "^4.1.6"
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"levn": {
@ -2249,11 +2322,6 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@ -2349,11 +2417,6 @@
"semver": "^7.3.8"
}
},
"superagent-prefix": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/superagent-prefix/-/superagent-prefix-0.0.2.tgz",
"integrity": "sha512-LssjLYPklgE/ZlaowG+DZjSvXkiwyT47U3D1tvgDlAeG7w7ew5MowVXwepIt+yoVql2xQmgR/sJr2sivkRyN7g=="
},
"superagent-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-3.0.0.tgz",
@ -2425,9 +2488,9 @@
"dev": true
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
},
"unpipe": {
"version": "1.0.0",
@ -2513,14 +2576,6 @@
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
"requires": {}
},
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"requires": {
"sax": "^1.2.4"
}
},
"xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",

View File

@ -12,15 +12,15 @@
"license": "AGPL-3.0",
"private": true,
"dependencies": {
"fs-extra": "^11.1.0",
"jsdom": "^21.0.0",
"math-sum": "^2.0.0",
"reggol": "^1.3.4",
"superagent": "^8.0.6",
"superagent-prefix": "^0.0.2",
"superagent-proxy": "^3.0.0",
"xml-js": "^1.6.11"
"superagent-proxy": "^3.0.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.18",

View File

@ -1,10 +1,13 @@
import fs from 'fs';
import fs from 'fs-extra';
import superagent from 'superagent';
import proxy from 'superagent-proxy';
import prefix from 'superagent-prefix';
import Logger from '@/utils/logger';
import sleep from '@/utils/sleep';
import * as TIME from '@/utils/time';
import Logger from './utils/logger';
import sleep from './utils/sleep';
import * as TIME from './utils/time';
import htmlspecialchars from './utils/htmlspecialchars';
import { apply } from './vjudge';
import path from 'path';
import child from 'child_process';
proxy(superagent);
@ -22,41 +25,152 @@ interface UOJSubmission {
problem_mtime: number;
content: any;
status: string;
judge_time: string;
}
export default async function daemon(config: UOJConfig) {
const agent = superagent
.agent()
.use(prefix(`${this.config.server_url}/judge`))
.type('application/x-www-form-urlencoded')
.serialize(data =>
new URLSearchParams({
const request = (url: string, data = {}) =>
superagent
.post(`${config.server_url}/judge${url}`)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(
Object.entries({
judger_name: config.judger_name,
password: config.password,
...data,
}).toString()
})
.map(
([k, v]) =>
`${k}=${encodeURIComponent(
typeof v === 'string' ? v : JSON.stringify(v)
)}`
)
.join('&')
);
const vjudge = await apply(request);
while (true) {
try {
const { body, error } = await agent.post('/submit');
const { text, error } = await request('/submit');
if (error) {
logger.error(error.message);
logger.error('/submit', error.message);
await sleep(TIME.second);
} else if (body === 'Nothing to judge') {
await sleep(2 * TIME.second);
await sleep(3 * TIME.second);
} else if (text.startsWith('Nothing to judge')) {
await sleep(3 * TIME.second);
} else {
const data: UOJSubmission = JSON.parse(body);
const data: UOJSubmission = JSON.parse(text);
const { id, content, judge_time } = data;
const config = Object.fromEntries(content.config);
const tmpdir = `/tmp/s2oj_rmj/${id}/`;
logger.info('Start judging', data.id);
fs.ensureDirSync(tmpdir);
// TODO: judge
const reportError = async (error: string, details: string) => {
await request('/submit', {
submit: true,
fetch_new: false,
id,
result: JSON.stringify({
status: 'Judged',
score: 0,
error,
details: `<error>${htmlspecialchars(details)}</error>`,
}),
judge_time,
});
};
// Download source code
logger.debug('Downloading source code.', id);
const zipFilePath = path.resolve(tmpdir, 'all.zip');
const res = request(`/download${content.file_name}`);
const stream = fs.createWriteStream(zipFilePath);
res.pipe(stream);
try {
await new Promise((resolve, reject) => {
stream.on('finish', resolve);
stream.on('error', reject);
});
} catch (e) {
await reportError(
'Judgment Failed',
`Failed to download source code.`
);
logger.error('Failed to download source code.', id, e.message);
fs.removeSync(tmpdir);
continue;
}
// Unzip source code
logger.debug('Unzipping source code.', id);
const extractedPath = path.resolve(tmpdir, 'all');
try {
await new Promise((resolve, reject) => {
child.exec(`unzip ${zipFilePath} -d ${extractedPath}`, e => {
if (e) reject(e);
else resolve(true);
});
});
} catch (e) {
await reportError('Judgment Failed', `Failed to unzip source code.`);
logger.error('Failed to unzip source code.', id, e.message);
fs.removeSync(tmpdir);
continue;
}
// Read source code
logger.debug('Reading source code.', id);
const sourceCodePath = path.resolve(extractedPath, 'answer.code');
let code = '';
try {
code = fs.readFileSync(sourceCodePath, 'utf-8');
} catch (e) {
await reportError('Judgment Failed', `Failed to read source code.`);
logger.error('Failed to read source code.', id, e.message);
fs.removeSync(tmpdir);
continue;
}
// Start judging
logger.info('Start judging', id, `(problem ${data.problem_id})`);
try {
await vjudge.judge(
id,
config.remote_online_judge,
config.remote_problem_id,
config.answer_language,
code,
judge_time
);
} catch (err) {
await reportError(
'Judgment Failed',
'No details, please contact admin!'
);
logger.error('Judgment Failed.', id, err.message);
fs.removeSync(tmpdir);
continue;
}
fs.removeSync(tmpdir);
}
} catch (err) {
logger.error(err);
await sleep(TIME.second);
logger.error(err.message);
await sleep(3 * TIME.second);
}
}
}

View File

@ -0,0 +1,31 @@
export interface RemoteAccount {
type: string;
cookie?: string[];
handle: string;
password: string;
endpoint?: string;
proxy?: string;
}
export type NextFunction = (body: Partial<any>) => void;
export interface IBasicProvider {
ensureLogin(): Promise<boolean | string>;
submitProblem(
id: string,
lang: string,
code: string,
submissionId: number,
next: NextFunction,
end: NextFunction
): Promise<string | void>;
waitForSubmission(
id: string,
next: NextFunction,
end: NextFunction
): Promise<void>;
}
export interface BasicProvider {
new (account: RemoteAccount): IBasicProvider;
}

View File

@ -0,0 +1,325 @@
import { JSDOM } from 'jsdom';
import superagent from 'superagent';
import proxy from 'superagent-proxy';
import sleep from '../utils/sleep';
import mathSum from 'math-sum';
import { IBasicProvider, RemoteAccount } from '../interface';
import { normalize, VERDICT } from '../verdict';
import Logger from '../utils/logger';
proxy(superagent);
const logger = new Logger('remote/codeforces');
const langs_map = {
C: {
name: 'GNU GCC C11 5.1.0',
id: 43,
comment: '//',
},
'C++': {
name: 'GNU G++14 6.4.0',
id: 50,
comment: '//',
},
'C++17': {
name: 'GNU G++17 7.3.0',
id: 54,
comment: '//',
},
Pascal: {
name: 'Free Pascal 3.0.2',
id: 4,
comment: '//',
},
'Python2.7': {
name: 'Python 2.7.18',
id: 7,
comment: '#',
},
Python3: {
name: 'Python 3.9.1',
id: 31,
comment: '#',
},
};
export function getAccountInfoFromEnv(): RemoteAccount | null {
const {
CODEFORCES_HANDLE,
CODEFORCES_PASSWORD,
CODEFORCES_ENDPOINT = 'https://codeforces.com',
CODEFORCES_PROXY,
} = process.env;
if (!CODEFORCES_HANDLE || !CODEFORCES_PASSWORD) return null;
const account: RemoteAccount = {
type: 'codeforces',
handle: CODEFORCES_HANDLE,
password: CODEFORCES_PASSWORD,
endpoint: CODEFORCES_ENDPOINT,
};
if (CODEFORCES_PROXY) account.proxy = CODEFORCES_PROXY;
return account;
}
function parseProblemId(id: string) {
const [, type, contestId, problemId] = id.startsWith('921')
? ['', '921', '01']
: /^(|GYM)(\d+)([A-Z]+[0-9]*)$/.exec(id);
if (type === 'GYM' && +contestId < 100000) {
return [type, (+contestId + 100000).toString(), problemId];
}
return [type, contestId, problemId];
}
export default class CodeforcesProvider implements IBasicProvider {
constructor(public account: RemoteAccount) {
if (account.cookie) this.cookie = account.cookie;
this.account.endpoint ||= 'https://codeforces.com';
}
cookie: string[] = [];
csrf: string;
get(url: string) {
logger.debug('get', url);
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
const req = superagent
.get(url)
.set('Cookie', this.cookie)
.set(
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 S2OJ/3.1.0'
);
if (this.account.proxy) return req.proxy(this.account.proxy);
return req;
}
post(url: string) {
logger.debug('post', url, this.cookie);
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
const req = superagent
.post(url)
.type('form')
.set('Cookie', this.cookie)
.set(
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 S2OJ/3.1.0'
);
if (this.account.proxy) return req.proxy(this.account.proxy);
return req;
}
getCookie(target: string) {
return this.cookie
.find(i => i.startsWith(`${target}=`))
?.split('=')[1]
?.split(';')[0];
}
setCookie(target: string, value: string) {
this.cookie = this.cookie.filter(i => !i.startsWith(`${target}=`));
this.cookie.push(`${target}=${value}`);
}
tta(_39ce7: string) {
let _tta = 0;
for (let c = 0; c < _39ce7.length; c++) {
_tta = (_tta + (c + 1) * (c + 2) * _39ce7.charCodeAt(c)) % 1009;
if (c % 3 === 0) _tta++;
if (c % 2 === 0) _tta *= 2;
if (c > 0)
_tta -=
Math.floor(_39ce7.charCodeAt(Math.floor(c / 2)) / 2) * (_tta % 5);
_tta = ((_tta % 1009) + 1009) % 1009;
}
return _tta;
}
async getCsrfToken(url: string) {
const { text: html } = await this.get(url);
const {
window: { document },
} = new JSDOM(html);
if (document.body.children.length < 2 && html.length < 512) {
throw new Error(document.body.textContent!);
}
const ftaa = this.getCookie('70a7c28f3de') || 'n/a';
const bfaa = this.getCookie('raa') || this.getCookie('bfaa') || 'n/a';
return [
(
document.querySelector('meta[name="X-Csrf-Token"]') ||
document.querySelector('input[name="csrf_token"]')
)?.getAttribute('content'),
ftaa,
bfaa,
];
}
get loggedIn() {
return this.get('/enter').then(res => {
const html = res.text;
if (html.includes('Login into Codeforces')) return false;
if (html.length < 1000 && html.includes('Redirecting...')) {
logger.debug('Got a redirect', html);
return false;
}
return true;
});
}
async ensureLogin() {
if (await this.loggedIn) return true;
logger.info('retry normal login');
const [csrf, ftaa, bfaa] = await this.getCsrfToken('/enter');
const { header } = await this.get('/enter');
if (header['set-cookie']) {
this.cookie = header['set-cookie'];
}
const res = await this.post('/enter').send({
csrf_token: csrf,
action: 'enter',
ftaa,
bfaa,
handleOrEmail: this.account.handle,
password: this.account.password,
remember: 'on',
_tta: this.tta(this.getCookie('39ce7')),
});
const cookie = res.header['set-cookie'];
if (cookie) {
this.cookie = cookie;
}
if (await this.loggedIn) {
logger.success('Logged in');
return true;
}
return false;
}
async submitProblem(
id: string,
lang: string,
code: string,
submissionId: number,
next,
end
) {
const programType = langs_map[lang] || langs_map['C++'];
const comment = programType.comment;
if (comment) {
const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`;
if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`;
else if (comment instanceof Array)
code = `${comment[0]} ${msg} ${comment[1]}\n${code}`;
}
const [type, contestId, problemId] = parseProblemId(id);
const [csrf, ftaa, bfaa] = await this.getCsrfToken(
type !== 'GYM' ? '/problemset/submit' : `/gym/${contestId}/submit`
);
logger.debug(
'Submitting',
id,
programType,
lang,
`(S2OJ Submission #${submissionId})`
);
// TODO: check submit time to ensure submission
const { text: submit } = await this.post(
`/${
type !== 'GYM' ? 'problemset' : `gym/${contestId}`
}/submit?csrf_token=${csrf}`
).send({
csrf_token: csrf,
action: 'submitSolutionFormSubmitted',
programTypeId: programType.id,
source: code,
tabsize: 4,
sourceFile: '',
ftaa,
bfaa,
_tta: this.tta(this.getCookie('39ce7')),
...(type !== 'GYM'
? {
submittedProblemCode: contestId + problemId,
sourceCodeConfirmed: true,
}
: {
submittedProblemIndex: problemId,
}),
});
const {
window: { document: statusDocument },
} = new JSDOM(submit);
const message = Array.from(statusDocument.querySelectorAll('.error'))
.map(i => i.textContent)
.join('')
.replace(/&nbsp;/g, ' ')
.trim();
if (message) {
end({ error: true, status: 'Compile Error', message });
return null;
}
const { text: status } = await this.get(
type !== 'GYM' ? '/problemset/status?my=on' : `/gym/${contestId}/my`
).retry(3);
const {
window: { document },
} = new JSDOM(status);
this.csrf = document
.querySelector('meta[name="X-Csrf-Token"]')
.getAttribute('content');
return document
.querySelector('[data-submission-id]')
.getAttribute('data-submission-id');
}
async waitForSubmission(id: string, next, end) {
let i = 1;
while (true) {
await sleep(3000);
const { body } = await this.post('/data/submitSource')
.send({
csrf_token: this.csrf,
submissionId: id,
})
.retry(3);
if (body.compilationError === 'true') {
return await end({
id,
error: 1,
status: 'Compile Error',
message: body['checkerStdoutAndStderr#1'],
});
}
const time = mathSum(
Object.keys(body)
.filter(k => k.startsWith('timeConsumed#'))
.map(k => +body[k])
);
const memory =
Math.max(
...Object.keys(body)
.filter(k => k.startsWith('memoryConsumed#'))
.map(k => +body[k])
) / 1024;
await next({ test_id: body.testCount });
if (body.waiting === 'true') continue;
const status =
VERDICT[
Object.keys(VERDICT).find(k => normalize(body.verdict).includes(k))
];
return await end({
id,
status,
score: status === 'Accepted' ? 100 : 0,
time,
memory,
});
}
}
}

View File

@ -0,0 +1,11 @@
export default function htmlspecialchars(text: string) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
};
return text.replace(/[&<>"']/g, m => map[m]);
}

View File

@ -0,0 +1,32 @@
export function normalize(key: string) {
return key.toUpperCase().replace(/ /g, '_');
}
export const VERDICT = new Proxy<Record<string, string>>(
{
RUNTIME_ERROR: 'Runtime Error',
WRONG_ANSWER: 'Wrong Answer',
OK: 'Accepted',
COMPILING: 'Compiling',
TIME_LIMIT_EXCEEDED: 'Time Limit Exceeded',
MEMORY_LIMIT_EXCEEDED: 'Memory Limit Exceeded',
IDLENESS_LIMIT_EXCEEDED: 'Idleness Limit Exceeded',
ACCEPTED: 'Accepted',
PRESENTATION_ERROR: 'Wrong Answer',
OUTPUT_LIMIT_EXCEEDED: 'Output Limit Exceeded',
EXTRA_TEST_PASSED: 'Accepted',
COMPILE_ERROR: 'Compile Error',
'RUNNING_&_JUDGING': 'Judging',
// Codeforces
'HAPPY_NEW_YEAR!': 'Accepted',
},
{
get(self, key) {
if (typeof key === 'symbol') return null;
key = normalize(key);
if (self[key]) return self[key];
return null;
},
}
);

155
remote_judger/src/vjudge.ts Normal file
View File

@ -0,0 +1,155 @@
import type { BasicProvider, IBasicProvider, RemoteAccount } from './interface';
import * as Time from './utils/time';
import Logger from './utils/logger';
import htmlspecialchars from './utils/htmlspecialchars';
const logger = new Logger('vjudge');
class AccountService {
api: IBasicProvider;
constructor(
public Provider: BasicProvider,
public account: RemoteAccount,
private request: any
) {
this.api = new Provider(account);
this.main().catch(e =>
logger.error(`Error occured in ${account.type}/${account.handle}`, e)
);
}
async judge(
id: number,
problem_id: string,
language: string,
code: string,
judge_time: string
) {
const next = async payload => {
return await this.request('/submit', {
'update-status': true,
fetch_new: false,
id,
status: `Judging Test #${payload.test_id}`,
});
};
const end = async payload => {
if (payload.error) {
return await this.request('/submit', {
submit: true,
fetch_new: false,
id,
result: JSON.stringify({
status: 'Judged',
score: 0,
error: payload.status,
details:
'<div>' +
`<info-block>ID = ${payload.id || 'None'}</info-block>` +
`<error>${htmlspecialchars(payload.message)}</error>` +
'</div>',
}),
judge_time,
});
}
return await this.request('/submit', {
submit: true,
fetch_new: false,
id,
result: JSON.stringify({
status: 'Judged',
score: payload.score,
time: payload.time,
memory: payload.memory,
details:
'<div>' +
`<info-block>ID = ${payload.id || 'None'}</info-block>` +
`<info-block>VERDICT = ${payload.status}</info-block>` +
'</div>',
}),
judge_time,
});
};
try {
const rid = await this.api.submitProblem(
problem_id,
language,
code,
id,
next,
end
);
if (!rid) return;
await this.api.waitForSubmission(rid, next, end);
} catch (e) {
logger.error(e);
await end({ error: true, message: e.message });
}
}
async login() {
const login = await this.api.ensureLogin();
if (login === true) {
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
return true;
}
logger.warn(
`${this.account.type}/${this.account.handle}: login fail`,
login || ''
);
return false;
}
async main() {
const res = await this.login();
if (!res) return;
setInterval(() => this.login(), Time.hour);
}
}
class VJudge {
private providers: Record<string, AccountService> = {};
constructor(private request: any) {}
async addProvider(type: string) {
if (this.providers[type]) throw new Error(`duplicate provider ${type}`);
const provider = await import(`./providers/${type}`);
const account = provider.getAccountInfoFromEnv();
if (!account) throw new Error(`no account info for ${type}`);
this.providers[type] = new AccountService(
provider.default,
account,
this.request
);
}
async judge(
id: number,
type: string,
problem_id: string,
language: string,
code: string,
judge_time: string
) {
if (!this.providers[type]) throw new Error(`no provider ${type}`);
this.providers[type].judge(id, problem_id, language, code, judge_time);
}
}
export async function apply(request: any) {
const vjudge = new VJudge(request);
await vjudge.addProvider('codeforces');
return vjudge;
}

View File

@ -13,9 +13,6 @@
"isolatedModules": true,
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"outDir": "./dist"
},
"include": ["**/*.ts"],

View File

@ -687,7 +687,7 @@ class JudgmentDetailsPrinter {
}
echo '<h4 class="mb-2">', $node->getAttribute("title"), ":</h4>";
}
echo '<pre>', "\n";
echo '<pre class="bg-light p-3 rounded">', "\n";
$this->_print_c($node);
echo "\n</pre>";
echo '</div>';