diff --git a/docker/images/java/Dockerfile b/docker/images/java/Dockerfile deleted file mode 100644 index 4da394f975f76f503ab0089197b954f894e3acfb..0000000000000000000000000000000000000000 --- a/docker/images/java/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -FROM baekjoon/onlinejudge-java:1.8 \ No newline at end of file diff --git a/package.json b/package.json index 064cf1d3cc25657442002bf470a698069cdde7df..6ed4420b6b5118fe3ab34759756727a7d7e908d8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.0.0", "description": "Algorithm Visualizer", "scripts": { + "postinstall": "docker pull baekjoon/onlinejudge-java:1.8", "dev": "NODE_ENV=development node bin/www", "start": "NODE_ENV=production node bin/www", "build": "npm run build:frontend && npm run build:backend", diff --git a/src/backend/apis/index.js b/src/backend/apis/index.js index cfee10c0f3a0cfb126eb2467e011990e5466f0c8..5aedb7235cab87e5f9c65b0c7237945aa23a3f28 100644 --- a/src/backend/apis/index.js +++ b/src/backend/apis/index.js @@ -1,14 +1,16 @@ import Promise from 'bluebird'; import axios from 'axios'; -import fs from 'fs-extra'; +import fs from 'fs'; import { githubClientId, githubClientSecret } from '/environment'; -axios.interceptors.request.use(request => { +const instance = axios.create(); + +instance.interceptors.request.use(request => { request.params = { client_id: githubClientId, client_secret: githubClientSecret, ...request.params }; return request; }); -axios.interceptors.response.use(response => { +instance.interceptors.response.use(response => { return response.data; }, error => { return Promise.reject(error.response.data); @@ -28,43 +30,43 @@ const request = (url, process) => { const GET = URL => { return request(URL, (mappedURL, args) => { const [params] = args; - return axios.get(mappedURL, { params }); + return instance.get(mappedURL, { params }); }); }; const DELETE = URL => { return request(URL, (mappedURL, args) => { const [params] = args; - return axios.delete(mappedURL, { params }); + return instance.delete(mappedURL, { params }); }); }; const POST = URL => { return request(URL, (mappedURL, args) => { const [body, params] = args; - return axios.post(mappedURL, body, { params }); + return instance.post(mappedURL, body, { params }); }); }; const PUT = URL => { return request(URL, (mappedURL, args) => { const [body, params] = args; - return axios.put(mappedURL, body, { params }); + return instance.put(mappedURL, body, { params }); }); }; const PATCH = URL => { return request(URL, (mappedURL, args) => { const [body, params] = args; - return axios.patch(mappedURL, body, { params }); + return instance.patch(mappedURL, body, { params }); }); }; const GitHubApi = { listCommits: GET('/repos/:owner/:repo/commits'), - getAccessToken: code => axios.post('https://github.com/login/oauth/access_token', { code }, { headers: { Accept: 'application/json' } }), + getAccessToken: code => instance.post('https://github.com/login/oauth/access_token', { code }, { headers: { Accept: 'application/json' } }), getLatestRelease: GET('/repos/:owner/:repo/releases/latest'), - download: (url, path) => axios({ + download: (url, path) => instance({ method: 'get', url, responseType: 'stream', diff --git a/src/backend/common/error.js b/src/backend/common/error.js index 1a0fccfc9d471a442fc001027d6f5775b6f92d8b..fefd65021558205daf18f1f2ab7b40d0432e8108 100644 --- a/src/backend/common/error.js +++ b/src/backend/common/error.js @@ -1,4 +1,7 @@ class ApplicationError extends Error { + toJSON() { + return { message: this.message }; + } } class NotFoundError extends ApplicationError { @@ -10,9 +13,17 @@ class PermissionError extends ApplicationError { class AuthorizationError extends ApplicationError { } +class CompileError extends ApplicationError { +} + +class RuntimeError extends ApplicationError { +} + export { ApplicationError, NotFoundError, PermissionError, AuthorizationError, + CompileError, + RuntimeError, }; \ No newline at end of file diff --git a/src/backend/controllers/categories.js b/src/backend/controllers/categories.js index 7b1815845fad79d7578e5ef7959cf1479e19ce4c..3b7b1155484bac2e718179ae58867aa03d706388 100644 --- a/src/backend/controllers/categories.js +++ b/src/backend/controllers/categories.js @@ -1,5 +1,5 @@ import express from 'express'; -import fs from 'fs-extra'; +import fs from 'fs'; import path from 'path'; import { NotFoundError } from '/common/error'; import { exec } from 'child_process'; diff --git a/src/backend/controllers/compilers.js b/src/backend/controllers/compilers.js deleted file mode 100644 index 3ae6df17d0afb881dcc8615fe1380c477933f6c4..0000000000000000000000000000000000000000 --- a/src/backend/controllers/compilers.js +++ /dev/null @@ -1,44 +0,0 @@ -import Promise from 'bluebird'; -import express from 'express'; -import fs from 'fs-extra'; -import uuid from 'uuid'; -import path from 'path'; -import { GitHubApi } from '/apis'; - -const router = express.Router(); - -const getLibPath = (...args) => path.resolve(__dirname, '..', 'public', 'libs', ...args); -const createTempDir = () => { - const dirPath = path.resolve(__dirname, '..', 'public', 'codes', uuid.v4()); - fs.mkdirSync(dirPath); - return dirPath; -}; - -const downloadLibs = () => { - GitHubApi.getLatestRelease('algorithm-visualizer', 'tracers').then(release => { - return Promise.each(release.assets, asset => GitHubApi.download(asset.browser_download_url, getLibPath(asset.name))); - }); -}; -downloadLibs(); // TODO: download again when webhooked - -const getJsWorker = (req, res, next) => { - res.sendFile(getLibPath('js.js')); -}; - -const compileJava = (req, res, next) => { - const dirPath = createTempDir(); - fs.writeFileSync(dirPath, req.body); - /* TODO: - 1. Write into a source file - 2. Execute in Docker - 3. Read the output file - */ -}; - -router.route('/js') - .get(getJsWorker); - -router.route('/java') - .post(compileJava); - -export default router; \ No newline at end of file diff --git a/src/backend/controllers/index.js b/src/backend/controllers/index.js index 9795620eca56a853ca0b1d04c9761742bf2f3f88..39cf1ef732e6a305b2b707314464a407f5a264b0 100644 --- a/src/backend/controllers/index.js +++ b/src/backend/controllers/index.js @@ -1,3 +1,3 @@ export { default as auth } from './auth'; export { default as categories } from './categories'; -export { default as compilers } from './compilers'; +export { default as tracers } from './tracers'; diff --git a/src/backend/controllers/tracers.js b/src/backend/controllers/tracers.js new file mode 100644 index 0000000000000000000000000000000000000000..0aa448f13a76504a09970212b007ab08fcbab731 --- /dev/null +++ b/src/backend/controllers/tracers.js @@ -0,0 +1,56 @@ +import Promise from 'bluebird'; +import express from 'express'; +import fs from 'fs-extra'; +import uuid from 'uuid'; +import path from 'path'; +import child_process from 'child_process'; +import { GitHubApi } from '/apis'; +import { __PROD__ } from '/environment'; +import { CompileError, RuntimeError } from '/common/error'; + +const router = express.Router(); + +const getLibPath = (...args) => path.resolve(__dirname, '..', 'public', 'libs', ...args); + +const downloadLibs = () => { + GitHubApi.getLatestRelease('algorithm-visualizer', 'tracers').then(release => { + return Promise.each(release.assets, asset => GitHubApi.download(asset.browser_download_url, getLibPath(asset.name))); + }); +}; +if (__PROD__) downloadLibs(); // TODO: download again when webhooked + +const getJsWorker = (req, res, next) => { + res.sendFile(getLibPath('js.js')); +}; + +const execute = (imageId, srcPath, command, ErrorClass) => new Promise((resolve, reject) => { + const libPath = getLibPath(); + const dockerCommand = `docker run --rm -w=/usr/judge -t -v=${libPath}:/usr/bin/tracers:ro -v=${srcPath}:/usr/judge:rw -e MAX_TRACES=1000000 -e MAX_TRACERS=100 ${imageId}`; + child_process.exec(`${dockerCommand} ${command}`, (error, stdout, stderr) => { + if (error) return reject(new ErrorClass(stdout)); + resolve(); + }); +}); + +const trace = ({ imageId, compileCommand, runCommand }) => (req, res, next) => { + const { code } = req.body; + const srcPath = path.resolve(__dirname, '..', 'public', 'codes', uuid.v4()); + fs.outputFile(path.resolve(srcPath, 'code.java'), code) + .then(() => execute(imageId, srcPath, compileCommand, CompileError)) + .then(() => execute(imageId, srcPath, runCommand, RuntimeError)) + .then(() => res.sendFile(path.resolve(srcPath, 'traces.json'))) + .catch(next) + .finally(() => fs.remove(srcPath)); +}; + +router.route('/js') + .get(getJsWorker); + +router.route('/java') + .post(trace({ + imageId: 'baekjoon/onlinejudge-java:1.8', + compileCommand: 'javac -cp /usr/bin/tracers/java.jar code.java', + runCommand: 'java -cp /usr/bin/tracers/java.jar:. Main', + })); + +export default router; \ No newline at end of file diff --git a/src/frontend/apis/index.js b/src/frontend/apis/index.js index a0427f7c7c1975c39602cdc38864ce3a7ad8add7..93a0e5d484b566fc9ff568c2ba5b751b2ac70a90 100644 --- a/src/frontend/apis/index.js +++ b/src/frontend/apis/index.js @@ -69,18 +69,19 @@ const GitHubApi = { }; let jsWorker = null; -const CompilerApi = { - js: code => new Promise((resolve, reject) => { +const TracerApi = { + js: ({ code }) => new Promise((resolve, reject) => { if (jsWorker) jsWorker.terminate(); - jsWorker = new Worker('/api/compilers/js'); + jsWorker = new Worker('/api/tracers/js'); jsWorker.onmessage = e => resolve(e.data); jsWorker.onerror = reject; jsWorker.postMessage(code); }), + java: POST('/tracers/java'), }; export { CategoryApi, GitHubApi, - CompilerApi, + TracerApi, }; \ No newline at end of file diff --git a/src/frontend/components/ToastContainer/stylesheet.scss b/src/frontend/components/ToastContainer/stylesheet.scss index 0bcd13e3203e1d400beca4d9a4b5d9266e95459f..e790e9531f925ee3af514c3592771335ba2defe9 100644 --- a/src/frontend/components/ToastContainer/stylesheet.scss +++ b/src/frontend/components/ToastContainer/stylesheet.scss @@ -13,6 +13,7 @@ padding: 16px; margin: 8px; font-size: $font-size-large; + white-space: pre-line; &.success { border-color: rgb(0, 150, 0); diff --git a/src/frontend/core/tracerManager.jsx b/src/frontend/core/tracerManager.jsx index b426d819a2d42bb3dddd1ec1a3c3460a4d1650de..d8223a231cc2a2424a6758c86efcdc75dff9dfb2 100644 --- a/src/frontend/core/tracerManager.jsx +++ b/src/frontend/core/tracerManager.jsx @@ -3,7 +3,7 @@ import Promise from 'bluebird'; import { extension } from '/common/util'; import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData } from '/core/datas'; import { Array1DRenderer, Array2DRenderer, ChartRenderer, GraphRenderer, LogRenderer, Renderer } from '/core/renderers'; -import { CompilerApi } from '/apis'; +import { TracerApi } from '/apis'; class TracerManager { constructor() { @@ -63,7 +63,7 @@ class TracerManager { setFile(file) { this.file = file; - this.runInitial(); + if (extension(file.name) === 'js') this.runInitial(); } reset(traces = []) { @@ -155,8 +155,12 @@ class TracerManager { execute() { const { name, content } = this.file; const ext = extension(name); - if (ext in CompilerApi) { - return CompilerApi[ext](content).then(traces => this.reset(traces)); + if (ext in TracerApi) { + return TracerApi[ext]({ code: content }) + .then(traces => this.reset(traces)) + .catch(e => { + throw e.err; + }); } else { return Promise.reject(new Error('Language Not Supported')); }