pytron-web/server.js

415 lines
13 KiB
JavaScript

// Requires
const fs = require('fs');
const process = require('process');
const pathtools = require('path');
const express = require('express');
const fileUpload = require('express-fileupload');
const { spawn } = require('child_process');
const pug = require('pug');
const unzip = require('unzip');
const touch = require('touch');
const bcrypt = require('bcryptjs');
const useragent = require('express-useragent');
const { port, pythonPath, aisPath, aisPathOld, hashPath } = require('./config');
// Create the directories to store the files
try { fs.mkdirSync(aisPath); } catch { }
try { fs.mkdirSync(aisPathOld); } catch { }
let pythonRunning = false;
let pythonShouldRun = false;
function runPythonTotal() {
let p = spawn(pythonPath, ['run.py'], {cwd: 'pytron_run'});
p.stdout.on('data', (data) => {
let content = data.toString().split('\n');
for (let line of content) {
console.log('run.py: ' + line);
}
});
p.stderr.on('data', (data) => {
let content = data.toString().split('\n');
for (let line of content) {
console.log('run.py: ' + line);
}
});
p.on('close', (code) => {
process.stdout.write('Python finished executing');
});
}
function runPython() {
process.stdout.write('Requested to run python');
if (pythonRunning) {
process.stdout.write(' buy python is already running');
pythonShouldRun = true;
} else {
pythonRunning = true;
}
process.stdout.write('\n');
let p = spawn(pythonPath, ['refresh.py'], {cwd: 'pytron_run'});
p.stdout.on('data', (data) => {
let content = data.toString().split('\n');
for (let line of content) {
console.log('run.py: ' + line);
}
});
p.stderr.on('data', (data) => {
let content = data.toString().split('\n');
for (let line of content) {
console.log('run.py: ' + line);
}
});
p.on('close', (code) => {
process.stdout.write('Python finished executing');
if (pythonShouldRun) {
process.stdout.write(' but another request arrive, so we will relaunch it\n');
pythonShouldRun = false;
pythonRunning = false;
runPython();
} else {
process.stdout.write('\n');
pythonRunning = false;
}
});
}
function parse(data) {
let content = JSON.parse(data);
let parsed = {ais: [], battles: {}};
for (let key in content) {
let battlers = key.split('/');
let ai1 = battlers[0];
let ai2 = battlers[1];
parsed.battles[ai1 + '/' + ai2] = content[key][0];
parsed.battles[ai2 + '/' + ai1] = content[key][1];
let realAi1 = parsed.ais.find((x) => x.name == ai1);
if (realAi1 === undefined) {
realAi1 = {
name: ai1,
victories: 0,
defeats: 0,
nulls: 0,
score: 0,
};
parsed.ais.push(realAi1);
}
let realAi2 = parsed.ais.find((x) => x.name == ai2);
if (realAi2 === undefined) {
realAi2 = {
name: ai2,
victories: 0,
defeats: 0,
nulls: 0,
score: 0,
};
parsed.ais.push(realAi2);
}
realAi1.victories += content[key][0];
realAi1.defeats += content[key][1];
realAi2.victories += content[key][1];
realAi2.defeats += content[key][0];
realAi1.nulls += content[key][2];
realAi2.nulls += content[key][2];
}
parsed.sortedAis = parsed.ais.slice(0);
for (let ai of parsed.sortedAis) {
ai.score = ai.victories * 3 + ai.nulls;
}
parsed.sortedAis.sort((a, b) => b.score - a.score);
if (parsed.sortedAis.length > 0) parsed.sortedAis[0].rank = 1;
if (parsed.sortedAis.length > 1) parsed.sortedAis[1].rank = 2;
if (parsed.sortedAis.length > 2) parsed.sortedAis[2].rank = 3;
return parsed;
}
function readData(req, res, callback) {
fs.readFile('pytron_run/assets/data.json', 'utf-8', (err, data) => {
if (err != null) {
console.log(err);
res.status(400);
render(req, res, 'error', {message: 'It seems like the site is broken :\'('});
return;
}
let parsed = parse(data);
callback(parsed);
});
}
function saveArchiveAndRun(req, res) {
console.log("Saving archive");
let path = pathtools.join(aisPath, req.body.name);
try { fs.mkdirSync(path); } catch { }
let zipfile = pathtools.join(path, 'archive.zip');
req.files.archive.mv(zipfile, (err) => {
if (err != null) {
console.log("Failed to save archive");
console.log(err);
res.status(400);
render(req, res, 'error', {message: 'Unable to save the ZIP archive to the server'});
return;
}
fs.createReadStream(zipfile)
.pipe(unzip.Extract({path}))
.on('close', () => {
// Touch __init__.py
touch(pathtools.join(path, '__init__.py'), () => {
// Trigger python_run
runPython();
if (req.useragent.isCurl) {
res.send('Success');
} else {
res.redirect('/');
}
});
})
.on('error', () => {
render(req, res, 'error', {message: 'Failed to unzip the archive'});
});
});
}
function render(req, res, template, context) {
if (req.useragent.isCurl) {
res.render("txt/" + template + "_txt", context);
} else {
res.render("html/" + template + "_html", context);
}
}
function startServer() {
const app = express();
app.set('view engine', 'pug');
app.use(fileUpload());
app.use(useragent.express());
app.get('/', (req, res) => {
readData(req, res, (parsed) => {
render(req, res, 'index', {
data: parsed,
running: pythonRunning,
});
});
});
app.get('/is-running', (req, res) => {
if (pythonRunning) {
res.send('yes');
} else {
res.send('no');
}
});
app.get('/leaderboard', (req, res) => {
readData(req, res, (parsed) => {
render(req, res, 'leaderboard', {
data: parsed,
running: pythonRunning,
});
});
});
app.get('/battles', (req, res) => {
readData(req, res, (parsed) => {
render(req, res, 'battles', {
data: parsed,
running: pythonRunning,
});
});
});
app.get('/upload', (req, res) => {
render(req, res, 'upload', {});
});
app.post('/upload-target', (req, res) => {
console.log("/upload-target");
if (!req.body) {
res.status(400);
render(req, res, 'error', {message: "You have to send the form"});
}
if (!req.body.name) {
res.status(400);
render(req, res, 'error', {message: "You have to enter a name in the form"});
return;
}
if (!req.body.password) {
res.status(400);
render(req, res, 'error', {message: "You have to enter a password in the form"});
}
if (req.body.name.indexOf('.') !== -1) {
res.status(400);
render(req, res, 'error', {message: "The name of your AI can't contain dots"});
}
if (!req.files.archive) {
res.status(400);
render(req, res, 'error', {message: "You have to send a ZIP archive"});
return;
}
let path = pathtools.join(aisPath, req.body.name);
let aiExisted;
try {
fs.statSync(path).isDirectory();
aiExisted = true;
} catch (e) {
aiExisted = false;
fs.mkdirSync(path);
}
if (aiExisted) {
console.log("Ai existed, try to verify password");
fs.readFile(pathtools.join(path, hashPath), 'utf-8', function(err, data) {
if (err != null) {
res.status(400);
render(req, res, 'error', {message: "Couldn't read hashed password"});
console.log(err);
return;
}
// If the AI already existed, verify the password
bcrypt.compare(req.body.password, data, function(err, success) {
if (err != null) {
res.status(400);
render(req, res, 'error', {message: "Couldn't compare password"});
return;
}
if (!success) {
console.log("Authentication failed");
res.status(400);
render(req, res, 'error', {message: "Authentication failed"});
return;
}
console.log("Authentication complete");
try {
if (fs.statSync(path).isDirectory()) {
// Move it to old
let version = 0;
for(;;) {
let movePath = pathtools.join(aisPathOld, req.body.name + '.' + version);
try {
fs.statSync(movePath);
// If the sync succeded, it means that the directory already exists
version++;
} catch {
// If we're here, it means that we found the right path. We
// will move the old one to the old directories, and save
// the new one
fs.rename(path, movePath, () => {
console.log("copying from " +
pathtools.join(movePath, "__bcrypt__hash.txt") +
" to " +
pathtools.join(path, "__bcrypt__hash.txt"));
// Don't forget to copy the hashed password.
fs.copyFileSync(
pathtools.join(movePath, "__bcrypt__hash.txt"),
pathtools.join(path, "__bcrypt__hash.txt"));
});
break;
}
}
}
} catch {
// Nothing to do here
}
// Save archive and run stuff
saveArchiveAndRun(req, res);
});
});
} else {
console.log("New user, hashing password");
bcrypt.hash(req.body.password, 10, function(err, hash) {
if (err != null) {
console.log(err);
res.status(400);
render(req, res, 'error', {message: "Couldn't hash password"});
return;
}
console.log("Storing password");
// Store hash in your password DB.
fs.writeFile(pathtools.join(path, hashPath), hash, (err) => {
if (err != null) {
console.log(err);
res.status(400);
render(req, res, 'error', {message: "Couldn't save hashed password"});
return;
}
// Save archive and run stuff
saveArchiveAndRun(req, res);
});
});
}
});
app.use('/static', express.static('static'));
app.listen(port, () => {
console.log(`Server listening on port ${port}!`)
})
}
function main() {
switch (process.argv[2]) {
case 'start':
startServer();
break;
case 'refresh':
runPython();
break;
case 'recompute':
runPythonTotal();
break;
default:
console.log('Unknown option: ' + process.argv[2]);
console.log('Usage: node server.js start|refresh|recompute')
process.exit(1);
break;
}
}
main();