From ce4fdc1523b24341b649cad60cbd83cdc1f0c421 Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Tue, 21 Mar 2023 19:14:43 +0800 Subject: [PATCH] feat: luogu remote judger --- .gitignore | 1 + luogu_remote_judger/.prettierrc | 14 + luogu_remote_judger/add_judger.sql | 2 + luogu_remote_judger/daemon.js | 181 ++++++++ luogu_remote_judger/entrypoint.js | 16 + luogu_remote_judger/luogu.js | 377 +++++++++++++++++ luogu_remote_judger/package-lock.json | 388 ++++++++++++++++++ luogu_remote_judger/package.json | 25 ++ luogu_remote_judger/utils/htmlspecialchars.js | 11 + luogu_remote_judger/utils/logger.js | 11 + luogu_remote_judger/utils/sleep.js | 5 + luogu_remote_judger/utils/time.js | 5 + 12 files changed, 1036 insertions(+) create mode 100644 .gitignore create mode 100644 luogu_remote_judger/.prettierrc create mode 100644 luogu_remote_judger/add_judger.sql create mode 100644 luogu_remote_judger/daemon.js create mode 100644 luogu_remote_judger/entrypoint.js create mode 100644 luogu_remote_judger/luogu.js create mode 100644 luogu_remote_judger/package-lock.json create mode 100644 luogu_remote_judger/package.json create mode 100644 luogu_remote_judger/utils/htmlspecialchars.js create mode 100644 luogu_remote_judger/utils/logger.js create mode 100644 luogu_remote_judger/utils/sleep.js create mode 100644 luogu_remote_judger/utils/time.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/luogu_remote_judger/.prettierrc b/luogu_remote_judger/.prettierrc new file mode 100644 index 0000000..ea645c8 --- /dev/null +++ b/luogu_remote_judger/.prettierrc @@ -0,0 +1,14 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "endOfLine": "lf", + "jsxSingleQuote": false, + "printWidth": 120, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "organizeImportsSkipDestructiveCodeActions": true +} diff --git a/luogu_remote_judger/add_judger.sql b/luogu_remote_judger/add_judger.sql new file mode 100644 index 0000000..900309c --- /dev/null +++ b/luogu_remote_judger/add_judger.sql @@ -0,0 +1,2 @@ +USE `app_uoj233`; +insert into judger_info (judger_name, password, ip) values ('luogu_remote_judger', '_judger_password_', 's2oj-luogu-remote-judger'); diff --git a/luogu_remote_judger/daemon.js b/luogu_remote_judger/daemon.js new file mode 100644 index 0000000..b3e5f12 --- /dev/null +++ b/luogu_remote_judger/daemon.js @@ -0,0 +1,181 @@ +import fs from 'fs-extra'; +import superagent from 'superagent'; +import path from 'node:path'; +import child from 'node:child_process'; + +import Luogu, { getAccountInfoFromEnv } from './luogu.js'; +import Logger from './utils/logger.js'; +import sleep from './utils/sleep.js'; +import * as TIME from './utils/time.js'; +import htmlspecialchars from './utils/htmlspecialchars.js'; + +const logger = new Logger('daemon'); + +async function daemon(config) { + function request(url, data) { + const req_url = `${config.server_url}/judge${url}`; + + logger.debug('request', req_url, data); + + return superagent + .post(req_url) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send( + Object.entries({ + judger_name: config.judger_name, + password: config.password, + ...data, + }) + .map(([k, v]) => `${k}=${encodeURIComponent(typeof v === 'string' ? v : JSON.stringify(v))}`) + .join('&') + ); + } + + const luogu = new Luogu(getAccountInfoFromEnv()); + + logger.info('Daemon started.'); + + while (true) { + try { + await sleep(TIME.second); + + const { text, error } = await request('/submit'); + + if (error) { + logger.error('/submit', error.message); + + continue; + } + + if (text.startsWith('Nothing to judge')) { + logger.debug('Nothing to judge.'); + + continue; + } + + const data = JSON.parse(text); + const { id, content, judge_time } = data; + const config = Object.fromEntries(content.config); + const tmpdir = `/tmp/s2oj_rmj/${id}/`; + + if (config.test_sample_only === 'on') { + await request('/submit', { + submit: 1, + fetch_new: 0, + id, + result: JSON.stringify({ + status: 'Judged', + score: 100, + time: 0, + memory: 0, + details: 'Sample test is not available.', + }), + judge_time, + }); + + continue; + } + + fs.ensureDirSync(tmpdir); + + let code = ''; + + try { + // ========================= + // Download source code + // ========================= + + logger.debug('Downloading source code for ' + id); + + const zipFilePath = path.resolve(tmpdir, 'all.zip'); + const res = request(`/download${content.file_name}`); + const stream = fs.createWriteStream(zipFilePath); + + res.pipe(stream); + + await new Promise((resolve, reject) => { + stream.on('finish', resolve); + stream.on('error', reject); + }); + + // ========================= + // Extract source code + // ========================= + + logger.debug('Extracting source code for ' + id); + + const extractedPath = path.resolve(tmpdir, 'all'); + + await new Promise((resolve, reject) => { + child.exec(`unzip ${zipFilePath} -d ${extractedPath}`, e => { + if (e) reject(e); + else resolve(true); + }); + }); + + // ========================= + // Read source code + // ========================= + logger.debug('Reading source code.', id); + + const sourceCodePath = path.resolve(extractedPath, 'answer.code'); + + code = fs.readFileSync(sourceCodePath, 'utf-8'); + } catch (e) { + await request('/submit', { + submit: 1, + fetch_new: 0, + id, + result: JSON.stringify({ + status: 'Judged', + score: 0, + error: 'Judgement Failed', + details: `Failed to download and extract source code.`, + }), + judge_time, + }); + + logger.error('Failed to download and extract source code.', id, e.message); + + fs.removeSync(tmpdir); + + continue; + } + + // ========================= + // Start judging + // ========================= + + logger.info('Start judging', id, `(problem ${data.problem_id})`); + + try { + await luogu.judge(id, config.luogu_pid, config.answer_language, code, judge_time, config, request); + } catch (err) { + await request('/submit', { + submit: 1, + fetch_new: 0, + id, + result: JSON.stringify({ + status: 'Judged', + score: 0, + error: 'Judgement Failed', + details: `${htmlspecialchars(err.message)}`, + }), + judge_time, + }); + + logger.error('Judgement Failed.', id, err.message); + + fs.removeSync(tmpdir); + + continue; + } + + fs.removeSync(tmpdir); + } catch (err) { + logger.error(err.message); + } + } +} + +export default daemon; diff --git a/luogu_remote_judger/entrypoint.js b/luogu_remote_judger/entrypoint.js new file mode 100644 index 0000000..fad17ca --- /dev/null +++ b/luogu_remote_judger/entrypoint.js @@ -0,0 +1,16 @@ +import daemon from './daemon.js'; + +const { + UOJ_PROTOCOL = 'http', + UOJ_HOST = 'uoj-web', + UOJ_JUDGER_NAME = 'luogu_remote_judger', + UOJ_JUDGER_PASSWORD = '', +} = process.env; + +const UOJ_BASEURL = `${UOJ_PROTOCOL}://${UOJ_HOST}`; + +daemon({ + server_url: UOJ_BASEURL, + judger_name: UOJ_JUDGER_NAME, + password: UOJ_JUDGER_PASSWORD, +}); diff --git a/luogu_remote_judger/luogu.js b/luogu_remote_judger/luogu.js new file mode 100644 index 0000000..4582aa0 --- /dev/null +++ b/luogu_remote_judger/luogu.js @@ -0,0 +1,377 @@ +import superagent from 'superagent'; + +import Logger from './utils/logger.js'; +import sleep from './utils/sleep.js'; +import htmlspecialchars from './utils/htmlspecialchars.js'; + +const logger = new Logger('remote/luogu'); + +const USER_AGENT = 'UniversalOJ/1.0 UOJ-Luogu-RemoteJudge/1.0 ( https://github.com/renbaoshuo/UOJ-Luogu-RemoteJudge )'; +const HTTP_ERROR_MAP = { + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', +}; +const STATUS_MAP = [ + 'Waiting', + 'Judging', + 'Compile Error', + 'Output Limit Exceeded', + 'Memory Limit Exceeded', + 'Time Limit Exceeded', + 'Wrong Answer', + 'Runtime Error', + 0, + 0, + 0, + 'Judgment Failed', + 'Accepted', + 0, + 'Wrong Answer', // WA +]; +const LANGS_MAP = { + C: { + id: 'c/99/gcc', + name: 'C', + comment: '//', + }, + 'C++': { + id: 'cxx/98/gcc', + name: 'C++ 98', + comment: '//', + }, + 'C++11': { + id: 'cxx/11/gcc', + name: 'C++ 11', + comment: '//', + }, + Python3: { + id: 'python3/c', + name: 'Python 3', + comment: '#', + }, + Java8: { + id: 'java/8', + name: 'Java 8', + comment: '//', + }, + Pascal: { + id: 'pascal/fpc', + name: 'Pascal', + comment: '//', + }, +}; + +function buildLuoguTestCaseInfoBlock(test) { + const attrs = [ + ['num', test.id], + ['info', STATUS_MAP[test.status]], + ['time', test.time || -1], + ['memory', test.memory || -1], + ['score', test.score || ''], + ] + .map(o => `${o[0]}="${o[1]}"`) + .join(' '); + const desc = htmlspecialchars(test.description || ''); + + return `${desc}`; +} + +export default class Luogu { + account; + + constructor(account) { + if (!account) { + throw new Error('No account info provided'); + } + + this.account = account; + } + + get(url) { + if (!url.includes('//')) { + url = `${this.account.endpoint || 'https://open-v1.lgapi.cn'}${url}`; + } + + logger.debug('get', url, this.cookie); + + const req = superagent + .get(url) + .set('User-Agent', USER_AGENT) + .auth(this.account.username, this.account.password) + .set('X-Requested-With', 'S2OJ Remote Judge (OpenSource Version)'); + + return req; + } + + post(url) { + if (!url.includes('//')) { + url = `${this.account.endpoint || 'https://open-v1.lgapi.cn'}${url}`; + } + + logger.debug('post', url); + + const req = superagent + .post(url) + .set('User-Agent', USER_AGENT) + .auth(this.account.username, this.account.password) + .set('X-Requested-With', 'S2OJ Remote Judge (OpenSource Version)'); + + return req; + } + + async submitProblem(id, lang, code, submissionId, next, end) { + if (code.length < 10) { + await end({ + error: true, + status: 'Compile Error', + message: 'Code too short', + }); + + return null; + } + + 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 result = await this.post('/judge/problem') + .send({ + pid: id, + code, + lang: programType.id, + o2: 1, + trackId: submissionId, + }) + .ok(status => true); + + if (HTTP_ERROR_MAP[result.status]) { + await end({ + error: true, + status: 'Judgment Failed', + message: `[Luogu] ${HTTP_ERROR_MAP[result.status]}.`, + }); + + logger.error('submitProblem', result.status, HTTP_ERROR_MAP[result.status]); + + return null; + } + + return result.body.requestId; + } + + async waitForSubmission(id, next, end) { + let fail = 0; + let count = 0; + + while (count < 360 && fail < 60) { + await sleep(500); + + count++; + + try { + const result = await this.get(`/judge/result?id=${id}`); + + if (HTTP_ERROR_MAP[result.status]) { + await end({ + error: true, + status: 'Judgment Failed', + message: `[Luogu] ${HTTP_ERROR_MAP[result.status]}.`, + }); + + logger.error('submitProblem', result.status, HTTP_ERROR_MAP[result.status]); + + return null; + } + + const data = result.body.data; + + if (result.status == 204) { + await next({ status: '[Luogu] Judging' }); + continue; + } + + if (result.status == 200 && !data) { + return await end({ + error: true, + id, + status: 'Judgment Failed', + message: 'Failed to fetch submission details.', + }); + } + + if (data.compile && data.compile.success === false) { + return await end({ + error: true, + id, + status: 'Compile Error', + message: data.compile.message, + }); + } + + if (!data.judge?.subtasks) continue; + + const finishedTestCases = Object.entries(data.judge.subtasks) + .map(o => o[1]) + .reduce( + (acc, sub) => + acc + + Object.entries(sub.cases) + .map(o => o[1]) + .filter(test => test.status >= 2).length, + 0 + ); + + await next({ + status: `[Luogu] Judging (${finishedTestCases} judged)`, + }); + + if (data.status < 2) continue; + + logger.info('RecordID:', id, 'done'); + + let details = ''; + + details += ''; + + if (data.judge.subtasks.length === 1) { + details += Object.entries(data.judge.subtasks[0].cases) + .map(o => o[1]) + .map(buildLuoguTestCaseInfoBlock) + .join('\n'); + } else { + details += Object.entries(data.judge.subtasks) + .map(o => o[1]) + .map( + (subtask, index) => + `${Object.entries(subtask.cases) + .map(o => o[1]) + .map(buildLuoguTestCaseInfoBlock) + .join('\n')}` + ) + .join('\n'); + } + + details += ''; + + return await end({ + id, + status: STATUS_MAP[data.judge.status], + score: + STATUS_MAP[data.judge.status] === 'Accepted' + ? 100 + : // Workaround for UOJ feature + Math.min(97, data.judge.score), + time: data.judge.time, + memory: data.judge.memory, + details, + }); + } catch (e) { + logger.error(e); + + fail++; + } + } + return await end({ + error: true, + id, + status: 'Judgment Failed', + message: 'Failed to fetch submission details.', + }); + } + + async judge(id, pid, lang, code, judge_time, config, request) { + const next = payload => + request('/submit', { + 'update-status': 1, + fetch_new: 0, + id, + status: payload.status || (payload.test_id ? `Judging Test #${payload.test_id}` : 'Judging'), + }); + + const end = payload => { + if (payload.error) { + return request('/submit', { + submit: 1, + fetch_new: 0, + id, + result: JSON.stringify({ + status: 'Judged', + score: 0, + error: payload.status, + details: + payload.details || + '
' + + `ID = ${payload.id || 'None'}` + + `${htmlspecialchars(payload.message)}` + + '
', + }), + judge_time, + }); + } + + return request('/submit', { + submit: 1, + fetch_new: 0, + id, + result: JSON.stringify({ + status: 'Judged', + score: payload.score, + time: payload.time, + memory: payload.memory, + details: + payload.details || + '
' + + `REMOTE_SUBMISSION_ID = ${payload.id || 'None'}\nVERDICT = ${payload.status}` + + '
', + }), + judge_time, + }); + }; + + try { + const rid = await this.submitProblem(pid, lang, code, id, next, end); + + if (!rid) return; + + await this.waitForSubmission(rid, next, end); + } catch (e) { + logger.error(e); + + await end({ + error: true, + status: 'Judgment Failed', + message: e.message, + }); + } + } +} + +export function getAccountInfoFromEnv() { + const { LUOGU_API_USERNAME, LUOGU_API_PASSWORD, LUOGU_API_ENDPOINT = 'https://open-v1.lgapi.cn' } = process.env; + + if (!LUOGU_API_USERNAME || !LUOGU_API_PASSWORD) return null; + + const account = { + type: 'luogu-api', + username: LUOGU_API_USERNAME, + password: LUOGU_API_PASSWORD, + endpoint: LUOGU_API_ENDPOINT, + }; + + return account; +} diff --git a/luogu_remote_judger/package-lock.json b/luogu_remote_judger/package-lock.json new file mode 100644 index 0000000..079a9d7 --- /dev/null +++ b/luogu_remote_judger/package-lock.json @@ -0,0 +1,388 @@ +{ + "name": "s2oj-luogu-remote-judger", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "s2oj-luogu-remote-judger", + "version": "0.0.0", + "dependencies": { + "fs-extra": "^11.1.1", + "reggol": "^1.3.5", + "superagent": "^8.0.9" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "node_modules/cosmokit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cosmokit/-/cosmokit-1.4.1.tgz", + "integrity": "sha512-d3ZRpKFahJRvLbo1T4y0ELCudjk9AeDUsfgKm+iAti6yPCeoPLGNUGT4expTWsNkrSA1uk7CKhtBPiizFYvDgA==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsonfile": { + "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" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/qs": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reggol": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/reggol/-/reggol-1.3.5.tgz", + "integrity": "sha512-kzkzs4nhZeiphyh+amekq25/3PndZDq+5Yt8qCJqPSyMXPC1pkwhfYCQyJdXxoRz3/uqt0+VqHulagUCVY84vA==", + "dependencies": { + "cosmokit": "^1.4.0", + "object-inspect": "^1.12.2", + "supports-color": "^8.1.1" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/superagent": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", + "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/luogu_remote_judger/package.json b/luogu_remote_judger/package.json new file mode 100644 index 0000000..a1a253f --- /dev/null +++ b/luogu_remote_judger/package.json @@ -0,0 +1,25 @@ +{ + "name": "s2oj-luogu-remote-judger", + "version": "0.0.0", + "description": "Luogu remote judger for S2OJ.", + "private": true, + "main": "entrypoint.js", + "type": "module", + "scripts": { + "start": "node entrypoint.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/renbaoshuo/S2OJ.git" + }, + "author": "Baoshuo ", + "bugs": { + "url": "https://github.com/renbaoshuo/S2OJ/issues" + }, + "homepage": "https://github.com/renbaoshuo/S2OJ#readme", + "dependencies": { + "fs-extra": "^11.1.1", + "reggol": "^1.3.5", + "superagent": "^8.0.9" + } +} diff --git a/luogu_remote_judger/utils/htmlspecialchars.js b/luogu_remote_judger/utils/htmlspecialchars.js new file mode 100644 index 0000000..9eb3556 --- /dev/null +++ b/luogu_remote_judger/utils/htmlspecialchars.js @@ -0,0 +1,11 @@ +export default function htmlspecialchars(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + + return text.replace(/[&<>"']/g, m => map[m]); +} diff --git a/luogu_remote_judger/utils/logger.js b/luogu_remote_judger/utils/logger.js new file mode 100644 index 0000000..e71209b --- /dev/null +++ b/luogu_remote_judger/utils/logger.js @@ -0,0 +1,11 @@ +import Logger from 'reggol'; + +Logger.levels.base = process.env.DEV ? 3 : 2; +Logger.targets[0].showTime = 'dd hh:mm:ss'; +Logger.targets[0].label = { + align: 'right', + width: 9, + margin: 1, +}; + +export default Logger; diff --git a/luogu_remote_judger/utils/sleep.js b/luogu_remote_judger/utils/sleep.js new file mode 100644 index 0000000..ba33476 --- /dev/null +++ b/luogu_remote_judger/utils/sleep.js @@ -0,0 +1,5 @@ +export default function sleep(timeout) { + return new Promise(resolve => { + setTimeout(() => resolve(true), timeout); + }); +} diff --git a/luogu_remote_judger/utils/time.js b/luogu_remote_judger/utils/time.js new file mode 100644 index 0000000..2a4fcd7 --- /dev/null +++ b/luogu_remote_judger/utils/time.js @@ -0,0 +1,5 @@ +export const second = 1000; +export const minute = second * 60; +export const hour = minute * 60; +export const day = hour * 24; +export const week = day * 7;