symbolicate.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env node
  2. /**
  3. * Copyright (c) Facebook, Inc. and its affiliates.
  4. *
  5. * This source code is licensed under the MIT license found in the
  6. * LICENSE file in the root directory of this source tree.
  7. *
  8. * @flow strict-local
  9. * @format
  10. */
  11. // Symbolicates a JavaScript stack trace using a source map.
  12. // In our first form, we read a stack trace from stdin and symbolicate it via
  13. // the provided source map.
  14. // In our second form, we symbolicate using an explicit line number, and
  15. // optionally a column.
  16. // In our third form, we symbolicate using a module ID, a line number, and
  17. // optionally a column.
  18. 'use strict';
  19. // flowlint-next-line untyped-import:off
  20. const SourceMapConsumer = require('source-map').SourceMapConsumer;
  21. const Symbolication = require('./Symbolication.js');
  22. const fs = require('fs');
  23. // flowlint-next-line untyped-import:off
  24. const through2 = require('through2');
  25. async function main(
  26. argvInput: Array<string> = process.argv.slice(2),
  27. {
  28. stdin,
  29. stderr,
  30. stdout,
  31. }: {
  32. stdin: stream$Readable | tty$ReadStream,
  33. stderr: stream$Writable,
  34. stdout: stream$Writable,
  35. ...
  36. } = process,
  37. ): Promise<number> {
  38. const argv = argvInput.slice();
  39. function checkAndRemoveArg(arg, valuesPerArg = 0) {
  40. let values = null;
  41. for (let idx = argv.indexOf(arg); idx !== -1; idx = argv.indexOf(arg)) {
  42. argv.splice(idx, 1);
  43. values = values || [];
  44. values.push(argv.splice(idx, valuesPerArg));
  45. }
  46. return values;
  47. }
  48. function checkAndRemoveArgWithValue(arg) {
  49. const values = checkAndRemoveArg(arg, 1);
  50. return values ? values[0][0] : null;
  51. }
  52. try {
  53. const noFunctionNames = checkAndRemoveArg('--no-function-names');
  54. const isHermesCrash = checkAndRemoveArg('--hermes-crash');
  55. const inputLineStart = Number.parseInt(
  56. checkAndRemoveArgWithValue('--input-line-start') || '1',
  57. 10,
  58. );
  59. const inputColumnStart = Number.parseInt(
  60. checkAndRemoveArgWithValue('--input-column-start') || '0',
  61. 10,
  62. );
  63. const outputLineStart = Number.parseInt(
  64. checkAndRemoveArgWithValue('--output-line-start') || '1',
  65. 10,
  66. );
  67. const outputColumnStart = Number.parseInt(
  68. checkAndRemoveArgWithValue('--output-column-start') || '0',
  69. 10,
  70. );
  71. if (argv.length < 1 || argv.length > 4) {
  72. /* eslint no-path-concat: "off" */
  73. const usages = [
  74. 'Usage: ' + __filename + ' <source-map-file>',
  75. ' ' + __filename + ' <source-map-file> <line> [column]',
  76. ' ' +
  77. __filename +
  78. ' <source-map-file> <moduleId>.js <line> [column]',
  79. ' ' + __filename + ' <source-map-file> <mapfile>.profmap',
  80. ' ' +
  81. __filename +
  82. ' <source-map-file> --attribution < in.jsonl > out.jsonl',
  83. ' ' + __filename + ' <source-map-file> <tracefile>.cpuprofile',
  84. ' Optional flags:',
  85. ' --no-function-names',
  86. ' --hermes-crash',
  87. ' --input-line-start <line> (default: 1)',
  88. ' --input-column-start <column> (default: 0)',
  89. ' --output-line-start <line> (default: 1)',
  90. ' --output-column-start <column> (default: 0)',
  91. ];
  92. console.error(usages.join('\n'));
  93. return 1;
  94. }
  95. // Read the source map.
  96. const sourceMapFileName = argv.shift();
  97. const options = {
  98. nameSource: noFunctionNames ? 'identifier_names' : 'function_names',
  99. inputLineStart,
  100. inputColumnStart,
  101. outputLineStart,
  102. outputColumnStart,
  103. };
  104. let context;
  105. if (fs.lstatSync(sourceMapFileName).isDirectory()) {
  106. context = Symbolication.unstable_createDirectoryContext(
  107. SourceMapConsumer,
  108. sourceMapFileName,
  109. options,
  110. );
  111. } else {
  112. const content = fs.readFileSync(sourceMapFileName, 'utf8');
  113. context = Symbolication.createContext(
  114. SourceMapConsumer,
  115. content,
  116. options,
  117. );
  118. }
  119. if (argv.length === 0) {
  120. const stackTrace = await readAll(stdin);
  121. if (isHermesCrash) {
  122. const stackTraceJSON = JSON.parse(stackTrace);
  123. const symbolicatedTrace = context.symbolicateHermesMinidumpTrace(
  124. stackTraceJSON,
  125. );
  126. stdout.write(JSON.stringify(symbolicatedTrace));
  127. } else {
  128. stdout.write(context.symbolicate(stackTrace));
  129. }
  130. } else if (argv[0].endsWith('.profmap')) {
  131. stdout.write(context.symbolicateProfilerMap(argv[0]));
  132. } else if (argv[0] === '--attribution') {
  133. let buffer = '';
  134. await waitForStream(
  135. stdin
  136. .pipe(
  137. through2(function(data, enc, callback) {
  138. // Take arbitrary strings, output single lines
  139. buffer += data;
  140. const lines = buffer.split('\n');
  141. for (let i = 0, e = lines.length - 1; i < e; i++) {
  142. this.push(lines[i]);
  143. }
  144. buffer = lines[lines.length - 1];
  145. callback();
  146. }),
  147. )
  148. .pipe(
  149. through2.obj(function(data, enc, callback) {
  150. // This is JSONL, so each line is a separate JSON object
  151. const obj = JSON.parse(data);
  152. context.symbolicateAttribution(obj);
  153. this.push(JSON.stringify(obj) + '\n');
  154. callback();
  155. }),
  156. )
  157. .pipe(stdout),
  158. );
  159. } else if (argv[0].endsWith('.cpuprofile')) {
  160. // NOTE: synchronous
  161. context.symbolicateChromeTrace(argv[0], {stdout, stderr});
  162. } else {
  163. // read-from-argv form.
  164. let moduleIds;
  165. if (argv[0].endsWith('.js')) {
  166. moduleIds = context.parseFileName(argv[0]);
  167. argv.shift();
  168. } else {
  169. moduleIds = null;
  170. }
  171. const lineNumber = argv.shift();
  172. const columnNumber = argv.shift() || 0;
  173. const original = context.getOriginalPositionFor(
  174. +lineNumber,
  175. +columnNumber,
  176. // $FlowFixMe context is a union here and so this parameter is a union
  177. moduleIds,
  178. );
  179. stdout.write(
  180. [
  181. original.source ?? 'null',
  182. original.line ?? 'null',
  183. original.name ?? 'null',
  184. ].join(':') + '\n',
  185. );
  186. }
  187. } catch (error) {
  188. stderr.write(error + '\n');
  189. return 1;
  190. }
  191. return 0;
  192. }
  193. function readAll(stream) {
  194. return new Promise(resolve => {
  195. let data = '';
  196. if (stream.isTTY === true) {
  197. resolve(data);
  198. return;
  199. }
  200. stream.setEncoding('utf8');
  201. stream.on('readable', () => {
  202. let chunk;
  203. // flowlint-next-line sketchy-null-string:off
  204. while ((chunk = stream.read())) {
  205. data += chunk.toString();
  206. }
  207. });
  208. stream.on('end', () => {
  209. resolve(data);
  210. });
  211. });
  212. }
  213. function waitForStream(stream) {
  214. return new Promise(resolve => {
  215. stream.on('finish', resolve);
  216. });
  217. }
  218. if (require.main === module) {
  219. main().then(code => process.exit(code));
  220. }
  221. module.exports = main;