mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
- latest biome, and fix its configuration - fixes "static" content to be globally configured too (instead of per-line) - fixes issues: - imports fixed up - `Date.now()` vs `+new Date()` - some unused things `_` prefixed After discussion with the team, turned off the unused parameter warning.
350 lines
14 KiB
TypeScript
350 lines
14 KiB
TypeScript
// Copyright (c) 2024, 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 fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
import {splitArguments} from '../../shared/common-utils.js';
|
|
import {
|
|
BuildResult,
|
|
ExecutionOptions,
|
|
ExecutionOptionsWithEnv,
|
|
ExecutionParams,
|
|
} from '../../types/compilation/compilation.interfaces.js';
|
|
import {
|
|
BasicExecutionResult,
|
|
ConfiguredRuntimeTool,
|
|
ConfiguredRuntimeTools,
|
|
ExecutableExecutionOptions,
|
|
RuntimeToolType,
|
|
UnprocessedExecResult,
|
|
} from '../../types/execution/execution.interfaces.js';
|
|
import {addHeaptrackResults} from '../artifact-utils.js';
|
|
import {assert, unwrap} from '../assert.js';
|
|
import {CompilationEnvironment} from '../compilation-env.js';
|
|
import * as exec from '../exec.js';
|
|
import {logger} from '../logger.js';
|
|
import {Packager} from '../packager.js';
|
|
import {propsFor} from '../properties.js';
|
|
import {HeaptrackWrapper} from '../runtime-tools/heaptrack-wrapper.js';
|
|
import * as temp from '../temp.js';
|
|
import * as utils from '../utils.js';
|
|
|
|
import {ExecutablePackageCacheMiss, IExecutionEnvironment} from './execution-env.interfaces.js';
|
|
|
|
export class LocalExecutionEnvironment implements IExecutionEnvironment {
|
|
protected packager: Packager;
|
|
protected dirPath: string;
|
|
protected buildResult: BuildResult | undefined;
|
|
protected environment: CompilationEnvironment;
|
|
protected timeoutMs: number;
|
|
protected sandboxType: string;
|
|
protected useSanitizerEnvHints: boolean;
|
|
protected maxExecOutputSize: number;
|
|
|
|
static get key() {
|
|
return 'local';
|
|
}
|
|
|
|
constructor(environment: CompilationEnvironment) {
|
|
this.environment = environment;
|
|
this.timeoutMs = this.environment.ceProps('binaryExecTimeoutMs', 2000);
|
|
this.maxExecOutputSize = this.environment.ceProps('max-executable-output-size', 32 * 1024);
|
|
|
|
this.useSanitizerEnvHints = true;
|
|
|
|
const execProps = propsFor('execution');
|
|
this.sandboxType = execProps('sandboxType', 'none');
|
|
|
|
this.packager = new Packager();
|
|
this.dirPath = 'not initialized';
|
|
}
|
|
|
|
protected async executableGet(hash: string, destinationFolder: string) {
|
|
const result = await this.environment.executableCache.get(hash);
|
|
if (!result.hit) return null;
|
|
const filepath = destinationFolder + '/' + hash;
|
|
await fs.writeFile(filepath, unwrap(result.data));
|
|
return filepath;
|
|
}
|
|
|
|
protected async loadPackageWithExecutable(hash: string, dirPath: string): Promise<BuildResult> {
|
|
const compilationResultFilename = 'compilation-result.json';
|
|
const startTime = process.hrtime.bigint();
|
|
const outputFilename = await this.executableGet(hash, dirPath);
|
|
if (outputFilename) {
|
|
logger.debug(`Using cached package ${outputFilename}`);
|
|
await this.packager.unpack(outputFilename, dirPath);
|
|
const buildResultsBuf = await fs.readFile(path.join(dirPath, compilationResultFilename));
|
|
const buildResults = JSON.parse(buildResultsBuf.toString('utf8'));
|
|
logger.debug(hash + ' => ' + JSON.stringify(buildResults));
|
|
const endTime = process.hrtime.bigint();
|
|
|
|
let inputFilename = '';
|
|
if (buildResults.inputFilename) {
|
|
inputFilename = path.join(dirPath, path.basename(buildResults.inputFilename));
|
|
}
|
|
|
|
let executableFilename = '';
|
|
if (buildResults.executableFilename) {
|
|
const execPath = utils.maskRootdir(buildResults.executableFilename);
|
|
executableFilename = path.join(dirPath, execPath);
|
|
logger.debug('executableFilename => ' + executableFilename);
|
|
} else {
|
|
logger.error(`No executableFilename provided for package ${hash}`);
|
|
}
|
|
|
|
return Object.assign({}, buildResults, {
|
|
code: 0,
|
|
inputFilename: inputFilename,
|
|
dirPath: dirPath,
|
|
executableFilename: executableFilename,
|
|
packageDownloadAndUnzipTime: utils.deltaTimeNanoToMili(startTime, endTime),
|
|
});
|
|
}
|
|
throw new ExecutablePackageCacheMiss('Tried to get executable from cache, but got a cache miss');
|
|
}
|
|
|
|
async downloadExecutablePackage(hash: string): Promise<void> {
|
|
this.dirPath = await temp.mkdir(utils.ce_temp_prefix);
|
|
|
|
this.buildResult = await this.loadPackageWithExecutable(hash, this.dirPath);
|
|
}
|
|
|
|
protected getDefaultExecOptions(params: ExecutionParams): ExecutionOptionsWithEnv {
|
|
const env: Record<string, string> = {};
|
|
env.PATH = '';
|
|
|
|
if (params.runtimeTools) {
|
|
const runtimeEnv = params.runtimeTools.find(tool => tool.name === RuntimeToolType.env);
|
|
if (runtimeEnv) {
|
|
for (const opt of runtimeEnv.options) {
|
|
env[opt.name] = opt.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.buildResult?.defaultExecOptions?.env?.PATH) {
|
|
if (env.PATH.length > 0)
|
|
env.PATH = env.PATH + path.delimiter + this.buildResult.defaultExecOptions.env.PATH;
|
|
else env.PATH = this.buildResult.defaultExecOptions.env.PATH;
|
|
}
|
|
|
|
let extraLdPaths: string[] = [];
|
|
if (env.LD_LIBRARY_PATH) {
|
|
extraLdPaths = env.LD_LIBRARY_PATH.split(path.delimiter);
|
|
delete env.LD_LIBRARY_PATH;
|
|
}
|
|
|
|
const execOptions: ExecutionOptionsWithEnv = {
|
|
env,
|
|
};
|
|
|
|
if (this.buildResult?.preparedLdPaths) {
|
|
execOptions.ldPath = this.buildResult.preparedLdPaths.concat(extraLdPaths);
|
|
} else {
|
|
execOptions.ldPath = extraLdPaths;
|
|
}
|
|
|
|
return execOptions;
|
|
}
|
|
|
|
async execute(params: ExecutionParams): Promise<BasicExecutionResult> {
|
|
assert(this.buildResult);
|
|
assert(this.dirPath !== 'not initialized');
|
|
|
|
const execExecutableOptions: ExecutableExecutionOptions = {
|
|
args: typeof params.args === 'string' ? splitArguments(params.args) : params.args || [],
|
|
stdin: params.stdin || '',
|
|
ldPath: this.buildResult.preparedLdPaths || [],
|
|
env: {},
|
|
runtimeTools: params.runtimeTools,
|
|
};
|
|
|
|
// note: this is for a small transition period only, can be removed after a few days
|
|
const file = utils.maskRootdir(this.buildResult.executableFilename);
|
|
assert(file !== '', 'Internal error, no executableFilename available');
|
|
|
|
return await this.execBinary(file, execExecutableOptions, this.dirPath);
|
|
}
|
|
|
|
static setEnvironmentVariablesFromRuntime(configuredTools: ConfiguredRuntimeTools, execOptions: ExecutionOptions) {
|
|
for (const runtime of configuredTools) {
|
|
if (runtime.name === RuntimeToolType.env) {
|
|
for (const env of runtime.options) {
|
|
if (!execOptions.env) execOptions.env = {};
|
|
|
|
execOptions.env[env.name] = env.value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async execBinary(
|
|
executable: string,
|
|
executeParameters: ExecutableExecutionOptions,
|
|
homeDir: string,
|
|
extraConfiguration?: any,
|
|
): Promise<BasicExecutionResult> {
|
|
try {
|
|
const execOptions: ExecutionOptions = {
|
|
maxOutput: this.maxExecOutputSize,
|
|
timeoutMs: this.timeoutMs,
|
|
ldPath: [...executeParameters.ldPath],
|
|
input: executeParameters.stdin,
|
|
customCwd: homeDir,
|
|
appHome: homeDir,
|
|
};
|
|
|
|
if (this.useSanitizerEnvHints) {
|
|
execOptions.env = {
|
|
ASAN_OPTIONS: 'color=always',
|
|
UBSAN_OPTIONS: 'color=always',
|
|
MSAN_OPTIONS: 'color=always',
|
|
LSAN_OPTIONS: 'color=always',
|
|
...executeParameters.env,
|
|
};
|
|
} else {
|
|
execOptions.env = {
|
|
...executeParameters.env,
|
|
};
|
|
}
|
|
|
|
return this.execBinaryMaybeWrapped(
|
|
executable,
|
|
executeParameters.args as string[],
|
|
execOptions,
|
|
executeParameters,
|
|
homeDir,
|
|
);
|
|
} catch (err: any) {
|
|
if (err.code && err.stderr) {
|
|
return utils.processExecutionResult(err);
|
|
}
|
|
return {
|
|
...utils.getEmptyExecutionResult(),
|
|
stdout: err.stdout ? utils.parseOutput(err.stdout) : [],
|
|
stderr: err.stderr ? utils.parseOutput(err.stderr) : [],
|
|
code: err.code === undefined ? -1 : err.code,
|
|
};
|
|
}
|
|
}
|
|
|
|
protected async execBinaryMaybeWrapped(
|
|
executable: string,
|
|
args: string[],
|
|
execOptions: ExecutionOptions,
|
|
executeParameters: ExecutableExecutionOptions,
|
|
homeDir: string,
|
|
): Promise<BasicExecutionResult> {
|
|
let runWithHeaptrack: ConfiguredRuntimeTool | undefined;
|
|
let runWithLibSegFault: ConfiguredRuntimeTool | undefined;
|
|
const lineParseOptions: Set<utils.LineParseOption> = new Set<utils.LineParseOption>();
|
|
|
|
if (!execOptions.env) execOptions.env = {};
|
|
|
|
if (executeParameters.runtimeTools) {
|
|
LocalExecutionEnvironment.setEnvironmentVariablesFromRuntime(executeParameters.runtimeTools, execOptions);
|
|
|
|
for (const runtime of executeParameters.runtimeTools) {
|
|
if (runtime.name === RuntimeToolType.heaptrack) {
|
|
runWithHeaptrack = runtime;
|
|
} else if (runtime.name === RuntimeToolType.libsegfault) {
|
|
runWithLibSegFault = runtime;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (runWithLibSegFault) {
|
|
lineParseOptions.add(utils.LineParseOption.AtFileLine);
|
|
|
|
const libSegFaultPath = this.environment.ceProps('libSegFaultPath', '/usr/lib');
|
|
const preloadSo = path.join(libSegFaultPath, 'libSegFault.so');
|
|
const tracer = path.join(libSegFaultPath, 'tracer');
|
|
|
|
if (execOptions.env.LD_PRELOAD) {
|
|
execOptions.env.LD_PRELOAD = preloadSo + ':' + execOptions.env.LD_PRELOAD;
|
|
} else {
|
|
execOptions.env.LD_PRELOAD = preloadSo;
|
|
}
|
|
|
|
execOptions.env.LIBSEGFAULT_TRACER = tracer;
|
|
|
|
for (const opt of runWithLibSegFault.options) {
|
|
if (opt.name === 'registers' && opt.value === 'yes') {
|
|
execOptions.env.LIBSEGFAULT_REGISTERS = '1';
|
|
} else if (opt.name === 'memory' && opt.value === 'yes') {
|
|
execOptions.env.LIBSEGFAULT_MEMORY = '1';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (runWithHeaptrack && HeaptrackWrapper.isSupported(this.environment)) {
|
|
lineParseOptions.add(utils.LineParseOption.AtFileLine);
|
|
|
|
const wrapper = new HeaptrackWrapper(
|
|
homeDir,
|
|
exec.sandbox,
|
|
exec.execute,
|
|
runWithHeaptrack.options,
|
|
this.environment.ceProps,
|
|
this.sandboxType,
|
|
);
|
|
const execResult: UnprocessedExecResult = await wrapper.exec(executable, args, execOptions);
|
|
const processed = this.processUserExecutableExecutionResult(
|
|
execResult,
|
|
Array.from(lineParseOptions.values()),
|
|
);
|
|
|
|
if (executeParameters.runtimeTools) {
|
|
for (const runtime of executeParameters.runtimeTools) {
|
|
if (runtime.name === RuntimeToolType.heaptrack) {
|
|
await addHeaptrackResults(processed, homeDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
return processed;
|
|
}
|
|
const execResult: UnprocessedExecResult = await exec.sandbox(executable, args, execOptions);
|
|
return this.processUserExecutableExecutionResult(execResult, Array.from(lineParseOptions.values()));
|
|
}
|
|
|
|
processUserExecutableExecutionResult(
|
|
input: UnprocessedExecResult,
|
|
stdErrlineParseOptions: utils.LineParseOptions,
|
|
): BasicExecutionResult {
|
|
const start = performance.now();
|
|
const stdout = utils.parseOutput(input.stdout, undefined, undefined, []);
|
|
const stderr = utils.parseOutput(input.stderr, undefined, undefined, stdErrlineParseOptions);
|
|
const end = performance.now();
|
|
return {
|
|
...input,
|
|
stdout,
|
|
stderr,
|
|
processExecutionResultTime: end - start,
|
|
};
|
|
}
|
|
}
|