/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format * @oncall react_native */ // Symbolicates a JavaScript stack trace using a source map. // In our first form, we read a stack trace from stdin and symbolicate it via // the provided source map. // In our second form, we symbolicate using an explicit line number, and // optionally a column. // In our third form, we symbolicate using a module ID, a line number, and // optionally a column. import * as Symbolication from './Symbolication'; import fs from 'fs'; // $FlowFixMe[untyped-import] source-map import {SourceMapConsumer} from 'source-map'; import {Transform} from 'stream'; function printHelp() { const usages = [ 'Usage: ' + __filename + ' ', ' ' + __filename + ' [column]', ' ' + __filename + ' .js [column]', ' ' + __filename + ' .profmap', ' ' + __filename + ' --attribution < in.jsonl > out.jsonl', ' ' + __filename + ' .cpuprofile', ' Optional flags:', ' --no-function-names', ' --hermes-crash (mutually exclusive with --hermes-coverage)', ' --hermes-coverage (mutually exclusive with --hermes-crash)', ' --input-line-start (default: 1)', ' --input-column-start (default: 0)', ' --output-line-start (default: 1)', ' --output-column-start (default: 0)', ]; console.error(usages.join('\n')); } export default async function main( argvInput: Array = process.argv.slice(2), { stdin, stderr, stdout, }: $ReadOnly<{ stdin: stream$Readable | tty$ReadStream, stderr: stream$Writable, stdout: stream$Writable, ... }> = (process: $FlowFixMe), ): Promise { const argv = argvInput.slice(); function checkAndRemoveArg(arg: string, valuesPerArg: number = 0) { let values: null | Array> = null; for (let idx = argv.indexOf(arg); idx !== -1; idx = argv.indexOf(arg)) { argv.splice(idx, 1); values = values || []; values.push(argv.splice(idx, valuesPerArg)); } return values; } function checkAndRemoveArgWithValue(arg: string) { const values = checkAndRemoveArg(arg, 1); return values ? values[0][0] : null; } try { const noFunctionNames = checkAndRemoveArg('--no-function-names'); const isHermesCrash = checkAndRemoveArg('--hermes-crash'); const isCoverage = checkAndRemoveArg('--hermes-coverage'); const inputLineStart = Number.parseInt( checkAndRemoveArgWithValue('--input-line-start') || '1', 10, ); const inputColumnStart = Number.parseInt( checkAndRemoveArgWithValue('--input-column-start') || '0', 10, ); const outputLineStart = Number.parseInt( checkAndRemoveArgWithValue('--output-line-start') || '1', 10, ); const outputColumnStart = Number.parseInt( checkAndRemoveArgWithValue('--output-column-start') || '0', 10, ); if (argv.length < 1 || argv.length > 4) { /* eslint no-path-concat: "off" */ printHelp(); return 1; } if (isHermesCrash && isCoverage) { console.error( 'Pass either --hermes-crash or --hermes-coverage, not both', ); printHelp(); return 1; } // Read the source map. const sourceMapFileName = argv.shift(); const options = { nameSource: noFunctionNames ? 'identifier_names' : 'function_names', inputLineStart, inputColumnStart, outputLineStart, outputColumnStart, }; let context; // $FlowFixMe[incompatible-type] if (fs.lstatSync(sourceMapFileName).isDirectory()) { context = Symbolication.unstable_createDirectoryContext( SourceMapConsumer, // $FlowFixMe[incompatible-type] sourceMapFileName, /* $FlowFixMe[incompatible-type] Natural Inference rollout. See * https://fburl.com/workplace/6291gfvu */ options, ); } else { // $FlowFixMe[incompatible-type] const content = fs.readFileSync(sourceMapFileName, 'utf8'); context = Symbolication.createContext( SourceMapConsumer, content, /* $FlowFixMe[incompatible-type] Natural Inference rollout. See * https://fburl.com/workplace/6291gfvu */ options, ); } if (argv.length === 0) { const stackTrace = await readAll(stdin); if (isHermesCrash) { const stackTraceJSON = JSON.parse(stackTrace); const symbolicatedTrace = context.symbolicateHermesMinidumpTrace(stackTraceJSON); stdout.write(JSON.stringify(symbolicatedTrace)); } else if (isCoverage) { const stackTraceJSON = JSON.parse(stackTrace); const symbolicatedTrace = context.symbolicateHermesCoverageTrace(stackTraceJSON); stdout.write(JSON.stringify(symbolicatedTrace)); } else { stdout.write(context.symbolicate(stackTrace)); } } else if (argv[0].endsWith('.profmap')) { stdout.write(context.symbolicateProfilerMap(argv[0])); } else if ( argv[0].endsWith('.heapsnapshot') || argv[0].endsWith('.heaptimeline') ) { stdout.write( JSON.stringify( context.symbolicateHeapSnapshot(fs.readFileSync(argv[0], 'utf8')), ), ); } else if (argv[0] === '--attribution') { let lineBuffer = ''; const streamToLines: Transform = new Transform({ transform(data, _enc, callback) { // Take arbitrary strings, output single lines lineBuffer += data.toString(); const lines = lineBuffer.split('\n'); for (let i = 0, e = lines.length - 1; i < e; i++) { streamToLines.push(lines[i]); } lineBuffer = lines[lines.length - 1]; callback(); }, }); const symbolicateLines: Transform = new Transform({ transform(data, enc, callback) { // This is JSONL, so each line is a separate JSON object const obj = JSON.parse(data.toString()); context.symbolicateAttribution(obj); symbolicateLines.push(JSON.stringify(obj) + '\n'); callback(); }, objectMode: true, }); await waitForStream( stdin.pipe(streamToLines).pipe(symbolicateLines).pipe(stdout), ); } else if (argv[0].endsWith('.cpuprofile')) { // NOTE: synchronous context.symbolicateChromeTrace(argv[0], {stdout, stderr}); } else { // read-from-argv form. let moduleIds; if (argv[0].endsWith('.js')) { moduleIds = context.parseFileName(argv[0]); argv.shift(); } else { moduleIds = null; } const lineNumber = argv.shift(); const columnNumber = argv.shift() || 0; const original = context.getOriginalPositionFor( +lineNumber, +columnNumber, // $FlowFixMe[incompatible-type] context is a union here and so this parameter is a union moduleIds, ); stdout.write( [ original.source ?? 'null', original.line ?? 'null', original.name ?? 'null', ].join(':') + '\n', ); } } catch (error) { stderr.write(error + '\n'); return 1; } return 0; } function readAll(stream: stream$Readable | tty$ReadStream) { return new Promise(resolve => { let data = ''; if (stream.isTTY === true) { resolve(data); return; } stream.setEncoding('utf8'); stream.on('readable', () => { let chunk; // flowlint-next-line sketchy-null-string:off while ((chunk = stream.read())) { data += chunk.toString(); } }); stream.on('end', () => { resolve(data); }); }); } function waitForStream(stream: $FlowFixMe) { return new Promise(resolve => { stream.on('finish', resolve); }); }