mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-09-08 00:35:17 +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
|
||||
): Promise<string | void>;
|
||||
waitForSubmission(
|
||||
problem_id: string,
|
||||
id: string,
|
||||
next: 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})`
|
||||
);
|
||||
// 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}`
|
||||
}/submit?csrf_token=${csrf}`
|
||||
@ -256,6 +256,17 @@ export default class CodeforcesProvider implements IBasicProvider {
|
||||
submittedProblemIndex: problemId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
end({
|
||||
error: true,
|
||||
status: 'Judgment Failed',
|
||||
message: 'Failed to submit code.',
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
window: { document: statusDocument },
|
||||
} = new JSDOM(submit);
|
||||
@ -264,10 +275,12 @@ export default class CodeforcesProvider implements IBasicProvider {
|
||||
.join('')
|
||||
.replace(/ /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);
|
||||
@ -282,17 +295,27 @@ export default class CodeforcesProvider implements IBasicProvider {
|
||||
.getAttribute('data-submission-id');
|
||||
}
|
||||
|
||||
async waitForSubmission(id: string, next, end) {
|
||||
let i = 1;
|
||||
async waitForSubmission(problem_id: string, id: string, next, end) {
|
||||
let i = 0;
|
||||
|
||||
while (true) {
|
||||
if (++i > 60) {
|
||||
return await end({
|
||||
id,
|
||||
error: true,
|
||||
status: 'Judgment Failed',
|
||||
message: 'Failed to fetch submission details.',
|
||||
});
|
||||
}
|
||||
|
||||
await sleep(3000);
|
||||
const { body } = await this.post('/data/submitSource')
|
||||
const { body, error } = await this.post('/data/submitSource')
|
||||
.send({
|
||||
csrf_token: this.csrf,
|
||||
submissionId: id,
|
||||
})
|
||||
.retry(3);
|
||||
if (error) continue;
|
||||
if (body.compilationError === 'true') {
|
||||
return await end({
|
||||
id,
|
||||
|
@ -86,7 +86,7 @@ class AccountService {
|
||||
|
||||
if (!rid) return;
|
||||
|
||||
await this.api.waitForSubmission(rid, next, end);
|
||||
await this.api.waitForSubmission(problem_id, rid, next, end);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
await end({ error: true, message: e.message });
|
||||
@ -150,6 +150,7 @@ export async function apply(request: any) {
|
||||
const vjudge = new VJudge(request);
|
||||
|
||||
await vjudge.addProvider('codeforces');
|
||||
await vjudge.addProvider('atcoder');
|
||||
|
||||
return vjudge;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user