// 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) { 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, 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[] { 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, 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), }; } }