Continue: use Isolate for isolation

This commit is contained in:
Omar Brikaa 2024-08-24 21:01:49 +03:00
parent c8b69fe5ea
commit 63dd925bb1
2 changed files with 80 additions and 44 deletions

View File

@ -211,7 +211,7 @@ router.ws('/connect', async (ws, req) => {
job = await get_job(msg);
try {
await job.prime();
const box = await job.prime();
ws.send(
JSON.stringify({
@ -221,7 +221,7 @@ router.ws('/connect', async (ws, req) => {
})
);
await job.execute(event_bus);
await job.execute(box, event_bus);
} catch (error) {
logger.error(
`Error cleaning up job: ${job.uuid}:\n${error}`
@ -279,9 +279,9 @@ router.post('/execute', async (req, res) => {
return res.status(400).json(error);
}
try {
await job.prime();
const box = await job.prime();
let result = await job.execute();
let result = await job.execute(box);
// Backward compatibility when the run stage is not started
if (result.run === undefined) {
result.run = result.compile;

View File

@ -21,10 +21,7 @@ let job_queue = [];
const get_next_box_id = () => (box_id + 1) % MAX_BOX_ID;
class Job {
#box_id;
#metadata_file_path;
#box_dir;
#dirty_boxes;
constructor({ runtime, files, args, stdin, timeouts, memory_limits }) {
this.uuid = uuidv4();
@ -50,6 +47,34 @@ class Job {
this.memory_limits = memory_limits;
this.state = job_states.READY;
this.#dirty_boxes = [];
}
async #create_isolate_box() {
const box_id = get_next_box_id();
const metadata_file_path = `/tmp/${box_id}-metadata.txt`;
return new Promise((res, rej) => {
cp.exec(
`isolate --init --cg -b${box_id}`,
(error, stdout, stderr) => {
if (error) {
rej(
`Failed to run isolate --init: ${error.message}\nstdout: ${stdout}\nstderr: ${stderr}`
);
}
if (stdout === '') {
rej('Received empty stdout from isolate --init');
}
const box = {
id: box_id,
metadata_file_path,
dir: stdout,
};
this.#dirty_boxes.push(box);
res(box);
}
);
});
}
async prime() {
@ -62,32 +87,14 @@ class Job {
this.logger.info(`Priming job`);
remaining_job_spaces--;
this.logger.debug('Running isolate --init');
this.#box_id = get_next_box_id();
this.#metadata_file_path = `/tmp/${this.#box_id}-metadata.txt`;
await new Promise((res, rej) => {
cp.exec(
`isolate --init --cg -b${this.#box_id}`,
(error, stdout, stderr) => {
if (error) {
rej(
`Failed to run isolate --init: ${error.message}\nstdout: ${stdout}\nstderr: ${stderr}`
);
}
if (stdout === '') {
rej('Received empty stdout from isolate --init');
}
this.#box_dir = stdout;
res();
}
);
});
const box = await this.#create_isolate_box();
this.logger.debug(`Creating submission files in Isolate box`);
await fs.mkdir(path.join(this.#box_dir, 'submission'));
const submission_dir = path.join(box.dir, 'submission');
await fs.mkdir(submission_dir);
for (const file of this.files) {
const file_path = path.join(box_dir, file.name);
const rel = path.relative(box_dir, file_path);
const file_path = path.join(submission_dir, file.name);
const rel = path.relative(submission_dir, file_path);
if (rel.startsWith('..'))
throw Error(
@ -106,9 +113,10 @@ class Job {
this.state = job_states.PRIMED;
this.logger.debug('Primed job');
return box;
}
async safe_call(file, args, timeout, memory_limit, event_bus = null) {
async safe_call(box, file, args, timeout, memory_limit, event_bus = null) {
var stdout = '';
var stderr = '';
var output = '';
@ -117,8 +125,8 @@ class Job {
ISOLATE_PATH,
[
'--run',
`-b${this.#box_id}`,
`--meta=${this.#metadata_file_path}`,
`-b${box.id}`,
`--meta=${box.metadata_file_path}`,
'--cg',
'-s',
'-c',
@ -131,7 +139,7 @@ class Job {
`--time=${timeout}`,
`--extra-time=0`,
...(memory_limit >= 0 ? [`--cg-mem=${memory_limit}`] : []),
...(config.disable_networking ? [] : '--share-net'),
...(config.disable_networking ? [] : ['--share-net']),
'--',
file,
...args,
@ -213,8 +221,8 @@ class Job {
let time = null;
try {
const metadata_str = await fs.readFile(
self.metadata_file_path,
const metadata_str = await fs.read_file(
box.metadata_file_path,
'utf-8'
);
const metadata_lines = metadata_str.split('\n');
@ -230,7 +238,7 @@ class Job {
switch (key) {
case 'cg-mem':
memory =
parseInt(value) ||
parse_int(value) ||
(() => {
throw new Error(
`Failed to parse memory usage, received value: ${value}`
@ -239,7 +247,7 @@ class Job {
break;
case 'exitcode':
code =
parseInt(value) ||
parse_int(value) ||
(() => {
throw new Error(
`Failed to parse exit code, received value: ${value}`
@ -248,7 +256,7 @@ class Job {
break;
case 'exitsig':
signal =
parseInt(value) ||
parse_int(value) ||
(() => {
throw new Error(
`Failed to parse exit signal, received value: ${value}`
@ -263,7 +271,7 @@ class Job {
break;
case 'time':
time =
parseFloat(value) ||
parse_float(value) ||
(() => {
throw new Error(
`Failed to parse cpu time, received value: ${value}`
@ -276,7 +284,7 @@ class Job {
}
} catch (e) {
throw new Error(
`Error reading metadata file: ${self.metadata_file_path}\nError: ${e.message}\nIsolate run stdout: ${stdout}\nIsolate run stderr: ${stderr}`
`Error reading metadata file: ${box.metadata_file_path}\nError: ${e.message}\nIsolate run stdout: ${stdout}\nIsolate run stderr: ${stderr}`
);
}
@ -310,7 +318,7 @@ class Job {
});
}
async execute(event_bus = null) {
async execute(box, event_bus = null) {
if (this.state !== job_states.PRIMED) {
throw new Error(
'Job must be in primed state, current state: ' +
@ -352,6 +360,7 @@ class Job {
this.logger.debug('Compiling');
emit_event_bus_stage('compile');
compile = await this.safe_call(
box,
'/runtime/compile',
code_files.map(x => x.name),
this.timeouts.compile,
@ -365,8 +374,15 @@ class Job {
let run;
if (!compile_errored) {
this.logger.debug('Running');
const old_box_dir = box.dir;
const new_box = await this.#create_isolate_box();
await fs.rename(
path.join(old_box_dir, 'submission'),
path.join(new_box, 'submission')
);
emit_event_bus_stage('run');
run = await this.safe_call(
new_box,
'/runtime/run',
[code_files[0].name, ...this.args],
this.timeouts.run,
@ -389,11 +405,31 @@ class Job {
async cleanup() {
this.logger.info(`Cleaning up job`);
await fs.rm(`${this.#metadata_file_path}`);
remaining_job_spaces++;
if (job_queue.length > 0) {
job_queue.shift()();
}
await Promise.all(
this.#dirty_boxes.map(async box => {
cp.exec(
`isolate --cleanup --cg -b${box.id}`,
(error, stdout, stderr) => {
if (error) {
this.logger.error(
`Failed to run isolate --cleanup: ${error.message} on box #${box.id}\nstdout: ${stdout}\nstderr: ${stderr}`
);
}
}
);
try {
await fs.rm(box.metadata_file_path);
} catch (e) {
this.logger.error(
`Failed to remove the metadata directory of box #${box.id}. Error: ${e.message}`
);
}
})
);
}
}