blob: 9bbc89b678be38fb5240721d548ee23efb962ce5 [file] [log] [blame] [raw]
// Copyright (c) 2016, Matt Godbolt
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
const temp = require('temp'),
fs = require('fs-extra'),
path = require('path'),
httpProxy = require('http-proxy'),
quote = require('shell-quote'),
_ = require('underscore'),
logger = require('../logger').logger,
utils = require('../utils'),
which = require('which'),
Sentry = require('@sentry/node');
temp.track();
let hasSetUpAutoClean = false;
function initialise(compilerEnv) {
if (hasSetUpAutoClean) return;
hasSetUpAutoClean = true;
const tempDirCleanupSecs = compilerEnv.ceProps("tempDirCleanupSecs", 600);
logger.info(`Cleaning temp dirs every ${tempDirCleanupSecs} secs`);
let cyclesBusy = 0;
setInterval(() => {
const status = compilerEnv.compilationQueue.status();
if (status.busy) {
cyclesBusy++;
logger.warn(
`temp cleanup skipped, pending: ${status.pending}, waiting: ${status.size}, cycles: ${cyclesBusy}`);
return;
}
cyclesBusy = 0;
temp.cleanup((err, stats) => {
if (err) logger.error('temp cleanup error', err);
if (stats) logger.debug('temp cleanup stats', stats);
});
}, tempDirCleanupSecs * 1000);
}
class CompileHandler {
constructor(compilationEnvironment, awsProps) {
this.compilersById = {};
this.compilerEnv = compilationEnvironment;
this.factories = {};
this.textBanner = this.compilerEnv.ceProps('textBanner');
this.proxy = httpProxy.createProxyServer({});
this.awsProps = awsProps;
initialise(this.compilerEnv);
// Mostly cribbed from
// https://github.com/nodejitsu/node-http-proxy/blob/master/examples/middleware/bodyDecoder-middleware.js
// We just keep the body as-is though: no encoding using queryString.stringify(), as we don't use a form
// decoding middleware.
this.proxy.on('proxyReq', function (proxyReq, req) {
if (!req.body || !Object.keys(req.body).length) {
return;
}
const contentType = proxyReq.getHeader('Content-Type');
let bodyData;
if (contentType === 'application/json') {
bodyData = JSON.stringify(req.body);
}
if (contentType === 'application/x-www-form-urlencoded') {
bodyData = req.body;
}
if (bodyData) {
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
proxyReq.write(bodyData);
}
});
}
async create(compiler) {
const type = compiler.compilerType || "default";
if (this.factories[type] === undefined) {
const compilerPath = '../compilers/' + type;
logger.info(`Loading compiler from ${compilerPath}`);
this.factories[type] = require(compilerPath);
}
// attempt to resolve non absolute exe paths
if (compiler.exe && !path.isAbsolute(compiler.exe)) {
const exe = await which(compiler.exe).catch(() => null);
if (exe) {
logger.debug(`Resolved '${compiler.exe}' to path '${exe}'`);
} else {
// errors resolving to absolute path are not fatal for backwards compatibility sake
logger.error(`Unable to resolve '${compiler.exe}'`);
}
}
if (compiler.exe && path.isAbsolute(compiler.exe)) {
// Try stat'ing the compiler to cache its mtime and only re-run it if it
// has changed since the last time.
try {
const res = await fs.stat(compiler.exe);
const cached = this.findCompiler(compiler.lang, compiler.id);
if (cached && cached.mtime.getTime() === res.mtime.getTime()) {
logger.debug(`${compiler.id} is unchanged`);
return cached;
}
const compilerObj = new this.factories[type](compiler, this.compilerEnv);
return compilerObj.initialise(res.mtime);
} catch (err) {
logger.warn(`Unable to stat ${compiler.id} compiler binary: `, err);
return null;
}
} else {
return new this.factories[type](compiler, this.compilerEnv);
}
}
async setCompilers(compilers) {
// Be careful not to update this.compilersById until we can replace it entirely.
const compilersById = {};
try {
const createdCompilers = _.compact(await Promise.all(_.map(compilers, this.create, this)));
for (const compiler of createdCompilers) {
const langId = compiler.compiler.lang;
if (!compilersById[langId]) compilersById[langId] = {};
compilersById[langId][compiler.compiler.id] = compiler;
}
if (this.awsProps) {
logger.info("Fetching possible arguments from storage");
await Promise.all(createdCompilers.map(
compiler => compiler.possibleArguments.loadFromStorage(this.awsProps)));
}
this.compilersById = compilersById;
return createdCompilers.map(compiler => compiler.getInfo());
} catch (err) {
logger.error("Exception while processing compilers:", err);
}
}
findCompiler(langId, compilerId) {
const langCompilers = this.compilersById[langId];
if (langCompilers && langCompilers[compilerId]) {
return langCompilers[compilerId];
}
// If the lang is bad, try to find it in every language
let response = undefined;
_.each(this.compilersById, compilerInLang => {
if (response === undefined) {
_.each(compilerInLang, compiler => {
if (response === undefined &&
(compiler.compiler.id === compilerId || compiler.compiler.alias === compilerId)) {
response = compiler;
}
});
}
});
return response;
}
compilerFor(req) {
if (req.is('json')) {
const compiler = this.findCompiler(req.lang || req.body.lang, req.params.compiler || req.body.compiler);
if (!compiler) {
const withoutBody = _.extend({}, req.body, {source: '<removed>'});
logger.warn(`Unable to find compiler with lang ${req.lang} for request`, withoutBody);
}
return compiler;
} else {
const compiler = this.findCompiler(req.lang, req.params.compiler);
if (!compiler) {
logger.warn(`Unable to find compiler with lang ${req.lang} for request params`, req.params);
}
return compiler;
}
}
parseRequest(req, compiler) {
let source, options, backendOptions, filters, bypassCache = false, tools, executionParameters = {};
let libraries = [];
// IF YOU MODIFY ANYTHING HERE PLEASE UPDATE THE DOCUMENTATION!
if (req.is('json')) {
// JSON-style request
const requestOptions = req.body.options;
source = req.body.source;
if (req.body.bypassCache)
bypassCache = true;
options = requestOptions.userArguments;
const execParams = requestOptions.executeParameters || {};
executionParameters.args = execParams.args;
executionParameters.stdin = execParams.stdin;
backendOptions = requestOptions.compilerOptions;
filters = requestOptions.filters || compiler.getDefaultFilters();
tools = requestOptions.tools;
libraries = requestOptions.libraries || [];
} else {
// API-style
source = req.body;
options = req.query.options;
// By default we get the default filters.
filters = compiler.getDefaultFilters();
// If specified exactly, we'll take that with ?filters=a,b,c
if (req.query.filters) {
filters = _.object(_.map(req.query.filters.split(","), filter => [filter, true]));
}
// Add a filter. ?addFilters=binary
_.each((req.query.addFilters || "").split(","), filter => {
if (filter) filters[filter] = true;
});
// Remove a filter. ?removeFilter=intel
_.each((req.query.removeFilters || "").split(","), filter => {
if (filter) delete filters[filter];
});
}
options = this.splitArguments(options);
if (!Array.isArray(executionParameters.args)) {
executionParameters.args = this.splitArguments(executionParameters.args);
}
tools = tools || [];
tools.forEach((tool) => {
tool.args = this.splitArguments(tool.args);
});
return {source, options, backendOptions, filters, bypassCache, tools, executionParameters, libraries};
}
splitArguments(options) {
return _.chain(quote.parse(options || '')
.map(x => typeof (x) === "string" ? x : x.pattern))
.compact()
.value();
}
handlePopularArguments(req, res, next) {
const compiler = this.compilerFor(req);
if (!compiler) return next();
res.send(compiler.possibleArguments.getPopularArguments(this.getUsedOptions(req)));
}
handleOptimizationArguments(req, res, next) {
const compiler = this.compilerFor(req);
if (!compiler) return next();
res.send(compiler.possibleArguments.getOptimizationArguments(this.getUsedOptions(req)));
}
getUsedOptions(req) {
if (req.body) {
const data = (typeof req.body === 'string') ? JSON.parse(req.body) : req.body;
if (data.presplit) {
return data.usedOptions;
} else {
return this.splitArguments(data.usedOptions);
}
}
return false;
}
handle(req, res, next) {
const compiler = this.compilerFor(req);
if (!compiler) {
return next();
}
const {
source, options, backendOptions, filters,
bypassCache, tools, executionParameters, libraries
} = this.parseRequest(req, compiler);
const remote = compiler.getRemote();
if (remote) {
req.url = remote.path;
this.proxy.web(req, res, {target: remote.target, changeOrigin: true}, e => {
logger.error("Proxy error: ", e);
next(e);
});
return;
}
if (source === undefined) {
logger.warn("No body found in request", req);
return next(new Error("Bad request"));
}
function textify(array) {
return _.pluck(array || [], 'text').join("\n");
}
compiler.compile(source, options, backendOptions, filters, bypassCache, tools, executionParameters, libraries)
.then(
result => {
if (req.accepts(['text', 'json']) === 'json') {
res.send(result);
} else {
res.set('Content-Type', 'text/plain');
try {
if (!_.isEmpty(this.textBanner)) res.write('# ' + this.textBanner + "\n");
res.write(textify(result.asm));
if (result.code !== 0) res.write("\n# Compiler exited with result code " + result.code);
if (!_.isEmpty(result.stdout)) res.write("\nStandard out:\n" + textify(result.stdout));
if (!_.isEmpty(result.stderr)) res.write("\nStandard error:\n" + textify(result.stderr));
} catch (ex) {
Sentry.captureException(ex);
res.write(`Error handling request: ${ex}`);
}
res.end('\n');
}
},
error => {
if (typeof (error) !== "string") {
if (error.stack) {
logger.error("Error during compilation: ", error);
Sentry.captureException(error);
} else if (error.code) {
logger.error("Error during compilation: ", error.code);
if (typeof (error.stderr) === "string") {
error.stdout = utils.parseOutput(error.stdout);
error.stderr = utils.parseOutput(error.stderr);
}
res.end(JSON.stringify(error));
return;
} else {
logger.error("Error during compilation: ", error);
}
error = `Internal Compiler Explorer error: ${error.stack || error}`;
} else {
logger.error("Error during compilation: ", {error});
}
res.end(JSON.stringify({code: -1, stderr: [{text: error}]}));
});
}
}
module.exports.Handler = CompileHandler;
module.exports.SetTestMode = function () {
hasSetUpAutoClean = true;
};