415 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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();
 |