mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-08 13:38:41 +00:00
feat(remote_judger): add atcoder
This commit is contained in:
parent
6510c5bc4e
commit
897c64a806
@ -20,6 +20,7 @@ export interface IBasicProvider {
|
|||||||
end: NextFunction
|
end: NextFunction
|
||||||
): Promise<string | void>;
|
): Promise<string | void>;
|
||||||
waitForSubmission(
|
waitForSubmission(
|
||||||
|
problem_id: string,
|
||||||
id: string,
|
id: string,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
end: NextFunction
|
end: NextFunction
|
||||||
|
332
remote_judger/src/providers/atcoder.ts
Normal file
332
remote_judger/src/providers/atcoder.ts
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
import superagent from 'superagent';
|
||||||
|
import proxy from 'superagent-proxy';
|
||||||
|
import sleep from '../utils/sleep';
|
||||||
|
import { IBasicProvider, RemoteAccount } from '../interface';
|
||||||
|
import Logger from '../utils/logger';
|
||||||
|
|
||||||
|
proxy(superagent);
|
||||||
|
const logger = new Logger('remote/atcoder');
|
||||||
|
|
||||||
|
const langs_map = {
|
||||||
|
C: {
|
||||||
|
name: 'C (GCC 9.2.1)',
|
||||||
|
id: 4001,
|
||||||
|
comment: '//',
|
||||||
|
},
|
||||||
|
'C++': {
|
||||||
|
name: 'C++ (GCC 9.2.1)',
|
||||||
|
id: 4003,
|
||||||
|
comment: '//',
|
||||||
|
},
|
||||||
|
Pascal: {
|
||||||
|
name: 'Pascal (FPC 3.0.4)',
|
||||||
|
id: 4041,
|
||||||
|
comment: '//',
|
||||||
|
},
|
||||||
|
Python3: {
|
||||||
|
name: 'Python (3.8.2)',
|
||||||
|
id: 4006,
|
||||||
|
comment: '#',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAccountInfoFromEnv(): RemoteAccount | null {
|
||||||
|
const {
|
||||||
|
ATCODER_HANDLE,
|
||||||
|
ATCODER_PASSWORD,
|
||||||
|
ATCODER_ENDPOINT = 'https://atcoder.jp',
|
||||||
|
ATCODER_PROXY,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
if (!ATCODER_HANDLE || !ATCODER_PASSWORD) return null;
|
||||||
|
|
||||||
|
const account: RemoteAccount = {
|
||||||
|
type: 'atcoder',
|
||||||
|
handle: ATCODER_HANDLE,
|
||||||
|
password: ATCODER_PASSWORD,
|
||||||
|
endpoint: ATCODER_ENDPOINT,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ATCODER_PROXY) account.proxy = ATCODER_PROXY;
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProblemId(id: string) {
|
||||||
|
let [, contestId, problemId] = /^(\w+)([a-z][1-9]?)$/.exec(id);
|
||||||
|
|
||||||
|
if (contestId.endsWith('_')) {
|
||||||
|
problemId = `${contestId}${problemId}`;
|
||||||
|
} else {
|
||||||
|
problemId = `${contestId}_${problemId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contestId = contestId.replace(/_/g, '');
|
||||||
|
|
||||||
|
return [contestId, problemId];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AtcoderProvider implements IBasicProvider {
|
||||||
|
constructor(public account: RemoteAccount) {
|
||||||
|
if (account.cookie) this.cookie = account.cookie;
|
||||||
|
this.account.endpoint ||= 'https://atcoder.jp';
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie: string[] = ['language=en'];
|
||||||
|
csrf: string;
|
||||||
|
|
||||||
|
get(url: string) {
|
||||||
|
logger.debug('get', url);
|
||||||
|
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
|
||||||
|
const req = superagent
|
||||||
|
.get(url)
|
||||||
|
.redirects(0)
|
||||||
|
.ok(res => res.status < 400)
|
||||||
|
.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')
|
||||||
|
.redirects(0)
|
||||||
|
.ok(res => res.status < 400)
|
||||||
|
.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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCsrfToken(url: string) {
|
||||||
|
const { text: html, header } = await this.get(url);
|
||||||
|
const {
|
||||||
|
window: { document },
|
||||||
|
} = new JSDOM(html);
|
||||||
|
|
||||||
|
if (header['set-cookie']) {
|
||||||
|
this.cookie = header['set-cookie'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body.children.length < 2 && html.length < 512) {
|
||||||
|
throw new Error(document.body.textContent!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document
|
||||||
|
.querySelector('input[name="csrf_token"]')
|
||||||
|
?.getAttribute('value');
|
||||||
|
}
|
||||||
|
|
||||||
|
get loggedIn() {
|
||||||
|
return this.get('/login').then(res => {
|
||||||
|
const html = res.text;
|
||||||
|
|
||||||
|
if (res.header['set-cookie']) {
|
||||||
|
this.cookie = res.header['set-cookie'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html.includes('<a href="/login">Sign In</a>')) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureLogin() {
|
||||||
|
if (await this.loggedIn) return true;
|
||||||
|
logger.info('retry normal login');
|
||||||
|
const csrf = await this.getCsrfToken('/login');
|
||||||
|
const res = await this.post('/login').send({
|
||||||
|
csrf_token: csrf,
|
||||||
|
username: this.account.handle,
|
||||||
|
password: this.account.password,
|
||||||
|
});
|
||||||
|
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 [contestId, problemId] = parseProblemId(id);
|
||||||
|
const csrf = await this.getCsrfToken(
|
||||||
|
`/contests/${contestId}/tasks/${problemId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
'Submitting',
|
||||||
|
id,
|
||||||
|
programType,
|
||||||
|
lang,
|
||||||
|
`(S2OJ Submission #${submissionId})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: check submit time to ensure submission
|
||||||
|
const res = await this.post(`/contests/${contestId}/submit`).send({
|
||||||
|
csrf_token: csrf,
|
||||||
|
'data.TaskScreenName': problemId,
|
||||||
|
'data.LanguageId': programType.id,
|
||||||
|
sourceCode: code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
await end({
|
||||||
|
error: true,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: 'Failed to submit code.',
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.header['set-cookie']) {
|
||||||
|
this.cookie = res.header['set-cookie'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text: status, header: status_header } = await this.get(
|
||||||
|
`/contests/${contestId}/submissions/me`
|
||||||
|
).retry(3);
|
||||||
|
|
||||||
|
if (status_header['set-cookie']) {
|
||||||
|
this.cookie = status_header['set-cookie'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
window: { document },
|
||||||
|
} = new JSDOM(status);
|
||||||
|
|
||||||
|
return document
|
||||||
|
.querySelector('.submission-score[data-id]')
|
||||||
|
.getAttribute('data-id');
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForSubmission(problem_id: string, id: string, next, end) {
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
const [contestId] = parseProblemId(problem_id);
|
||||||
|
const status_url = `/contests/${contestId}/submissions/me/status/json?reload=true&sids[]=${id}`;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (++i > 60) {
|
||||||
|
return await end({
|
||||||
|
id,
|
||||||
|
error: true,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: 'Failed to fetch submission details.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(2000);
|
||||||
|
const { body, error, header } = await this.get(status_url).retry(3);
|
||||||
|
|
||||||
|
if (header['set-cookie']) {
|
||||||
|
this.cookie = header['set-cookie'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) continue;
|
||||||
|
|
||||||
|
const result = body.Result[id];
|
||||||
|
const {
|
||||||
|
window: { document },
|
||||||
|
} = new JSDOM(`<table>${result.Html}</table>`);
|
||||||
|
|
||||||
|
const elements = document.querySelectorAll('td');
|
||||||
|
const statusTd = elements[0];
|
||||||
|
const statusElem = statusTd.querySelector('span');
|
||||||
|
|
||||||
|
if (
|
||||||
|
statusElem.title === 'Waiting for Judging' ||
|
||||||
|
statusElem.title === 'Waiting for Re-judging' ||
|
||||||
|
['WJ', 'WR'].includes(statusElem.innerHTML.trim())
|
||||||
|
) {
|
||||||
|
await next({ test_id: 0 });
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
statusElem.title === 'Judging' ||
|
||||||
|
(statusTd.colSpan == 3 && statusTd.className.includes('waiting-judge'))
|
||||||
|
) {
|
||||||
|
await next({ test_id: /(\d+)/.exec(statusElem.innerHTML)[1] || 0 });
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusElem.title === 'Compilation Error') {
|
||||||
|
return await end({
|
||||||
|
id,
|
||||||
|
error: true,
|
||||||
|
status: 'Compile Error',
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusElem.title === 'Internal Error') {
|
||||||
|
return await end({
|
||||||
|
error: true,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: 'AtCoder Internal Error.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = parseInt(elements[1].innerHTML.trim());
|
||||||
|
const memory = parseInt(elements[2].innerHTML.trim());
|
||||||
|
|
||||||
|
return await end({
|
||||||
|
id,
|
||||||
|
status: statusElem.title || 'None',
|
||||||
|
score:
|
||||||
|
statusElem.title === 'Accepted' ||
|
||||||
|
statusElem.innerHTML.trim() === 'AC'
|
||||||
|
? 100
|
||||||
|
: 0,
|
||||||
|
time,
|
||||||
|
memory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -233,7 +233,7 @@ export default class CodeforcesProvider implements IBasicProvider {
|
|||||||
`(S2OJ Submission #${submissionId})`
|
`(S2OJ Submission #${submissionId})`
|
||||||
);
|
);
|
||||||
// TODO: check submit time to ensure submission
|
// TODO: check submit time to ensure submission
|
||||||
const { text: submit } = await this.post(
|
const { text: submit, error } = await this.post(
|
||||||
`/${
|
`/${
|
||||||
type !== 'GYM' ? 'problemset' : `gym/${contestId}`
|
type !== 'GYM' ? 'problemset' : `gym/${contestId}`
|
||||||
}/submit?csrf_token=${csrf}`
|
}/submit?csrf_token=${csrf}`
|
||||||
@ -256,6 +256,17 @@ export default class CodeforcesProvider implements IBasicProvider {
|
|||||||
submittedProblemIndex: problemId,
|
submittedProblemIndex: problemId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
end({
|
||||||
|
error: true,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: 'Failed to submit code.',
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
window: { document: statusDocument },
|
window: { document: statusDocument },
|
||||||
} = new JSDOM(submit);
|
} = new JSDOM(submit);
|
||||||
@ -264,10 +275,12 @@ export default class CodeforcesProvider implements IBasicProvider {
|
|||||||
.join('')
|
.join('')
|
||||||
.replace(/ /g, ' ')
|
.replace(/ /g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
end({ error: true, status: 'Compile Error', message });
|
end({ error: true, status: 'Compile Error', message });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text: status } = await this.get(
|
const { text: status } = await this.get(
|
||||||
type !== 'GYM' ? '/problemset/status?my=on' : `/gym/${contestId}/my`
|
type !== 'GYM' ? '/problemset/status?my=on' : `/gym/${contestId}/my`
|
||||||
).retry(3);
|
).retry(3);
|
||||||
@ -282,17 +295,27 @@ export default class CodeforcesProvider implements IBasicProvider {
|
|||||||
.getAttribute('data-submission-id');
|
.getAttribute('data-submission-id');
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForSubmission(id: string, next, end) {
|
async waitForSubmission(problem_id: string, id: string, next, end) {
|
||||||
let i = 1;
|
let i = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (++i > 60) {
|
||||||
|
return await end({
|
||||||
|
id,
|
||||||
|
error: true,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: 'Failed to fetch submission details.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
const { body } = await this.post('/data/submitSource')
|
const { body, error } = await this.post('/data/submitSource')
|
||||||
.send({
|
.send({
|
||||||
csrf_token: this.csrf,
|
csrf_token: this.csrf,
|
||||||
submissionId: id,
|
submissionId: id,
|
||||||
})
|
})
|
||||||
.retry(3);
|
.retry(3);
|
||||||
|
if (error) continue;
|
||||||
if (body.compilationError === 'true') {
|
if (body.compilationError === 'true') {
|
||||||
return await end({
|
return await end({
|
||||||
id,
|
id,
|
||||||
|
@ -86,7 +86,7 @@ class AccountService {
|
|||||||
|
|
||||||
if (!rid) return;
|
if (!rid) return;
|
||||||
|
|
||||||
await this.api.waitForSubmission(rid, next, end);
|
await this.api.waitForSubmission(problem_id, rid, next, end);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
await end({ error: true, message: e.message });
|
await end({ error: true, message: e.message });
|
||||||
@ -150,6 +150,7 @@ export async function apply(request: any) {
|
|||||||
const vjudge = new VJudge(request);
|
const vjudge = new VJudge(request);
|
||||||
|
|
||||||
await vjudge.addProvider('codeforces');
|
await vjudge.addProvider('codeforces');
|
||||||
|
await vjudge.addProvider('atcoder');
|
||||||
|
|
||||||
return vjudge;
|
return vjudge;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user