blob: 9577f6af15b34ef3a02c0298318c8b17c839d04c [file] [log] [blame] [raw]
// Copyright (c) 2016, Compiler Explorer Authors
// 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.
import path from 'path';
import * as Sentry from '@sentry/node';
import express from 'express';
import fs from 'fs-extra';
import Server from 'http-proxy';
import PromClient, {Counter} from 'prom-client';
import temp from 'temp';
import _ from 'underscore';
import which from 'which';
import {ICompiler} from '../../types/compiler.interfaces';
import {BaseCompiler} from '../base-compiler';
import {CompilationEnvironment} from '../compilation-env';
import {getCompilerTypeByKey} from '../compilers';
import {logger} from '../logger';
import * as utils from '../utils';
import {
CompileRequestJsonBody,
CompileRequestQueryArgs,
CompileRequestTextBody,
ExecutionRequestParams,
} from './compile.interfaces';
temp.track();
let hasSetUpAutoClean = false;
function initialise(compilerEnv: CompilationEnvironment) {
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);
}
export type ExecutionParams = {
args: string[];
stdin: string;
};
type ParsedRequest = {
source: string;
options: string[];
backendOptions: Record<string, any>;
filters: Record<string, boolean>;
bypassCache: boolean;
tools: any;
executionParameters: ExecutionParams;
libraries: any[];
};
export class CompileHandler {
private compilersById: Record<string, Record<string, BaseCompiler>> = {};
private readonly compilerEnv: CompilationEnvironment;
private readonly textBanner: string;
private readonly proxy: Server;
private readonly awsProps: (name: string, def?: string) => string;
private clientOptions: Record<string, any> | null = null;
private readonly compileCounter: Counter<string> = new PromClient.Counter({
name: 'ce_compilations_total',
help: 'Number of compilations',
labelNames: ['language'],
});
private readonly executeCounter: Counter<string> = new PromClient.Counter({
name: 'ce_executions_total',
help: 'Number of executions',
labelNames: ['language'],
});
private readonly cmakeCounter: Counter<string> = new PromClient.Counter({
name: 'ce_cmake_compilations_total',
help: 'Number of CMake compilations',
labelNames: ['language'],
});
private readonly cmakeExecuteCounter: Counter<string> = new PromClient.Counter({
name: 'ce_cmake_executions_total',
help: 'Number of executions after CMake',
labelNames: ['language'],
});
constructor(compilationEnvironment: CompilationEnvironment, awsProps: (name: string, def?: string) => string) {
this.compilerEnv = compilationEnvironment;
this.textBanner = this.compilerEnv.ceProps('textBanner');
this.proxy = Server.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) {
// TODO ideally I'd work out if this is "ok" - IncomingMessage doesn't have a body, but pragmatically the
// object we get here does.
const body = (req as any).body;
if (!body || Object.keys(body).length === 0) {
return;
}
const contentType = proxyReq.getHeader('Content-Type');
let bodyData;
if (contentType === 'application/json') {
bodyData = JSON.stringify(body);
}
if (contentType === 'application/x-www-form-urlencoded') {
bodyData = body;
}
if (bodyData) {
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
proxyReq.write(bodyData);
}
});
}
async create(compiler): Promise<ICompiler | null> {
const isPrediscovered = !!compiler.version;
const type = compiler.compilerType || 'default';
let compilerClass;
try {
compilerClass = getCompilerTypeByKey(type);
} catch (e) {
logger.error(`Compiler ID: ${compiler.id}`);
logger.error(e);
process.exit(1);
}
// 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}'`);
compiler.exe = 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 {
let modificationTime;
if (!isPrediscovered) {
const res = await fs.stat(compiler.exe);
modificationTime = res.mtime;
const cached = this.findCompiler(compiler.lang, compiler.id);
if (cached && cached.getModificationTime() === res.mtime.getTime()) {
logger.debug(`${compiler.id} is unchanged`);
return cached;
}
} else {
modificationTime = compiler.mtime;
}
const compilerObj = new compilerClass(compiler, this.compilerEnv);
return compilerObj.initialise(modificationTime, this.clientOptions, isPrediscovered);
} catch (err) {
logger.warn(`Unable to stat ${compiler.id} compiler binary: `, err);
return null;
}
} else {
return new compilerClass(compiler, this.compilerEnv);
}
}
async setCompilers(compilers: ICompiler[], clientOptions: Record<string, any>) {
// Be careful not to update this.compilersById until we can replace it entirely.
const compilersById = {};
try {
this.clientOptions = clientOptions;
logger.info('Creating compilers: ' + compilers.length);
let compilersCreated = 0;
const createdCompilers = _.compact(await Promise.all(_.map(compilers, this.create, this)));
for (const compiler of createdCompilers) {
const langId = compiler.getInfo().lang;
if (!compilersById[langId]) compilersById[langId] = {};
compilersById[langId][compiler.getInfo().id] = compiler;
compilersCreated++;
}
logger.info('Compilers created: ' + compilersCreated);
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);
}
}
compilerAliasMatch(compiler, compilerId): boolean {
return compiler.compiler.alias && compiler.compiler.alias.includes(compilerId);
}
compilerIdOrAliasMatch(compiler, compilerId): boolean {
return compiler.compiler.id === compilerId || this.compilerAliasMatch(compiler, compilerId);
}
findCompiler(langId, compilerId): BaseCompiler | undefined {
if (!compilerId) return;
const langCompilers: Record<string, BaseCompiler> | undefined = this.compilersById[langId];
if (langCompilers) {
if (langCompilers[compilerId]) {
return langCompilers[compilerId];
} else {
const compiler = _.find(langCompilers, compiler => {
return this.compilerAliasMatch(compiler, compilerId);
});
if (compiler) return compiler;
}
}
// If the lang is bad, try to find it in every language
let response: BaseCompiler | undefined;
_.each(this.compilersById, compilerInLang => {
if (!response) {
response = _.find(compilerInLang, compiler => {
return this.compilerIdOrAliasMatch(compiler, compilerId);
});
}
});
return response;
}
compilerFor(req) {
if (req.is('json')) {
const lang = req.lang || req.body.lang;
const compiler = this.findCompiler(lang, req.params.compiler);
if (!compiler) {
const withoutBody = _.extend({}, req.body, {source: '<removed>'});
logger.warn(`Unable to find compiler with lang ${lang} for JSON request`, withoutBody);
}
return compiler;
} else if (req.body && req.body.compiler) {
const compiler = this.findCompiler(req.body.lang, req.body.compiler);
if (!compiler) {
const withoutBody = _.extend({}, req.body, {source: '<removed>'});
logger.warn(`Unable to find compiler with lang ${req.body.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;
}
}
checkRequestRequirements(req: express.Request): CompileRequestJsonBody {
if (req.body.options === undefined) throw new Error('Missing options property');
if (req.body.source === undefined) throw new Error('Missing source property');
return req.body;
}
parseRequest(req: express.Request, compiler: BaseCompiler): ParsedRequest {
let source: string,
options: string,
backendOptions: Record<string, any> = {},
filters: Record<string, boolean>,
bypassCache = false,
tools;
const execReqParams: ExecutionRequestParams = {};
let libraries: any[] = [];
// IF YOU MODIFY ANYTHING HERE PLEASE UPDATE THE DOCUMENTATION!
if (req.is('json')) {
// JSON-style request
const jsonRequest = this.checkRequestRequirements(req);
const requestOptions = jsonRequest.options;
source = jsonRequest.source;
if (jsonRequest.bypassCache) bypassCache = true;
options = requestOptions.userArguments;
const execParams = requestOptions.executeParameters || {};
execReqParams.args = execParams.args;
execReqParams.stdin = execParams.stdin;
backendOptions = requestOptions.compilerOptions || {};
filters = {...compiler.getDefaultFilters(), ...requestOptions.filters};
tools = requestOptions.tools;
libraries = requestOptions.libraries || [];
} else if (req.body && req.body.compiler) {
const textRequest = req.body as CompileRequestTextBody;
source = textRequest.source;
if (textRequest.bypassCache) bypassCache = true;
options = textRequest.userArguments;
execReqParams.args = textRequest.executeParametersArgs;
execReqParams.stdin = textRequest.executeParametersStdin;
filters = compiler.getDefaultFilters();
_.each(filters, (value, item) => {
filters[item] = textRequest[item] === 'true';
});
backendOptions.skipAsm = textRequest.skipAsm === 'true';
} else {
// API-style
source = req.body;
const query = req.query as CompileRequestQueryArgs;
options = 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 (query.filters) {
filters = _.object(_.map(query.filters.split(','), filter => [filter, true])) as Record<
string,
boolean
>;
}
// Add a filter. ?addFilters=binary
_.each((query.addFilters || '').split(','), filter => {
if (filter) filters[filter] = true;
});
// Remove a filter. ?removeFilter=intel
_.each((query.removeFilters || '').split(','), filter => {
if (filter) delete filters[filter];
});
// Ask for asm not to be returned
backendOptions.skipAsm = query.skipAsm === 'true';
backendOptions.skipPopArgs = query.skipPopArgs === 'true';
}
const executionParameters: ExecutionParams = {
args: !Array.isArray(execReqParams.args)
? utils.splitArguments(execReqParams.args)
: execReqParams.args || '',
stdin: execReqParams.stdin || '',
};
tools = tools || [];
for (const tool of tools) {
tool.args = utils.splitArguments(tool.args);
}
return {
source,
options: utils.splitArguments(options),
backendOptions,
filters,
bypassCache,
tools,
executionParameters,
libraries,
};
}
handlePopularArguments(req: express.Request, res) {
const compiler = this.compilerFor(req);
if (!compiler) {
return res.sendStatus(404);
}
res.send(compiler.possibleArguments.getPopularArguments(this.getUsedOptions(req)));
}
handleOptimizationArguments(req: express.Request, res) {
const compiler = this.compilerFor(req);
if (!compiler) {
return res.sendStatus(404);
}
res.send(compiler.possibleArguments.getOptimizationArguments(this.getUsedOptions(req)));
}
getUsedOptions(req: express.Request) {
if (req.body) {
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
if (data.presplit) {
return data.usedOptions;
} else {
return utils.splitArguments(data.usedOptions);
}
}
return false;
}
handleApiError(error, res: express.Response, next: express.NextFunction) {
if (error.message) {
return res.status(400).send({
error: true,
message: error.message,
});
} else {
return next(error);
}
}
handleCmake(req: express.Request, res: express.Response, next: express.NextFunction) {
const compiler = this.compilerFor(req);
if (!compiler) {
return res.sendStatus(404);
}
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;
}
try {
if (req.body.files === undefined) throw new Error('Missing files property');
this.cmakeCounter.inc({language: compiler.lang.id});
const options = this.parseRequest(req, compiler);
compiler
.cmake(req.body.files, options)
.then(result => {
if (result.didExecute || (result.execResult && result.execResult.didExecute))
this.cmakeExecuteCounter.inc({language: compiler.lang.id});
res.send(result);
})
.catch(e => {
return this.handleApiError(e, res, next);
});
} catch (e) {
return this.handleApiError(e, res, next);
}
}
handle(req: express.Request, res: express.Response, next: express.NextFunction) {
const compiler = this.compilerFor(req);
if (!compiler) {
return res.sendStatus(404);
}
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;
}
let parsedRequest: ParsedRequest | undefined;
try {
parsedRequest = this.parseRequest(req, compiler);
} catch (error) {
return this.handleApiError(error, res, next);
}
const {source, options, backendOptions, filters, bypassCache, tools, executionParameters, libraries} =
parsedRequest;
let files;
if (req.body.files) files = req.body.files;
if (source === undefined || Object.keys(req.body).length === 0) {
logger.warn('No body found in request', req);
return next(new Error('Bad request'));
}
function textify(array) {
return _.pluck(array || [], 'text').join('\n');
}
this.compileCounter.inc({language: compiler.lang.id});
// eslint-disable-next-line promise/catch-or-return
compiler
.compile(
source,
options,
backendOptions,
filters,
bypassCache,
tools,
executionParameters,
libraries,
files,
)
.then(
result => {
if (result.didExecute || (result.execResult && result.execResult.didExecute))
this.executeCounter.inc({language: compiler.lang.id});
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));
if (result.execResult) {
res.write('\n\n# Execution result with exit code ' + result.execResult.code + '\n');
if (!_.isEmpty(result.execResult.stdout)) {
res.write('# Standard out:\n' + textify(result.execResult.stdout));
}
if (!_.isEmpty(result.execResult.stderr)) {
res.write('\n# Standard error:\n' + textify(result.execResult.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, stdout: [], stderr: [{text: error}]}));
},
);
}
}
export function SetTestMode() {
hasSetUpAutoClean = true;
}