mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 09:23:52 -05:00
228 lines
8.9 KiB
TypeScript
Executable File
228 lines
8.9 KiB
TypeScript
Executable File
// Copyright (c) 2018, 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 'node:path';
|
|
|
|
import PromClient from 'prom-client';
|
|
import _ from 'underscore';
|
|
|
|
import {CompilationInfo, ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
|
|
import {UnprocessedExecResult} from '../../types/execution/execution.interfaces.js';
|
|
import {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
|
|
import {ResultLine} from '../../types/resultline/resultline.interfaces.js';
|
|
import {ToolInfo, ToolResult} from '../../types/tool.interfaces.js';
|
|
import * as exec from '../exec.js';
|
|
import {logger} from '../logger.js';
|
|
import {OptionsHandlerLibrary} from '../options-handler.js';
|
|
import {propsFor} from '../properties.js';
|
|
import {parseOutput} from '../utils.js';
|
|
import {ITool, ToolEnv} from './base-tool.interface.js';
|
|
|
|
const toolCounter = new PromClient.Counter({
|
|
name: 'tool_invocations_total',
|
|
help: 'Number of tool invocations',
|
|
labelNames: ['language', 'name'],
|
|
});
|
|
|
|
export class BaseTool implements ITool {
|
|
public readonly tool: ToolInfo;
|
|
protected env: ToolEnv;
|
|
protected addOptionsToToolArgs = true;
|
|
public readonly id: string;
|
|
public readonly type: string;
|
|
public readonly sandboxType: string;
|
|
|
|
constructor(toolInfo: ToolInfo, env: ToolEnv) {
|
|
this.tool = toolInfo;
|
|
this.env = env;
|
|
this.addOptionsToToolArgs = true;
|
|
this.id = toolInfo.id;
|
|
this.type = toolInfo.type || 'independent';
|
|
|
|
const execProps = propsFor('execution');
|
|
this.sandboxType = execProps('sandboxType', 'none');
|
|
}
|
|
|
|
getUniqueFilePrefix() {
|
|
const timestamp = process.hrtime();
|
|
const timestamp_str = '_' + timestamp[0] * 1000000 + timestamp[1] / 1000;
|
|
return this.tool.id.replaceAll(/[^\da-z]/gi, '_') + timestamp_str + '_';
|
|
}
|
|
|
|
isCompilerExcluded(compilerId: string, compilerProps: ToolEnv['compilerProps']): boolean {
|
|
if (this.tool.includeKey) {
|
|
// If the includeKey is set, we only support compilers that have a truthy 'includeKey'.
|
|
if (!compilerProps(this.tool.includeKey)) {
|
|
return true;
|
|
}
|
|
// Even if the include key is truthy, we fall back to the exclusion list.
|
|
}
|
|
|
|
// an empty value (e.g. 'tool.foo.exclude=') yields a single empty
|
|
// string in the array, not an empty array.
|
|
if (this.tool.exclude.length === 1 && this.tool.exclude[0] === '') return false;
|
|
|
|
return (
|
|
this.tool.exclude.find(excl => {
|
|
if (excl.endsWith('$')) {
|
|
return compilerId === excl.substring(0, excl.length - 1);
|
|
}
|
|
return compilerId.includes(excl);
|
|
}) !== undefined
|
|
);
|
|
}
|
|
|
|
exec(toolExe: string, args: string[], options: ExecutionOptions) {
|
|
return exec.execute(toolExe, args, options);
|
|
}
|
|
|
|
getDefaultExecOptions(): ExecutionOptions {
|
|
return {
|
|
timeoutMs: this.env.ceProps('compileTimeoutMs', 7500) as number,
|
|
maxErrorOutput: this.env.ceProps('max-error-output', 5000) as number,
|
|
wrapper: this.env.compilerProps('compiler-wrapper'),
|
|
};
|
|
}
|
|
|
|
// By default calls utils.parseOutput, but lets subclasses override their output processing
|
|
protected parseOutput(lines: string, inputFilename?: string, pathPrefix?: string): ResultLine[] {
|
|
return parseOutput(lines, inputFilename, pathPrefix);
|
|
}
|
|
|
|
createErrorResponse(message: string): ToolResult {
|
|
return {
|
|
id: this.tool.id,
|
|
name: this.tool.name,
|
|
code: -1,
|
|
languageId: 'stderr',
|
|
stdout: [],
|
|
stderr: this.parseOutput(message),
|
|
};
|
|
}
|
|
|
|
// mostly copy&paste from base-compiler.js
|
|
findLibVersion(selectedLib: SelectedLibraryVersion, supportedLibraries: Record<string, OptionsHandlerLibrary>) {
|
|
const foundLib = _.find(supportedLibraries, (o, libId) => libId === selectedLib.id);
|
|
if (!foundLib) return false;
|
|
|
|
return _.find(foundLib.versions, (o, versionId) => versionId === selectedLib.version);
|
|
}
|
|
|
|
protected replacePathsIfNeededForSandbox(args: string[], physicalPath: string): string[] {
|
|
if (this.sandboxType !== 'nsjail' || !physicalPath) return args;
|
|
|
|
return args.map(arg => {
|
|
if (arg && arg.length > 1) {
|
|
return arg.replace(physicalPath, '/app');
|
|
} else {
|
|
return '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// mostly copy&paste from base-compiler.js, but has diverged a lot :(
|
|
getIncludeArguments(
|
|
libraries: SelectedLibraryVersion[],
|
|
supportedLibraries: Record<string, OptionsHandlerLibrary>,
|
|
dirPath?: string,
|
|
): string[] {
|
|
const includeFlag = '-I';
|
|
|
|
return libraries.flatMap(selectedLib => {
|
|
const foundVersion = this.findLibVersion(selectedLib, supportedLibraries);
|
|
if (!foundVersion) return [];
|
|
|
|
if (foundVersion.packagedheaders && dirPath) {
|
|
const includePath = path.join(dirPath, selectedLib.id, 'include');
|
|
return [includeFlag + includePath];
|
|
}
|
|
|
|
return foundVersion.path.map(path => includeFlag + path);
|
|
});
|
|
}
|
|
|
|
getLibraryOptions(
|
|
libraries: SelectedLibraryVersion[],
|
|
supportedLibraries: Record<string, OptionsHandlerLibrary>,
|
|
): string[] {
|
|
return libraries.flatMap(selectedLib => {
|
|
const foundVersion = this.findLibVersion(selectedLib, supportedLibraries);
|
|
if (!foundVersion) return [];
|
|
|
|
return foundVersion.options;
|
|
});
|
|
}
|
|
|
|
protected getToolExe(compilationInfo: CompilationInfo): string {
|
|
return this.tool.exe;
|
|
}
|
|
|
|
async runTool(
|
|
compilationInfo: CompilationInfo,
|
|
inputFilepath?: string,
|
|
args?: string[],
|
|
stdin?: string,
|
|
supportedLibraries?: Record<string, OptionsHandlerLibrary>,
|
|
dontAppendInputFilepath?: boolean,
|
|
) {
|
|
if (this.tool.name) {
|
|
toolCounter.inc({
|
|
language: compilationInfo.compiler.lang,
|
|
name: this.tool.name,
|
|
});
|
|
}
|
|
const execOptions = compilationInfo.execOptions || this.getDefaultExecOptions();
|
|
if (compilationInfo.preparedLdPaths) execOptions.ldPath = compilationInfo.preparedLdPaths;
|
|
if (inputFilepath) execOptions.customCwd = path.dirname(inputFilepath);
|
|
execOptions.input = stdin;
|
|
|
|
args = args || [];
|
|
if (this.addOptionsToToolArgs) args = this.tool.options.concat(args);
|
|
if (inputFilepath && !dontAppendInputFilepath) args.push(inputFilepath);
|
|
|
|
const toolExe = this.getToolExe(compilationInfo);
|
|
const exeDir = path.dirname(toolExe);
|
|
|
|
try {
|
|
const result = await this.exec(toolExe, args, execOptions);
|
|
return this.convertResult(result, inputFilepath, exeDir);
|
|
} catch (e) {
|
|
logger.error('Error while running tool: ', e);
|
|
return this.createErrorResponse('Error while running tool');
|
|
}
|
|
}
|
|
|
|
convertResult(result: UnprocessedExecResult, inputFilepath?: string, exeDir?: string): ToolResult {
|
|
const transformedFilepath = inputFilepath ? result.filenameTransform(inputFilepath) : undefined;
|
|
return {
|
|
id: this.tool.id,
|
|
name: this.tool.name,
|
|
code: result.code,
|
|
languageId: this.tool.languageId,
|
|
stderr: this.parseOutput(result.stderr, transformedFilepath, exeDir),
|
|
stdout: this.parseOutput(result.stdout, transformedFilepath, exeDir),
|
|
};
|
|
}
|
|
}
|