Compare commits

...

No commits in common. 'master' and 'main' have entirely different histories.
master ... main

@ -1 +0,0 @@
HTTP_PORT = 8080

@ -1,2 +0,0 @@
CREDENTIALS_ENABLED = 0
WEBHOOK_ENABLED = 0

@ -1,2 +0,0 @@
CREDENTIALS_ENABLED = 0
WEBHOOK_ENABLED = 0

@ -1,5 +0,0 @@
# Optional: fill for GitHub auth / AWS when running `npm run start` (NODE_ENV=production).
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

12
.gitignore vendored

@ -1,12 +0,0 @@
# WebStorm settings
/.idea
# npm
/node_modules
/npm-debug.log
# macOS
.DS_Store
# downloaded files
/public

@ -1,54 +0,0 @@
# Contributing
> #### Table of Contents
> - [Running Locally](#running-locally)
> - [Directory Structure](#directory-structure)
Are you a first-timer in contributing to open source? [These guidelines](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution) from GitHub might help!
## Running Locally
1. Fork this repository.
2. Clone your forked repo to your machine.
```bash
git clone https://github.com/<your-username>/server.git
```
3. Install [Docker](https://docs.docker.com/install/), if not done already.
4. Create `.env.local` in the project root:
```bash
# By putting dummy values, GitHub sign in will not work locally
GITHUB_CLIENT_ID = dummy
GITHUB_CLIENT_SECRET = dummy
# By putting dummy values, extracting visualizing commands will not work locally (except for JavaScript).
AWS_ACCESS_KEY_ID = dummy
AWS_SECRET_ACCESS_KEY = dummy
```
5. Install dependencies, and run the server.
```bash
cd server
npm install
npm run watch
```
6. Open [`http://localhost:8080/`](http://localhost:8080/) in a web browser.
## Directory Structure
- [**src/**](src) contains source code.
- [**config/**](src/config) contains configuration files.
- [**controllers/**](src/controllers) routes and processes incoming requests.
- [**middlewares/**](src/middlewares) contains Express middlewares.
- [**models/**](src/models) manages algorithm visualizations and their hierarchy.
- [**tracers/**](src/tracers) build visualization libraries and compiles/runs code.
- [**utils/**](src/utils) contains utility files.
**NOTE** that for JavaScript, it builds a web worker rather than a docker image. Once a browser fetches the web worker, it will submit users' code to the web worker locally, instead of submitting to the remote server, to extract visualizing commands.

@ -1,9 +1,2 @@
# Server # algorithm-visualizer-server
> This repository is part of the project [Algorithm Visualizer](https://github.com/algorithm-visualizer).
`server` serves [`algorithm-visualizer`](https://github.com/algorithm-visualizer/algorithm-visualizer) and provides APIs that the web app needs on the fly. (e.g., GitHub sign in, compiling/running code, etc.)
## Contributing
Check out the [contributing guidelines](https://github.com/algorithm-visualizer/server/blob/master/CONTRIBUTING.md).

@ -1,7 +0,0 @@
config-dir = /home/ubuntu/.certbot/config
work-dir = /home/ubuntu/.certbot/work
logs-dir = /home/ubuntu/.certbot/logs
email = parkjs814@gmail.com
authenticator = webroot
webroot-path = /home/ubuntu/server/public/frontend-built
domains = algorithm-visualizer.org

1648
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,47 +0,0 @@
{
"name": "@algorithm-visualizer/server",
"version": "2.0.0",
"title": "Algorithm Visualizer",
"description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.",
"scripts": {
"watch": "NODE_ENV=development NODE_PATH=src ts-node-dev --respawn --ignore-watch node_modules --no-notify src",
"start": "NODE_ENV=production NODE_PATH=src ts-node --transpile-only src",
"tslint": "tslint -c tslint.json -p tsconfig.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/algorithm-visualizer/server.git"
},
"license": "MIT",
"devDependencies": {
"@types/compression": "0.0.36",
"@types/execa": "^0.9.0",
"@types/express": "^4.16.1",
"@types/fs-extra": "^7.0.0",
"@types/morgan": "^1.7.35",
"@types/node": "^12.0.0",
"@types/node-cron": "^3.0.1",
"@types/remove-markdown": "^0.1.1",
"@types/uuid": "^3.4.4",
"ts-node-dev": "^1.0.0-pre.39",
"tslint": "^5.16.0"
},
"dependencies": {
"aws-sdk": "^2.814.0",
"axios": "^0.21.2",
"body-parser": "^1.18.2",
"compression": "^1.7.3",
"dotenv": "^8.0.0",
"dotenv-flow": "^2.0.0",
"express": "^4.16.4",
"express-github-webhook": "^1.0.6",
"fs-extra": "^6.0.1",
"morgan": "^1.9.1",
"node-cron": "^3.0.0",
"remove-markdown": "^0.3.0",
"ts-httpexceptions": "^4.1.0",
"ts-node": "^8.1.0",
"typescript": "^3.4.5",
"uuid": "^3.3.2"
}
}

@ -1,8 +0,0 @@
{
"apps": [
{
"name": "algorithm-visualizer",
"script": "npm start"
}
]
}

@ -1,118 +0,0 @@
import express, { NextFunction, Request, Response } from 'express';
import morgan from 'morgan';
import bodyParser from 'body-parser';
import * as Controllers from 'controllers';
import { NotFound } from 'ts-httpexceptions';
import compression from 'compression';
import { __PROD__, credentials, httpPort, httpsPort, webhookOptions } from 'config/environments';
import http from 'http';
import https from 'https';
import cron from 'node-cron';
import { Hierarchy } from 'models';
import * as Tracers from 'tracers';
import { errorHandlerMiddleware, frontendMiddleware, redirectMiddleware } from 'middlewares';
import { execute, issueHttpsCertificate, pull } from 'utils/misc';
import { frontendBuildDir, frontendBuiltDir, frontendDir, rootDir } from 'config/paths';
const Webhook = require('express-github-webhook');
export default class Server {
readonly hierarchy = new Hierarchy();
readonly tracers = Object.values(Tracers).map(Tracer => new Tracer());
private readonly app = express();
private readonly webhook = webhookOptions && Webhook(webhookOptions);
constructor() {
this.app
.use(compression())
.use(morgan(__PROD__ ? 'tiny' : 'dev'))
.use(redirectMiddleware())
.use(bodyParser.json())
.use(bodyParser.urlencoded({ extended: true }))
.use('/api', this.getApiRouter())
.use(frontendMiddleware(this));
if (this.webhook) {
this.app.use(this.webhook);
}
this.app.use(errorHandlerMiddleware());
if (this.webhook) {
this.webhook.on('push', async (repo: string, data: any) => {
const { ref, head_commit } = data;
if (ref !== 'refs/heads/master') return;
if (!head_commit) throw new Error('The `head_commit` is empty.');
switch (repo) {
case 'server':
await this.update(head_commit.id);
break;
case 'algorithm-visualizer':
await this.updateFrontend(head_commit.id);
break;
case 'algorithms':
await this.hierarchy.update(head_commit.id);
break;
default:
throw new Error(`Webhook from unknown repository '${repo}'.`);
}
});
this.webhook.on('release', async (repo: string, data: any) => {
const tracer = this.tracers.find(tracer => repo === `tracers.${tracer.lang}`);
if (!tracer) throw new Error(`Tracer not found for repository '${repo}'.`);
await tracer.update(data.release);
});
}
if (credentials) {
cron.schedule('0 0 1 * *', () => {
issueHttpsCertificate();
});
}
}
getApiRouter() {
const router = express.Router();
Object.values(Controllers).forEach(Controller => new Controller(this).route(router));
router.use((req: Request, res: Response, next: NextFunction) => {
next(new NotFound('API not found.'));
});
return router;
};
async update(commit?: string) {
await pull(rootDir, 'server', commit);
await execute('npm install', {
cwd: rootDir,
stdout: process.stdout,
stderr: process.stderr,
});
process.exit(0);
};
async updateFrontend(commit?: string) {
await pull(frontendDir, 'algorithm-visualizer', commit);
await execute([
'npm install',
'npm run build',
`rm -rf ${frontendBuiltDir}`,
`mv ${frontendBuildDir} ${frontendBuiltDir}`,
].join(' && '), {
cwd: frontendDir,
stdout: process.stdout,
stderr: process.stderr,
});
}
start() {
const httpServer = http.createServer(this.app);
httpServer.listen(httpPort);
console.info(`http: listening on port ${httpPort}`);
if (credentials) {
const httpsServer = https.createServer(credentials, this.app);
httpsServer.listen(httpsPort);
console.info(`https: listening on port ${httpsPort}`);
}
}
}

@ -1,2 +0,0 @@
export const memoryLimit = 256; // in megabytes
export const timeLimit = 5000; // in milliseconds

@ -1,88 +0,0 @@
import fs from 'fs';
import { ServerOptions } from 'https';
import path from 'path';
import { issueHttpsCertificate } from '../utils/misc';
require('dotenv-flow').config();
const {
NODE_ENV,
HTTP_PORT,
HTTPS_PORT,
CREDENTIALS_ENABLED,
CREDENTIALS_PATH,
CREDENTIALS_CA,
CREDENTIALS_KEY,
CREDENTIALS_CERT,
WEBHOOK_ENABLED,
WEBHOOK_SECRET,
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
} = process.env as {
[key: string]: string,
};
const isEnabled = (v: string) => v === '1';
const isDev = NODE_ENV === 'development';
const missingVars = [
'NODE_ENV',
'HTTP_PORT',
'CREDENTIALS_ENABLED',
...(isEnabled(CREDENTIALS_ENABLED) ? [
'HTTPS_PORT',
'CREDENTIALS_PATH',
'CREDENTIALS_CA',
'CREDENTIALS_KEY',
'CREDENTIALS_CERT',
] : []),
'WEBHOOK_ENABLED',
...(isEnabled(WEBHOOK_ENABLED) ? [
'WEBHOOK_SECRET',
] : []),
...(!isDev ? [
'GITHUB_CLIENT_ID',
'GITHUB_CLIENT_SECRET',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
] : []),
].filter(variable => process.env[variable] === undefined);
if (missingVars.length) throw new Error(`The following environment variables are missing: ${missingVars.join(', ')}`);
export const __PROD__ = NODE_ENV === 'production';
export const __DEV__ = NODE_ENV === 'development';
export const httpPort = parseInt(HTTP_PORT, 10);
export const httpsPort = isEnabled(CREDENTIALS_ENABLED) ? parseInt(HTTPS_PORT || '8443', 10) : 0;
export const webhookOptions = isEnabled(WEBHOOK_ENABLED) ? {
path: '/webhook',
secret: WEBHOOK_SECRET,
} : undefined;
export let credentials: ServerOptions | undefined;
if (isEnabled(CREDENTIALS_ENABLED)) {
if (fs.existsSync(CREDENTIALS_PATH)) {
const readCredentials = (file: string) => fs.readFileSync(path.resolve(CREDENTIALS_PATH, file));
credentials = {
ca: readCredentials(CREDENTIALS_CA),
key: readCredentials(CREDENTIALS_KEY),
cert: readCredentials(CREDENTIALS_CERT),
};
} else {
issueHttpsCertificate();
}
}
export const githubClientId = GITHUB_CLIENT_ID ?? '';
export const githubClientSecret = GITHUB_CLIENT_SECRET ?? '';
export const awsAccessKeyId = AWS_ACCESS_KEY_ID ?? '';
export const awsSecretAccessKey = AWS_SECRET_ACCESS_KEY ?? '';

@ -1,10 +0,0 @@
import path from 'path';
export const rootDir = path.resolve(__dirname, '..', '..');
export const publicDir = path.resolve(rootDir, 'public');
export const algorithmsDir = path.resolve(publicDir, 'algorithms');
export const codesDir = path.resolve(publicDir, 'codes');
export const visualizationsDir = path.resolve(publicDir, 'visualizations');
export const frontendDir = path.resolve(publicDir, 'frontend');
export const frontendBuildDir = path.resolve(frontendDir, 'build');
export const frontendBuiltDir = path.resolve(publicDir, 'frontend-built');

@ -1,38 +0,0 @@
import express from 'express';
import { Controller } from 'controllers/Controller';
import { NotFound } from 'ts-httpexceptions';
import Server from 'Server';
export class AlgorithmsController extends Controller {
constructor(server: Server) {
super(server);
this.router
.get('/', this.getHierarchy)
.get('/:categoryKey/:algorithmKey', this.getAlgorithm)
.get('/sitemap.txt', this.getSitemap);
}
route = (router: express.Router): void => {
router.use('/algorithms', this.router);
};
getHierarchy = (req: express.Request, res: express.Response) => {
res.json(this.server.hierarchy);
};
getAlgorithm = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const {categoryKey, algorithmKey} = req.params;
const algorithm = this.server.hierarchy.find(categoryKey, algorithmKey);
if (!algorithm) return next(new NotFound('Algorithm not found.'));
res.json({algorithm});
};
getSitemap = (req: express.Request, res: express.Response) => {
const urls: string[] = [];
this.server.hierarchy.iterate((category, algorithm) => {
urls.push(`https://algorithm-visualizer.org/${category.key}/${algorithm.key}`);
});
res.set('Content-Type', 'text/plain');
res.send(urls.join('\n'));
};
}

@ -1,36 +0,0 @@
import express from 'express';
import { githubClientId } from 'config/environments';
import { GitHubApi } from 'utils/apis';
import { Controller } from 'controllers/Controller';
import Server from 'Server';
export class AuthController extends Controller {
constructor(server: Server) {
super(server);
this.router
.get('/request', this.request)
.get('/response', this.response)
.get('/destroy', this.destroy);
}
route = (router: express.Router): void => {
router.use('/auth', this.router);
};
request = (req: express.Request, res: express.Response) => {
res.redirect(`https://github.com/login/oauth/authorize?client_id=${githubClientId}&scope=user,gist`);
};
response = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const {code} = req.query;
GitHubApi.getAccessToken(code).then(({data}) => {
const {access_token} = data;
res.send(`<script>window.opener.signIn('${access_token}');window.close();</script>`);
}).catch(next);
};
destroy = (req: express.Request, res: express.Response) => {
res.send(`<script>window.opener.signOut();window.close();</script>`);
};
}

@ -1,12 +0,0 @@
import express from "express";
import Server from 'Server';
export abstract class Controller {
protected readonly router: express.Router;
protected constructor(protected server: Server) {
this.router = express.Router();
}
abstract route(router: express.Router): void;
}

@ -1,14 +0,0 @@
import express from 'express';
import { Controller } from 'controllers/Controller';
import Server from 'Server';
export class TracersController extends Controller {
constructor(server: Server) {
super(server);
this.server.tracers.forEach(tracer => tracer.route(this.router));
}
route = (router: express.Router): void => {
router.use('/tracers', this.router);
};
}

@ -1,41 +0,0 @@
import express from 'express';
import path from 'path';
import uuid from 'uuid';
import fs from 'fs-extra';
import { Controller } from 'controllers/Controller';
import Server from 'Server';
import { visualizationsDir } from 'config/paths';
export class VisualizationsController extends Controller {
constructor(server: Server) {
super(server);
this.router
.post('/', this.uploadVisualization)
.get('/:visualizationId', this.getVisualization);
fs.remove(visualizationsDir).catch(console.error);
}
route = (router: express.Router): void => {
router.use('/visualizations', this.router);
};
uploadVisualization = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const {content} = req.body;
const visualizationId = uuid.v4();
const visualizationPath = path.resolve(visualizationsDir, `${visualizationId}.json`);
const url = `https://algorithm-visualizer.org/scratch-paper/new?visualizationId=${visualizationId}`;
fs.outputFile(visualizationPath, content)
.then(() => res.send(url))
.catch(next);
};
getVisualization = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const {visualizationId} = req.params;
const visualizationPath = path.resolve(visualizationsDir, `${visualizationId}.json`);
res.sendFile(visualizationPath, err => {
if (err) next(new Error('Visualization Expired'));
fs.remove(visualizationPath);
});
};
}

@ -1,4 +0,0 @@
export { AlgorithmsController } from './AlgorithmsController';
export { AuthController } from './AuthController';
export { TracersController } from './TracersController';
export { VisualizationsController } from './VisualizationsController';

@ -1,3 +0,0 @@
import Server from 'Server';
new Server().start();

@ -1,14 +0,0 @@
import { NextFunction, Request, Response } from 'express';
import { Exception, InternalServerError } from 'ts-httpexceptions';
export function errorHandlerMiddleware() {
return (err: any, req: Request, res: Response, next: NextFunction) => {
if (!(err instanceof Exception)) {
console.error(err);
err = new InternalServerError(err.message, err);
}
const {message, status} = err;
res.status(status).send(message);
};
}

@ -1,51 +0,0 @@
import express, { NextFunction, Request, Response } from 'express';
import path from 'path';
import fs from 'fs-extra';
import url from 'url';
import Server from 'Server';
import { frontendBuiltDir } from 'config/paths';
const packageJson = require('../../package.json');
export function frontendMiddleware(server: Server) {
const staticMiddleware = express.static(frontendBuiltDir, {index: false});
if (!fs.pathExistsSync(frontendBuiltDir)) {
server.updateFrontend().catch(console.error);
}
return (req: Request, res: Response, next: NextFunction) => {
staticMiddleware(req, res, err => {
if (err) return next(err);
if (req.method !== 'GET') return next();
const filePath = path.resolve(frontendBuiltDir, 'index.html');
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) return next(err);
const {pathname} = url.parse(req.originalUrl);
if (!pathname) return next(new Error('Failed to get `pathname`.'));
const [, categoryKey, algorithmKey] = pathname.split('/');
let {title, description} = packageJson;
let algorithm = undefined;
if (categoryKey && categoryKey !== 'scratch-paper') {
algorithm = server.hierarchy.find(categoryKey, algorithmKey) || null;
if (algorithm) {
title = [algorithm.categoryName, algorithm.algorithmName].join(' - ');
description = algorithm.description;
} else {
res.status(404);
return;
}
}
const indexFile = data
.replace(/\$TITLE/g, title)
.replace(/\$DESCRIPTION/g, description)
.replace(/\$ALGORITHM/g, algorithm === undefined ? 'undefined' :
JSON.stringify(algorithm).replace(/</g, '\\u003c'));
res.send(indexFile);
});
});
};
}

@ -1,3 +0,0 @@
export { errorHandlerMiddleware } from './errorHandlerMiddleware';
export { frontendMiddleware } from './frontendMiddleware';
export { redirectMiddleware } from './redirectMiddleware';

@ -1,14 +0,0 @@
import { NextFunction, Request, Response } from 'express';
import { credentials } from 'config/environments';
export function redirectMiddleware() {
return (req: Request, res: Response, next: NextFunction) => {
if (req.hostname === 'algo-visualizer.jasonpark.me') {
res.redirect(301, 'https://algorithm-visualizer.org/');
} else if (credentials && !req.secure) {
res.redirect(301, `https://${req.hostname}${req.url}`);
} else {
next();
}
};
}

@ -1,26 +0,0 @@
import path from 'path';
import { createKey, listFiles } from 'utils/hierarchy';
import { File } from 'models';
import { getDescription } from 'utils/misc';
export class Algorithm {
key: string;
files!: File[];
description!: string;
constructor(private path: string, public name: string) {
this.key = createKey(name);
this.refresh();
}
refresh() {
this.files = listFiles(this.path)
.map(fileName => new File(path.resolve(this.path, fileName), fileName));
this.description = getDescription(this.files);
}
toJSON() {
const {key, name} = this;
return {key, name};
}
}

@ -1,25 +0,0 @@
import path from 'path';
import { createKey, listDirectories } from 'utils/hierarchy';
import { Algorithm } from 'models';
export class Category {
key: string;
algorithms!: Algorithm[];
constructor(private path: string, public name: string) {
this.key = createKey(name);
this.refresh();
}
refresh() {
this.algorithms = listDirectories(this.path)
.map(algorithmName => new Algorithm(path.resolve(this.path, algorithmName), algorithmName));
}
toJSON() {
const {key, name, algorithms} = this;
return {key, name, algorithms};
}
}
export default Category;

@ -1,25 +0,0 @@
import fs from 'fs-extra';
export type Author = {
login: string,
avatar_url: string,
}
export class File {
content!: string;
contributors!: Author[];
constructor(public path: string, public name: string) {
this.refresh();
}
refresh() {
this.content = fs.readFileSync(this.path, 'utf-8');
this.contributors = [];
}
toJSON() {
const {name, content, contributors} = this;
return {name, content, contributors};
}
}

@ -1,92 +0,0 @@
import path from 'path';
import { listDirectories } from 'utils/hierarchy';
import { GitHubApi } from 'utils/apis';
import { Algorithm, Category, File } from 'models';
import { Author } from 'models/File';
import { algorithmsDir } from 'config/paths';
import { execute, pull } from 'utils/misc';
type CommitAuthors = {
[sha: string]: Author,
}
export class Hierarchy {
private categories!: Category[];
readonly path: string = algorithmsDir;
constructor() {
this.refresh();
this.update().catch(console.error);
}
refresh() {
this.categories = listDirectories(this.path)
.map(categoryName => new Category(path.resolve(this.path, categoryName), categoryName));
const files: File[] = [];
this.categories.forEach(category => category.algorithms.forEach(algorithm => files.push(...algorithm.files)));
this.cacheCommitAuthors().then(commitAuthors => this.cacheContributors(files, commitAuthors));
}
async update(commit?: string) {
await pull(this.path, 'algorithms', commit);
this.refresh();
};
async cacheCommitAuthors(page = 1, commitAuthors: CommitAuthors = {}): Promise<CommitAuthors> {
const per_page = 100;
const {data} = await GitHubApi.listCommits('algorithm-visualizer', 'algorithms', {per_page, page});
const commits: any[] = data;
for (const {sha, author} of commits) {
if (!author) continue;
const {login, avatar_url} = author;
commitAuthors[sha] = {login, avatar_url};
}
if (commits.length < per_page) {
return commitAuthors;
} else {
return this.cacheCommitAuthors(page + 1, commitAuthors);
}
}
async cacheContributors(files: File[], commitAuthors: CommitAuthors) {
for (const file of files) {
const stdout = await execute(`git --no-pager log --follow --no-merges --format="%H" -- "${path.relative(this.path, file.path)}"`, {
cwd: this.path,
});
const output = stdout.toString().replace(/\n$/, '');
const shas = output.split('\n').reverse();
const contributors: Author[] = [];
for (const sha of shas) {
const author = commitAuthors[sha];
if (author && !contributors.find(contributor => contributor.login === author.login)) {
contributors.push(author);
}
}
file.contributors = contributors;
}
}
find(categoryKey: string, algorithmKey: string) {
const category = this.categories.find(category => category.key === categoryKey);
if (!category) return;
const algorithm = category.algorithms.find(algorithm => algorithm.key === algorithmKey);
if (!algorithm) return;
const categoryName = category.name;
const algorithmName = algorithm.name;
const files = algorithm.files;
const description = algorithm.description;
return {categoryKey, categoryName, algorithmKey, algorithmName, files, description};
}
iterate(callback: (category: Category, algorithm: Algorithm) => void) {
this.categories.forEach(category => category.algorithms.forEach(algorithm => callback(category, algorithm)));
}
toJSON() {
const {categories} = this;
return {categories};
}
}

@ -1,4 +0,0 @@
export { Algorithm } from './Algorithm';
export { Category } from './Category';
export { File } from './File';
export { Hierarchy } from './Hierarchy';

@ -1,66 +0,0 @@
import path from 'path';
import { Release, Tracer } from 'tracers/Tracer';
import express from 'express';
import uuid from 'uuid';
import fs from 'fs-extra';
import { memoryLimit, timeLimit } from 'config/constants';
import { codesDir } from 'config/paths';
import { execute } from 'utils/misc';
export class DockerTracer extends Tracer {
private readonly directory: string;
private readonly imageName: string;
constructor(lang: string) {
super(lang);
this.directory = path.resolve(__dirname, lang);
this.imageName = `tracer-${this.lang}`;
}
build(release: Release) {
const {tag_name} = release;
return execute(`docker build -t ${this.imageName} . --build-arg tag_name=${tag_name}`, {
cwd: this.directory,
stdout: process.stdout,
stderr: process.stderr,
});
}
route(router: express.Router) {
router.post(`/${this.lang}`, (req, res, next) => {
const {code} = req.body;
const tempPath = path.resolve(codesDir, uuid.v4());
fs.outputFile(path.resolve(tempPath, `Main.${this.lang}`), code)
.then(() => {
const containerName = uuid.v4();
let killed = false;
const timer = setTimeout(() => {
execute(`docker kill ${containerName}`).then(() => {
killed = true;
});
}, timeLimit);
return execute([
'docker run --rm',
`--name=${containerName}`,
'-w=/usr/visualization',
`-v=${tempPath}:/usr/visualization:rw`,
`-m=${memoryLimit}m`,
'-e ALGORITHM_VISUALIZER=1',
this.imageName,
].join(' ')).catch(error => {
if (killed) throw new Error('Time Limit Exceeded');
throw error;
}).finally(() => clearTimeout(timer));
})
.then(() => new Promise((resolve, reject) => {
const visualizationPath = path.resolve(tempPath, 'visualization.json');
res.sendFile(visualizationPath, (err: any) => {
if (err) return reject(new Error('Visualization Not Found'));
resolve();
});
}))
.catch(next)
.finally(() => fs.remove(tempPath));
});
}
}

@ -1,33 +0,0 @@
import AWS from 'aws-sdk';
import express from 'express';
import { Release, Tracer } from 'tracers/Tracer';
import { awsAccessKeyId, awsSecretAccessKey } from 'config/environments';
import { BadRequest } from 'ts-httpexceptions';
export class LambdaTracer extends Tracer {
static lambda = new AWS.Lambda({
region: 'us-east-2',
accessKeyId: awsAccessKeyId,
secretAccessKey: awsSecretAccessKey,
});
async build(release: Release) {
}
route(router: express.Router) {
router.post(`/${this.lang}`, (req, res, next) => {
const {code} = req.body;
LambdaTracer.lambda.invoke({
FunctionName: `extractor-${this.lang}`,
InvocationType: 'RequestResponse',
Payload: JSON.stringify(code),
}, function (err, data) {
if (err) return next(err);
if (typeof data.Payload !== 'string') return next(new Error('Unexpected Payload Type'));
const payload = JSON.parse(data.Payload);
if (!payload.success) return next(new BadRequest(payload.errorMessage));
res.send(payload.commands);
});
});
}
}

@ -1,24 +0,0 @@
import express from 'express';
import { GitHubApi } from 'utils/apis';
export type Release = {
tag_name: string;
}
export abstract class Tracer {
protected constructor(public lang: string) {
this.update().catch(console.error);
}
abstract build(release: Release): Promise<any>;
abstract route(router: express.Router): void;
async update(release?: Release) {
if (release) {
return this.build(release);
}
const {data} = await GitHubApi.getLatestRelease('algorithm-visualizer', `tracers.${this.lang}`);
return this.build(data);
}
}

@ -1,7 +0,0 @@
import { DockerTracer } from 'tracers/DockerTracer';
export class CppTracer extends DockerTracer {
constructor() {
super('cpp');
}
}

@ -1,17 +0,0 @@
FROM rikorose/gcc-cmake
ARG tag_name
RUN curl --create-dirs -o /usr/local/include/nlohmann/json.hpp -L "https://github.com/nlohmann/json/releases/download/v3.1.2/json.hpp" \
&& curl --create-dirs -o /usr/tmp/algorithm-visualizer.tar.gz -L "https://github.com/algorithm-visualizer/tracers.cpp/archive/${tag_name}.tar.gz" \
&& cd /usr/tmp \
&& mkdir algorithm-visualizer \
&& tar xvzf algorithm-visualizer.tar.gz -C algorithm-visualizer --strip-components=1 \
&& cd /usr/tmp/algorithm-visualizer \
&& mkdir build \
&& cd build \
&& cmake .. \
&& make install
CMD g++ Main.cpp -o Main -O2 -std=c++11 -lcurl \
&& ./Main

@ -1,3 +0,0 @@
export { CppTracer } from './cpp/CppTracer';
export { JavaTracer } from './java/JavaTracer';
export { JsTracer } from './js/JsTracer';

@ -1,7 +0,0 @@
import { LambdaTracer } from 'tracers/LambdaTracer';
export class JavaTracer extends LambdaTracer {
constructor() {
super('java');
}
}

@ -1,27 +0,0 @@
import path from 'path';
import { Release, Tracer } from 'tracers/Tracer';
import express from 'express';
export class JsTracer extends Tracer {
readonly workerPath: string;
tagName?: string;
constructor() {
super('js');
this.workerPath = path.resolve(__dirname, 'worker.js');
}
async build(release: Release) {
const {tag_name} = release;
this.tagName = tag_name;
}
route(router: express.Router) {
router.get(`/${this.lang}`, (req, res) => {
if (!this.tagName) throw new Error('JsTracer has not been built yet.');
const version = this.tagName.slice(1);
res.redirect(`https://unpkg.com/algorithm-visualizer@${version}/dist/algorithm-visualizer.umd.js`);
});
router.get(`/${this.lang}/worker`, (req, res) => res.sendFile(this.workerPath));
}
}

@ -1,14 +0,0 @@
const process = { env: { ALGORITHM_VISUALIZER: '1' } };
importScripts('/api/tracers/js');
const sandbox = code => {
const require = name => ({ 'algorithm-visualizer': AlgorithmVisualizer }[name]); // fake require
eval(code);
};
onmessage = e => {
const lines = e.data.split('\n').map((line, i) => line.replace(/(\.\s*delay\s*)\(\s*\)/g, `$1(${i})`));
const code = lines.join('\n');
sandbox(code);
postMessage(AlgorithmVisualizer.Commander.commands);
};

@ -1,61 +0,0 @@
import axios, { AxiosResponse } from 'axios';
import { githubClientId, githubClientSecret } from 'config/environments';
const instance = axios.create();
instance.interceptors.request.use(request => {
request.params = {client_id: githubClientId, client_secret: githubClientSecret, ...request.params};
return request;
});
const request = (url: string, process: (mappedUrl: string, args: any[]) => Promise<AxiosResponse<any>>) => {
const tokens = url.split('/');
const baseURL = /^https?:\/\//i.test(url) ? '' : 'https://api.github.com';
return (...args: any[]) => {
const mappedUrl = baseURL + tokens.map(token => token.startsWith(':') ? args.shift() : token).join('/');
return process(mappedUrl, args);
};
};
const GET = (url: string) => {
return request(url, (mappedUrl: string, args: any[]) => {
const [params] = args;
return instance.get(mappedUrl, {params});
});
};
const DELETE = (url: string) => {
return request(url, (mappedUrl: string, args: any[]) => {
const [params] = args;
return instance.delete(mappedUrl, {params});
});
};
const POST = (url: string) => {
return request(url, (mappedUrl: string, args: any[]) => {
const [body, params] = args;
return instance.post(mappedUrl, body, {params});
});
};
const PUT = (url: string) => {
return request(url, (mappedUrl: string, args: any[]) => {
const [body, params] = args;
return instance.put(mappedUrl, body, {params});
});
};
const PATCH = (url: string) => {
return request(url, (mappedUrl: string, args: any[]) => {
const [body, params] = args;
return instance.patch(mappedUrl, body, {params});
});
};
export const GitHubApi = {
listCommits: GET('/repos/:owner/:repo/commits'),
getAccessToken: (code: string) => instance.post('https://github.com/login/oauth/access_token', {code}, {headers: {Accept: 'application/json'}}),
getLatestRelease: GET('/repos/:owner/:repo/releases/latest'),
};

@ -1,18 +0,0 @@
import path from 'path';
import fs from 'fs-extra';
export function createKey(name: string) {
return name.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
}
export function isDirectory(dirPath: string) {
return fs.lstatSync(dirPath).isDirectory();
}
export function listFiles(dirPath: string) {
return fs.pathExistsSync(dirPath) ? fs.readdirSync(dirPath).filter(fileName => !fileName.startsWith('.')) : [];
}
export function listDirectories(dirPath: string) {
return listFiles(dirPath).filter(fileName => isDirectory(path.resolve(dirPath, fileName)));
}

@ -1,80 +0,0 @@
import axios from 'axios';
import fs from 'fs-extra';
import { File } from 'models';
import removeMarkdown from 'remove-markdown';
import * as child_process from 'child_process';
import { ExecOptions, spawn } from 'child_process';
import { rootDir } from '../config/paths';
import path from 'path';
export function download(url: string, localPath: string) {
return axios({ url, method: 'GET', responseType: 'stream' })
.then(response => new Promise((resolve, reject) => {
const writer = fs.createWriteStream(localPath);
writer.on('finish', resolve);
writer.on('error', reject);
response.data.pipe(writer);
}));
}
export async function pull(dir: string, repo: string, commit = 'origin/master') {
if (fs.pathExistsSync(dir)) {
await execute(`git fetch`, {
cwd: dir,
stdout: process.stdout,
stderr: process.stderr,
});
} else {
await execute(`git clone https://github.com/algorithm-visualizer/${repo}.git ${dir}`, {
stdout: process.stdout,
stderr: process.stderr,
});
}
await execute(`git reset --hard ${commit}`, {
cwd: dir,
stdout: process.stdout,
stderr: process.stderr,
});
}
export function getDescription(files: File[]) {
const readmeFile = files.find(file => file.name === 'README.md');
if (!readmeFile) return '';
const lines = readmeFile.content.split('\n');
lines.shift();
while (lines.length && !lines[0].trim()) lines.shift();
const descriptionLines = [];
while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift());
return removeMarkdown(descriptionLines.join(' '));
}
type ExecuteOptions = ExecOptions & {
stdout?: NodeJS.WriteStream;
stderr?: NodeJS.WriteStream;
};
export function execute(command: string, { stdout, stderr, ...options }: ExecuteOptions = {}): Promise<string> {
return new Promise((resolve, reject) => {
const child = child_process.exec(command, options, (error, stdout, stderr) => {
if (error) return reject(error.code ? new Error(stderr) : error);
resolve(stdout);
});
if (child.stdout && stdout) child.stdout.pipe(stdout);
if (child.stderr && stderr) child.stderr.pipe(stderr);
});
}
export function issueHttpsCertificate() {
const certbotIniPath = path.resolve(rootDir, 'certbot.ini');
const childProcess = spawn('certbot', ['certonly', '--non-interactive', '--agree-tos', '--config', certbotIniPath]);
childProcess.stdout.pipe(process.stdout);
childProcess.stderr.pipe(process.stderr);
childProcess.on('error', console.error);
childProcess.on('exit', code => {
if (code === 0) {
process.exit(0);
} else {
console.error(new Error(`certbot failed with exit code ${code}.`));
}
});
}

@ -1,23 +0,0 @@
{
"compilerOptions": {
"target": "es2015",
"lib": [
"es2015",
"esnext"
],
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
"declaration": false,
"strict": true,
"baseUrl": "src",
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"public"
]
}

@ -1,61 +0,0 @@
{
"rules": {
"class-name": true,
"comment-format": [
true,
"check-space"
],
"indent": [
true,
"spaces",
2
],
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"no-var-keyword": true,
"quotemark": [
true,
"single",
"avoid-escape"
],
"semicolon": [
true,
"always",
"ignore-bound-class-methods"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-module",
"check-separator",
"check-type"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
],
"no-internal-module": true,
"no-trailing-whitespace": true,
"no-null-keyword": true,
"prefer-const": true,
"jsdoc-format": true
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save