import { Logger } from "@perf-profiler/logger"; import { execSync, spawn, ChildProcess, SpawnSyncReturns } from "child_process"; export const executeCommand = (command: string): string => { try { return execSync(command, { stdio: "pipe" }).toString(); } catch (error: unknown) { // The Error object will contain the entire result from child_process.spawnSync() // (source: https://nodejs.org/api/child_process.html#child_processexecsynccommand-options) Logger.debug( `Error while executing command "${command}": ${( error as SpawnSyncReturns<{ toString(): string }> ).stderr.toString()}` ); throw error; } }; const childProcesses: ChildProcess[] = []; export const cleanup = () => { childProcesses.forEach((child) => { child.kill(); }); }; const exit = () => { cleanup(); process.exit(); }; declare const global: { Flipper: unknown; }; if (!global.Flipper) { process.on("SIGINT", exit); // CTRL+C process.on("SIGQUIT", exit); // Keyboard quit process.on("SIGTERM", exit); // `kill` command } class AsyncExecutionError extends Error {} /** * In AWS when we properly kill the process termination gets logged in stderr with a weird log */ export const canIgnoreAwsTerminationError = (log: string) => log.includes("Terminated LD_LIBRARY_PATH"); export const executeAsync = ( command: string, { logStderr } = { logStderr: true, } ): ChildProcess => { const parts = command.split(" "); const childProcess = spawn(parts[0], parts.slice(1)); childProcess.stdout?.on("end", () => { Logger.debug(`Process for ${command} ended`); }); childProcess.stderr?.on("data", (data) => { if (logStderr && !canIgnoreAwsTerminationError(data.toString())) Logger.error(`Process for ${command} errored with ${data.toString()}`); }); childProcess.on("close", (code) => { Logger.debug(`child process exited with code ${code}`); const AUTHORIZED_CODES = [ 0, // Success 130, // SIGINT 140, // SIGKILL 143, // SIGTERM 255, // SSH EXECUTION STOPPED ]; // SIGKILL or SIGTERM are likely to be normal, since we request termination from JS side if (code && !AUTHORIZED_CODES.includes(code)) { throw new AsyncExecutionError(`Process for ${command} exited with code ${code}`); } }); childProcess.on("error", (err) => { Logger.error(`Process for ${command} errored with ${err}`); }); childProcesses.push(childProcess); return childProcess; }; export const executeLongRunningProcess = ( command: string, delimiter: string, onData: (data: string) => void ) => { const process = executeAsync(command, { logStderr: false, }); let currentChunk = ""; process.stdout?.on("data", (data: ReadableStream) => { currentChunk += data.toString(); const dataSplits = currentChunk.split(delimiter); dataSplits.slice(0, -1).forEach((split) => { onData(split.trim()); }); if (dataSplits.length > 0) { currentChunk = currentChunk.slice( currentChunk.length - 1 * dataSplits[dataSplits.length - 1].length ); } }); return process; };