Port Comp_iler to Myriad (#8)
This commit is contained in:
parent
0dd7f992be
commit
c72376656c
91 changed files with 156 additions and 1140 deletions
|
@ -14,6 +14,9 @@ class AboutCommand extends Command {
|
|||
'Comp_iler is made by 1Computer.',
|
||||
'Source code is available at <https://github.com/1Computer1/comp_iler>.',
|
||||
'',
|
||||
'Comp_iler runs on Myriad, a Docker-based arbitrary code evaluation server!',
|
||||
'Check it out here <https://github.com/1Computer1/myriad>.',
|
||||
'',
|
||||
`**Guilds**: ${this.client.guilds.size}`,
|
||||
`**Channels**: ${this.client.channels.size}`,
|
||||
`**Users**: ${this.client.users.size}`,
|
||||
|
|
|
@ -11,7 +11,7 @@ class CleanupCommand extends Command {
|
|||
|
||||
async exec(message) {
|
||||
await message.util.send('Cleaning up...');
|
||||
await this.client.languageHandler.cleanup();
|
||||
await this.client.myriad.postCleanup();
|
||||
await message.util.send('Cleaned up!');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
const { Command } = require('discord-akairo');
|
||||
|
||||
class ExitCommand extends Command {
|
||||
constructor() {
|
||||
super('exit', {
|
||||
aliases: ['exit'],
|
||||
ownerOnly: true,
|
||||
clientPermissions: ['SEND_MESSAGES']
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message) {
|
||||
await message.util.send('Cleaning up...');
|
||||
await this.client.languageHandler.cleanup();
|
||||
await message.util.send('Exiting!');
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExitCommand;
|
|
@ -13,11 +13,9 @@ class HelpCommand extends Command {
|
|||
return message.util.send([
|
||||
'**Usage:**',
|
||||
`Put a \`${codePrefix}\` before a code block or inline codeblock that starts with a language code to execute it.`,
|
||||
`You can add options, separated by semicolons, after the \`${codePrefix}\`.`,
|
||||
'',
|
||||
`For list of enabled languages, use the \`${prefix}languages\` command.`,
|
||||
'',
|
||||
'See the readme for usage examples, supported languages, and options: <https://github.com/1Computer1/comp_iler>'
|
||||
'See the readme for usage examples and supported languages: <https://github.com/1Computer1/comp_iler>'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
const { Command } = require('discord-akairo');
|
||||
const Myriad = require('../struct/Myriad');
|
||||
|
||||
class LanguagesCommand extends Command {
|
||||
constructor() {
|
||||
super('languages', {
|
||||
aliases: ['languages', 'language'],
|
||||
aliases: ['languages', 'language', 'langs', 'lang'],
|
||||
clientPermissions: ['SEND_MESSAGES']
|
||||
});
|
||||
}
|
||||
|
||||
exec(message) {
|
||||
async exec(message) {
|
||||
const languages = await this.client.myriad.getLanguages();
|
||||
return message.util.send([
|
||||
'**List of enabled languages (Name: Language Codes)**:',
|
||||
...this.client.languageHandler.modules.map(lang => `${lang.name}: \`${lang.aliases.join('`, `')}\``),
|
||||
'**List of enabled languages (Language Codes)**:',
|
||||
...languages.map(lang => `\`${Myriad.Languages.get(lang).join('`, `')}\``),
|
||||
'',
|
||||
'See the readme for usage examples, supported languages, and options: <https://github.com/1Computer1/comp_iler>'
|
||||
'See the readme for usage examples: <https://github.com/1Computer1/comp_iler>'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class APL extends Language {
|
||||
constructor() {
|
||||
super('apl', {
|
||||
name: 'APL',
|
||||
aliases: ['apl']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = APL;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Bash extends Language {
|
||||
constructor() {
|
||||
super('bash', {
|
||||
name: 'Bash',
|
||||
aliases: ['bash', 'sh']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bash;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Brainfuck extends Language {
|
||||
constructor() {
|
||||
super('brainfuck', {
|
||||
name: 'Brainfuck',
|
||||
aliases: ['brainfuck', 'bf']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Brainfuck;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class C extends Language {
|
||||
constructor() {
|
||||
super('c', {
|
||||
name: 'C',
|
||||
aliases: ['c']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = C;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Clojure extends Language {
|
||||
constructor() {
|
||||
super('clojure', {
|
||||
name: 'Clojure',
|
||||
aliases: ['clojure', 'clj']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Clojure;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class CPP extends Language {
|
||||
constructor() {
|
||||
super('cpp', {
|
||||
name: 'C++',
|
||||
aliases: ['cpp', 'c++']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CPP;
|
|
@ -1,23 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class CSharp extends Language {
|
||||
constructor() {
|
||||
super('csharp', {
|
||||
name: 'C#',
|
||||
aliases: ['csharp', 'cs'],
|
||||
options: {
|
||||
e: () => ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runWith(options) {
|
||||
if (options.has('e')) {
|
||||
return { env: { EVAL_EXPR: 'true' } };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CSharp;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Elixir extends Language {
|
||||
constructor() {
|
||||
super('elixir', {
|
||||
name: 'Elixir',
|
||||
aliases: ['elixir']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Elixir;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class FSharp extends Language {
|
||||
constructor() {
|
||||
super('fsharp', {
|
||||
name: 'F#',
|
||||
aliases: ['fsharp', 'fs']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FSharp;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Go extends Language {
|
||||
constructor() {
|
||||
super('go', {
|
||||
name: 'Go',
|
||||
aliases: ['golang', 'go']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Go;
|
|
@ -1,23 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Haskell extends Language {
|
||||
constructor() {
|
||||
super('haskell', {
|
||||
name: 'Haskell',
|
||||
aliases: ['haskell', 'hs'],
|
||||
options: {
|
||||
e: () => ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runWith(options) {
|
||||
if (options.has('e')) {
|
||||
return { env: { EVAL_EXPR: 'true' } };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Haskell;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Java extends Language {
|
||||
constructor() {
|
||||
super('java', {
|
||||
name: 'Java',
|
||||
aliases: ['java']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Java;
|
|
@ -1,24 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class JavaScript extends Language {
|
||||
constructor() {
|
||||
super('javascript', {
|
||||
name: 'JavaScript',
|
||||
aliases: ['javascript', 'js'],
|
||||
options: {
|
||||
harmony: () => ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runWith(options) {
|
||||
const ret = { id: this.id, env: {} };
|
||||
if (options.has('harmony')) {
|
||||
ret.env.EVAL_HARMONY = 'true';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JavaScript;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Julia extends Language {
|
||||
constructor() {
|
||||
super('julia', {
|
||||
name: 'Julia',
|
||||
aliases: ['julia']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Julia;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Lua extends Language {
|
||||
constructor() {
|
||||
super('lua', {
|
||||
name: 'Lua',
|
||||
aliases: ['lua']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Lua;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class OCaml extends Language {
|
||||
constructor() {
|
||||
super('ocaml', {
|
||||
name: 'OCaml',
|
||||
aliases: ['ocaml']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OCaml;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Pascal extends Language {
|
||||
constructor() {
|
||||
super('pascal', {
|
||||
name: 'Pascal',
|
||||
aliases: ['pascal', 'pas', 'freepascal']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Pascal;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Perl extends Language {
|
||||
constructor() {
|
||||
super('perl', {
|
||||
name: 'Perl',
|
||||
aliases: ['perl', 'pl']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Perl;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class PHP extends Language {
|
||||
constructor() {
|
||||
super('php', {
|
||||
name: 'PHP',
|
||||
aliases: ['php']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PHP;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Prolog extends Language {
|
||||
constructor() {
|
||||
super('prolog', {
|
||||
name: 'Prolog',
|
||||
aliases: ['prolog']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Prolog;
|
|
@ -1,24 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Python extends Language {
|
||||
constructor() {
|
||||
super('python', {
|
||||
name: 'Python',
|
||||
aliases: ['python', 'py'],
|
||||
loads: ['python3', 'python2'],
|
||||
options: {
|
||||
2: () => ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runWith(options) {
|
||||
if (options.has('2')) {
|
||||
return { id: 'python2' };
|
||||
}
|
||||
|
||||
return { id: 'python3' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Python;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Racket extends Language {
|
||||
constructor() {
|
||||
super('racket', {
|
||||
name: 'Racket',
|
||||
aliases: ['lisp']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Racket;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Ruby extends Language {
|
||||
constructor() {
|
||||
super('ruby', {
|
||||
name: 'Ruby',
|
||||
aliases: ['ruby', 'rb']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Ruby;
|
|
@ -1,12 +0,0 @@
|
|||
const Language = require('../struct/Language');
|
||||
|
||||
class Rust extends Language {
|
||||
constructor() {
|
||||
super('rust', {
|
||||
name: 'Rust',
|
||||
aliases: ['rust', 'rs']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Rust;
|
|
@ -1,4 +1,5 @@
|
|||
const { Listener } = require('discord-akairo');
|
||||
const Myriad = require('../struct/Myriad');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
class MessageInvalidListener extends Listener {
|
||||
|
@ -26,95 +27,51 @@ class MessageInvalidListener extends Listener {
|
|||
reaction = await message.react('📝');
|
||||
}
|
||||
|
||||
let errored = false;
|
||||
let result;
|
||||
try {
|
||||
({ output: result, hasStderr: errored } = await this.client.languageHandler.evalCode(parse));
|
||||
} catch (e) {
|
||||
errored = true;
|
||||
result = e.message;
|
||||
}
|
||||
|
||||
result = result || '';
|
||||
const [ok, response] = await this.client.myriad.postEval(parse.language, parse.code);
|
||||
if (!message.guild || message.channel.permissionsFor(this.client.user).has('ADD_REACTIONS')) {
|
||||
if (reaction) {
|
||||
reaction.users.remove();
|
||||
}
|
||||
|
||||
if (errored) {
|
||||
message.react('✖');
|
||||
} else {
|
||||
if (ok) {
|
||||
message.react('✔');
|
||||
} else {
|
||||
message.react('✖');
|
||||
}
|
||||
}
|
||||
|
||||
const invalid = parse.invalid.length ? `Invalid options: ${parse.invalid.join(', ')}\n` : '';
|
||||
const output = `${invalid}\`\`\`\n${result}\n\`\`\``;
|
||||
const output = `\`\`\`\n${response}\n\`\`\``;
|
||||
if (output.length >= 2000) {
|
||||
const key = await fetch('https://hastebin.com/documents', { method: 'POST', body: result })
|
||||
const key = await fetch('https://hastebin.com/documents', { method: 'POST', body: response })
|
||||
.then(res => res.json())
|
||||
.then(json => json.key);
|
||||
|
||||
return message.util.send(`${invalid}Output was too long: <https://hastebin.com/${key}.js>`);
|
||||
return message.util.send(`Output was too long: <https://hastebin.com/${key}>`);
|
||||
}
|
||||
|
||||
return message.util.send(output);
|
||||
}
|
||||
|
||||
parseMessage(message) {
|
||||
const prefix = this.client.config.codePrefix.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||
const regex1 = new RegExp(`^\\s*${prefix}\\s*(.+?)?\\s*\`\`\`(.+?)\\n([^]+)\`\`\`\\s*$`);
|
||||
const regex2 = new RegExp(`^\\s*${prefix}\\s*(.+?)?\\s*\`(.+?) \\s*([^]+)\`\\s*$`);
|
||||
const match = message.content.match(regex1) || message.content.match(regex2);
|
||||
const prefix = this.client.config.codePrefix;
|
||||
const starts = message.content.slice(0, prefix.length).toLowerCase().startsWith(prefix.toLowerCase());
|
||||
if (!starts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const regex = /^\s*(`{1,3})(.+?)\n([^]+)\1\s*$/;
|
||||
const match = message.content.slice(prefix.length).match(regex);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const language = this.parseLanguage(match[2]);
|
||||
const language = Myriad.Aliases.get(match[2].toLowerCase());
|
||||
if (!language) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const code = match[3].trim();
|
||||
const [valid, invalid] = this.parseOptions(language, match[1] || '');
|
||||
return { id: message.id, language, code, options: valid, invalid };
|
||||
}
|
||||
|
||||
parseLanguage(language) {
|
||||
const match = this.client.languageHandler.findLanguage(language.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
parseOptions(language, options) {
|
||||
const kvs = options.split(';').map(opt => {
|
||||
const [k, v = ''] = opt.split('=');
|
||||
return [k.toLowerCase().trim(), v.trim()];
|
||||
});
|
||||
|
||||
const valid = new Map();
|
||||
const invalid = [];
|
||||
for (const [key, value] of kvs) {
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(language.options, key)) {
|
||||
const parse = language.options[key](value);
|
||||
if (parse != null) {
|
||||
valid.set(key, parse);
|
||||
} else {
|
||||
invalid.push(key);
|
||||
}
|
||||
} else {
|
||||
invalid.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return [valid, invalid];
|
||||
return { language, code };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const { AkairoClient, CommandHandler, ListenerHandler } = require('discord-akairo');
|
||||
const LanguageHandler = require('./LanguageHandler');
|
||||
const Myriad = require('./Myriad');
|
||||
const path = require('path');
|
||||
|
||||
class CompilerClient extends AkairoClient {
|
||||
|
@ -24,25 +24,19 @@ class CompilerClient extends AkairoClient {
|
|||
directory: path.join(__dirname, '../listeners')
|
||||
});
|
||||
|
||||
this.languageHandler = new LanguageHandler(this, {
|
||||
directory: path.join(__dirname, '../languages')
|
||||
});
|
||||
|
||||
this.myriad = new Myriad(config.myriad);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async start() {
|
||||
start() {
|
||||
this.commandHandler.useListenerHandler(this.listenerHandler);
|
||||
this.listenerHandler.setEmitters({
|
||||
commandHandler: this.commandHandler,
|
||||
listenerHandler: this.listenerHandler,
|
||||
languageHandler: this.languageHandler
|
||||
listenerHandler: this.listenerHandler
|
||||
});
|
||||
|
||||
this.commandHandler.loadAll();
|
||||
this.listenerHandler.loadAll();
|
||||
this.languageHandler.loadAll();
|
||||
await this.languageHandler.buildDocker();
|
||||
return this.login(this.config.token);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
const { AkairoModule } = require('discord-akairo');
|
||||
|
||||
class Language extends AkairoModule {
|
||||
constructor(id, {
|
||||
category,
|
||||
name,
|
||||
aliases,
|
||||
loads = [id],
|
||||
options = {}
|
||||
} = {}) {
|
||||
super(id, { category });
|
||||
|
||||
this.name = name;
|
||||
this.aliases = aliases;
|
||||
this.loads = loads;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
runWith() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Language;
|
|
@ -1,227 +0,0 @@
|
|||
const { AkairoHandler } = require('discord-akairo');
|
||||
const { Collection } = require('discord.js');
|
||||
const Language = require('./Language');
|
||||
const Queue = require('./Queue');
|
||||
const childProcess = require('child_process');
|
||||
const util = require('util');
|
||||
const path = require('path');
|
||||
|
||||
const exec = util.promisify(childProcess.exec);
|
||||
|
||||
class LanguageHandler extends AkairoHandler {
|
||||
constructor(client, {
|
||||
directory,
|
||||
classToHandle = Language,
|
||||
extensions = ['.js', '.ts'],
|
||||
automateCategories,
|
||||
loadFilter = filepath =>
|
||||
!this.client.config.languages || this.client.config.languages.includes(path.parse(filepath).name)
|
||||
}) {
|
||||
super(client, {
|
||||
directory,
|
||||
classToHandle,
|
||||
extensions,
|
||||
automateCategories,
|
||||
loadFilter
|
||||
});
|
||||
|
||||
this.aliases = new Collection();
|
||||
this.containers = new Collection();
|
||||
this.queues = new Collection();
|
||||
}
|
||||
|
||||
register(language, filepath) {
|
||||
super.register(language, filepath);
|
||||
|
||||
for (let alias of language.aliases) {
|
||||
const conflict = this.aliases.get(alias.toLowerCase());
|
||||
if (conflict) {
|
||||
throw new TypeError(`Alias conflict of ${alias} between ${language.id} and ${conflict}`);
|
||||
}
|
||||
|
||||
alias = alias.toLowerCase();
|
||||
this.aliases.set(alias, language.id);
|
||||
}
|
||||
}
|
||||
|
||||
deregister(language) {
|
||||
for (let alias of language.aliases) {
|
||||
alias = alias.toLowerCase();
|
||||
this.aliases.delete(alias);
|
||||
}
|
||||
|
||||
super.deregister(language);
|
||||
}
|
||||
|
||||
findLanguage(alias) {
|
||||
return this.modules.get(this.aliases.get(alias.toLowerCase()));
|
||||
}
|
||||
|
||||
async buildDocker() {
|
||||
if (this.client.config.parallel) {
|
||||
await Promise.all(this.modules.map(({ loads }) => Promise.all(loads.map(dockerID => this.buildImage(dockerID)))));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { loads } of this.modules.values()) {
|
||||
for (const dockerID of loads) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.buildImage(dockerID);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.client.config.cleanup > 0) {
|
||||
setInterval(() => this.cleanup().catch(() => null), this.client.config.cleanup * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async buildImage(dockerID) {
|
||||
const folder = path.join(__dirname, '../../docker', dockerID);
|
||||
await util.promisify(childProcess.exec)(`docker build -t "1computer1/comp_iler:${dockerID}" ${folder}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Built image 1computer1/comp_iler:${dockerID}.`);
|
||||
const concurrent = this.getCompilerConfig(dockerID, 'concurrent', 'number');
|
||||
this.queues.set(dockerID, { evalQueue: new Queue(concurrent), setupQueue: new Queue(1) });
|
||||
if (this.client.config.prepare) {
|
||||
await this.setupContainer(dockerID);
|
||||
}
|
||||
}
|
||||
|
||||
async setupContainer(dockerID) {
|
||||
if (this.containers.has(dockerID)) {
|
||||
return this.containers.get(dockerID);
|
||||
}
|
||||
|
||||
const cpus = this.getCompilerConfig(dockerID, 'cpus', 'string');
|
||||
const memory = this.getCompilerConfig(dockerID, 'memory', 'string');
|
||||
const name = `comp_iler-${dockerID}-${Date.now()}`;
|
||||
try {
|
||||
await exec([
|
||||
`docker run --rm --name=${name}`,
|
||||
'-u1000:1000 -w/tmp/ -dt',
|
||||
`--net=none --cpus=${cpus} -m=${memory} --memory-swap=${memory}`,
|
||||
`1computer1/comp_iler:${dockerID} /bin/sh`
|
||||
].join(' '));
|
||||
|
||||
await exec(`docker exec ${name} mkdir eval`);
|
||||
await exec(`docker exec ${name} chmod 711 eval`);
|
||||
this.containers.set(dockerID, { name });
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Started container ${name}.`);
|
||||
return this.containers.get(dockerID);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
evalCode({ language, code, options, retries = 0 }) {
|
||||
const { id: dockerID = language.id, env = {} } = language.runWith(options);
|
||||
const { evalQueue, setupQueue } = this.queues.get(dockerID);
|
||||
return evalQueue.enqueue(async () => {
|
||||
const { name } = await setupQueue.enqueue(() => this.setupContainer(dockerID));
|
||||
const now = Date.now();
|
||||
try {
|
||||
await exec(`docker exec ${name} mkdir eval/${now}`);
|
||||
await exec(`docker exec ${name} chmod 777 eval/${now}`);
|
||||
const proc = childProcess.spawn('docker', [
|
||||
'exec', '-u1001:1001', `-w/tmp/eval/${now}`,
|
||||
...Object.entries(env).map(([k, v]) => `-e${k}=${v}`),
|
||||
name, '/bin/sh', '/var/run/run.sh', code
|
||||
]);
|
||||
|
||||
const timeout = this.getCompilerConfig(dockerID, 'timeout', 'number');
|
||||
const result = await this.handleSpawn(proc, timeout);
|
||||
await exec(`docker exec ${name} rm -rf eval/${now}`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.containers.delete(dockerID);
|
||||
try {
|
||||
await this.kill(name);
|
||||
} catch (err2) {
|
||||
// The container was not alive to be killed, i.e. multiple evals were occuring,
|
||||
// one of them caused the container to be killed, the remaining evals all fail.
|
||||
// Retry those remaining ones until they work!
|
||||
if (retries < this.getCompilerConfig(dockerID, 'retries', 'number')) {
|
||||
return this.evalCode({ language, code, options, retries: retries + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleSpawn(proc, timeout = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let handled = false;
|
||||
if (timeout !== null) {
|
||||
setTimeout(() => {
|
||||
handled = true;
|
||||
reject(new Error('Timed out'));
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
let output = '';
|
||||
let hasStderr = false;
|
||||
let error;
|
||||
proc.stdout.on('data', chunk => {
|
||||
output += chunk;
|
||||
});
|
||||
|
||||
proc.stderr.on('data', chunk => {
|
||||
hasStderr = true;
|
||||
output += chunk;
|
||||
});
|
||||
|
||||
proc.on('error', e => {
|
||||
error = e;
|
||||
});
|
||||
|
||||
proc.on('close', status => {
|
||||
if (!handled) {
|
||||
handled = true;
|
||||
if (status !== 0 || error) {
|
||||
if (!error) {
|
||||
error = new Error(output || 'Something went wrong');
|
||||
}
|
||||
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ output, hasStderr });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async kill(name) {
|
||||
let cmd;
|
||||
if (process.platform === 'win32') {
|
||||
cmd = `docker kill --signal=9 ${name} >nul 2>nul`;
|
||||
} else {
|
||||
cmd = `docker kill --signal=9 ${name} >/dev/null 2>/dev/null`;
|
||||
}
|
||||
|
||||
await exec(cmd);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Killed container ${name}.`);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
return Promise.all(this.containers.map(({ name }, dockerID) => {
|
||||
this.containers.delete(dockerID);
|
||||
return this.kill(name);
|
||||
}));
|
||||
}
|
||||
|
||||
getCompilerConfig(dockerID, key, type) {
|
||||
const o = this.client.config[key];
|
||||
return typeof o === type
|
||||
? o
|
||||
: o[dockerID] !== undefined
|
||||
? o[dockerID]
|
||||
: o.default;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LanguageHandler;
|
87
src/struct/Myriad.js
Normal file
87
src/struct/Myriad.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
const fetch = require('node-fetch');
|
||||
|
||||
class Myriad {
|
||||
constructor(port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
url(k) {
|
||||
return `http://localhost:${this.port}/${k}`;
|
||||
}
|
||||
|
||||
getLanguages() {
|
||||
return fetch(this.url('languages'), { method: 'GET' }).then(x => x.json());
|
||||
}
|
||||
|
||||
async postEval(language, code) {
|
||||
const response = await fetch(this.url('eval'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ language, code }),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404 && /^Language .+? was not found$/.test(response.statusText)) {
|
||||
return [false, 'Invalid language'];
|
||||
}
|
||||
|
||||
if (response.status === 500 && response.statusText === 'Evaluation failed') {
|
||||
return [false, 'Evaluation failed'];
|
||||
}
|
||||
|
||||
if (response.status === 504 && response.statusText === 'Evaluation timed out') {
|
||||
return [false, 'Evaluation timed out'];
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected ${response.status} response from ['Myriad', ${response.statusText}`);
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
return [true, body.result || '\n'];
|
||||
}
|
||||
|
||||
getContainers() {
|
||||
return fetch(this.url('containers'), { method: 'GET' }).then(x => x.json());
|
||||
}
|
||||
|
||||
postCleanup() {
|
||||
return fetch(this.url('cleanup'), { method: 'POST' }).then(x => x.json());
|
||||
}
|
||||
}
|
||||
|
||||
const entries = [
|
||||
['apl', ['apl']],
|
||||
['bash', ['bash', 'sh']],
|
||||
['brainfuck', ['brainfuck', 'bf']],
|
||||
['c', ['c']],
|
||||
['clojure', ['clojure', 'clj']],
|
||||
['cpp', ['cpp']],
|
||||
['csharp', ['csharp', 'cs']],
|
||||
['elixir', ['elixir']],
|
||||
['fsharp', ['fsharp', 'fs']],
|
||||
['go', ['golang', 'go']],
|
||||
['haskell', ['haskell', 'hs']],
|
||||
['java', ['java']],
|
||||
['javascript', ['javascript', 'js']],
|
||||
['julia', ['julia']],
|
||||
['lua', ['lua']],
|
||||
['ocaml', ['ocaml', 'ml']],
|
||||
['pascal', ['pascal', 'pas', 'freepascal']],
|
||||
['perl', ['perl', 'pl']],
|
||||
['php', ['php']],
|
||||
['prolog', ['prolog']],
|
||||
['python', ['python', 'py']],
|
||||
['racket', ['lisp']],
|
||||
['ruby', ['ruby', 'rb']],
|
||||
['rust', ['rust', 'rs']]
|
||||
];
|
||||
|
||||
Myriad.Languages = new Map(entries);
|
||||
Myriad.Aliases = new Map();
|
||||
for (const [l, xs] of entries) {
|
||||
for (const x of xs) {
|
||||
Myriad.Aliases.set(x, l);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Myriad;
|
|
@ -1,38 +0,0 @@
|
|||
class Queue {
|
||||
constructor(limit) {
|
||||
this.limit = limit;
|
||||
this.tasks = [];
|
||||
this.ongoing = 0;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.tasks.length;
|
||||
}
|
||||
|
||||
enqueue(task) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.tasks.push({ task, resolve, reject });
|
||||
if (this.ongoing < this.limit) {
|
||||
this.process();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async process() {
|
||||
this.ongoing++;
|
||||
const { task, resolve, reject } = this.tasks.shift();
|
||||
try {
|
||||
const x = await task();
|
||||
resolve(x);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
|
||||
this.ongoing--;
|
||||
while (this.ongoing < this.limit && this.tasks.length > 0) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Queue;
|
Loading…
Add table
Add a link
Reference in a new issue