1'use strict';
2const assert = require('assert');
3const fs = require('fs');
4const net = require('net');
5const os = require('os');
6const path = require('path');
7const util = require('util');
8const cpus = os.cpus();
9
10function findReports(pid, dir) {
11  // Default filenames are of the form
12  // report.<date>.<time>.<pid>.<tid>.<seq>.json
13  const format = '^report\\.\\d+\\.\\d+\\.' + pid + '\\.\\d+\\.\\d+\\.json$';
14  const filePattern = new RegExp(format);
15  const files = fs.readdirSync(dir);
16  const results = [];
17
18  files.forEach((file) => {
19    if (filePattern.test(file))
20      results.push(path.join(dir, file));
21  });
22
23  return results;
24}
25
26function validate(filepath, fields) {
27  const report = fs.readFileSync(filepath, 'utf8');
28  if (process.report.compact) {
29    const end = report.indexOf('\n');
30    assert.strictEqual(end, report.length - 1);
31  }
32  validateContent(JSON.parse(report), fields);
33}
34
35function validateContent(report, fields = []) {
36  if (typeof report === 'string') {
37    try {
38      report = JSON.parse(report);
39    } catch {
40      throw new TypeError(
41        'validateContent() expects a JSON string or JavaScript Object');
42    }
43  }
44  try {
45    _validateContent(report, fields);
46  } catch (err) {
47    try {
48      err.stack += util.format('\n------\nFailing Report:\n%O', report);
49    } catch {
50      // Continue regardless of error.
51    }
52    throw err;
53  }
54}
55
56function _validateContent(report, fields = []) {
57  const isWindows = process.platform === 'win32';
58  const isJavaScriptThreadReport = report.javascriptHeap != null;
59
60  // Verify that all sections are present as own properties of the report.
61  const sections = ['header', 'nativeStack', 'javascriptStack', 'libuv',
62                    'environmentVariables', 'sharedObjects', 'resourceUsage', 'workers'];
63  if (!isWindows)
64    sections.push('userLimits');
65
66  if (report.uvthreadResourceUsage)
67    sections.push('uvthreadResourceUsage');
68
69  if (isJavaScriptThreadReport)
70    sections.push('javascriptHeap');
71
72  checkForUnknownFields(report, sections);
73  sections.forEach((section) => {
74    assert(Object.hasOwn(report, section));
75    assert(typeof report[section] === 'object' && report[section] !== null);
76  });
77
78  fields.forEach((field) => {
79    function checkLoop(actual, rest, expect) {
80      actual = actual[rest.shift()];
81      if (rest.length === 0 && actual !== undefined) {
82        assert.strictEqual(actual, expect);
83      } else {
84        assert(actual);
85        checkLoop(actual, rest, expect);
86      }
87    }
88    let actual, expect;
89    if (Array.isArray(field)) {
90      [actual, expect] = field;
91    } else {
92      actual = field;
93      expect = undefined;
94    }
95    checkLoop(report, actual.split('.'), expect);
96  });
97
98  // Verify the format of the header section.
99  const header = report.header;
100  const headerFields = ['event', 'trigger', 'filename', 'dumpEventTime',
101                        'dumpEventTimeStamp', 'processId', 'commandLine',
102                        'nodejsVersion', 'wordSize', 'arch', 'platform',
103                        'componentVersions', 'release', 'osName', 'osRelease',
104                        'osVersion', 'osMachine', 'cpus', 'host',
105                        'glibcVersionRuntime', 'glibcVersionCompiler', 'cwd',
106                        'reportVersion', 'networkInterfaces', 'threadId'];
107  checkForUnknownFields(header, headerFields);
108  assert.strictEqual(header.reportVersion, 3);  // Increment as needed.
109  assert.strictEqual(typeof header.event, 'string');
110  assert.strictEqual(typeof header.trigger, 'string');
111  assert(typeof header.filename === 'string' || header.filename === null);
112  assert.notStrictEqual(new Date(header.dumpEventTime).toString(),
113                        'Invalid Date');
114  assert(String(+header.dumpEventTimeStamp), header.dumpEventTimeStamp);
115  assert(Number.isSafeInteger(header.processId));
116  assert(Number.isSafeInteger(header.threadId) || header.threadId === null);
117  assert.strictEqual(typeof header.cwd, 'string');
118  assert(Array.isArray(header.commandLine));
119  header.commandLine.forEach((arg) => {
120    assert.strictEqual(typeof arg, 'string');
121  });
122  assert.strictEqual(header.nodejsVersion, process.version);
123  assert(Number.isSafeInteger(header.wordSize));
124  assert.strictEqual(header.arch, os.arch());
125  assert.strictEqual(header.platform, os.platform());
126  assert.deepStrictEqual(header.componentVersions, process.versions);
127  assert.deepStrictEqual(header.release, process.release);
128  assert.strictEqual(header.osName, os.type());
129  assert.strictEqual(header.osRelease, os.release());
130  assert.strictEqual(typeof header.osVersion, 'string');
131  assert.strictEqual(typeof header.osMachine, 'string');
132  assert(Array.isArray(header.cpus));
133  assert.strictEqual(header.cpus.length, cpus.length);
134  header.cpus.forEach((cpu) => {
135    assert.strictEqual(typeof cpu.model, 'string');
136    assert.strictEqual(typeof cpu.speed, 'number');
137    assert.strictEqual(typeof cpu.user, 'number');
138    assert.strictEqual(typeof cpu.nice, 'number');
139    assert.strictEqual(typeof cpu.sys, 'number');
140    assert.strictEqual(typeof cpu.idle, 'number');
141    assert.strictEqual(typeof cpu.irq, 'number');
142    assert(cpus.some((c) => {
143      return c.model === cpu.model;
144    }));
145  });
146
147  assert(Array.isArray(header.networkInterfaces));
148  header.networkInterfaces.forEach((iface) => {
149    assert.strictEqual(typeof iface.name, 'string');
150    assert.strictEqual(typeof iface.internal, 'boolean');
151    assert.match(iface.mac, /^([0-9A-F][0-9A-F]:){5}[0-9A-F]{2}$/i);
152
153    if (iface.family === 'IPv4') {
154      assert.strictEqual(net.isIPv4(iface.address), true);
155      assert.strictEqual(net.isIPv4(iface.netmask), true);
156      assert.strictEqual(iface.scopeid, undefined);
157    } else if (iface.family === 'IPv6') {
158      assert.strictEqual(net.isIPv6(iface.address), true);
159      assert.strictEqual(net.isIPv6(iface.netmask), true);
160      assert(Number.isInteger(iface.scopeid));
161    } else {
162      assert.strictEqual(iface.family, 'unknown');
163      assert.strictEqual(iface.address, undefined);
164      assert.strictEqual(iface.netmask, undefined);
165      assert.strictEqual(iface.scopeid, undefined);
166    }
167  });
168  assert.strictEqual(header.host, os.hostname());
169
170  // Verify the format of the nativeStack section.
171  assert(Array.isArray(report.nativeStack));
172  report.nativeStack.forEach((frame) => {
173    assert(typeof frame === 'object' && frame !== null);
174    checkForUnknownFields(frame, ['pc', 'symbol']);
175    assert.strictEqual(typeof frame.pc, 'string');
176    assert.match(frame.pc, /^0x[0-9a-f]+$/);
177    assert.strictEqual(typeof frame.symbol, 'string');
178  });
179
180  if (isJavaScriptThreadReport) {
181    // Verify the format of the javascriptStack section.
182    checkForUnknownFields(report.javascriptStack,
183                          ['message', 'stack', 'errorProperties']);
184    assert.strictEqual(typeof report.javascriptStack.errorProperties,
185                       'object');
186    assert.strictEqual(typeof report.javascriptStack.message, 'string');
187    if (report.javascriptStack.stack !== undefined) {
188      assert(Array.isArray(report.javascriptStack.stack));
189      report.javascriptStack.stack.forEach((frame) => {
190        assert.strictEqual(typeof frame, 'string');
191      });
192    }
193
194    // Verify the format of the javascriptHeap section.
195    const heap = report.javascriptHeap;
196    // See `PrintGCStatistics` in node_report.cc
197    const jsHeapFields = [
198      'totalMemory',
199      'executableMemory',
200      'totalCommittedMemory',
201      'availableMemory',
202      'totalGlobalHandlesMemory',
203      'usedGlobalHandlesMemory',
204      'usedMemory',
205      'memoryLimit',
206      'mallocedMemory',
207      'externalMemory',
208      'peakMallocedMemory',
209      'nativeContextCount',
210      'detachedContextCount',
211      'doesZapGarbage',
212      'heapSpaces',
213    ];
214    checkForUnknownFields(heap, jsHeapFields);
215    // Do not check `heapSpaces` here
216    for (let i = 0; i < jsHeapFields.length - 1; i++) {
217      assert(
218        Number.isSafeInteger(heap[jsHeapFields[i]]),
219        `heap.${jsHeapFields[i]} is not a safe integer`,
220      );
221    }
222    assert(typeof heap.heapSpaces === 'object' && heap.heapSpaces !== null);
223    const heapSpaceFields = ['memorySize', 'committedMemory', 'capacity',
224                             'used', 'available'];
225    Object.keys(heap.heapSpaces).forEach((spaceName) => {
226      const space = heap.heapSpaces[spaceName];
227      checkForUnknownFields(space, heapSpaceFields);
228      heapSpaceFields.forEach((field) => {
229        assert(Number.isSafeInteger(space[field]));
230      });
231    });
232  }
233
234  // Verify the format of the resourceUsage section.
235  const usage = { ...report.resourceUsage };
236  // Delete it, otherwise checkForUnknownFields will throw error
237  delete usage.constrained_memory;
238  const resourceUsageFields = ['userCpuSeconds', 'kernelCpuSeconds',
239                               'cpuConsumptionPercent', 'userCpuConsumptionPercent',
240                               'kernelCpuConsumptionPercent',
241                               'maxRss', 'rss', 'free_memory', 'total_memory',
242                               'available_memory', 'pageFaults', 'fsActivity'];
243  checkForUnknownFields(usage, resourceUsageFields);
244  assert.strictEqual(typeof usage.userCpuSeconds, 'number');
245  assert.strictEqual(typeof usage.kernelCpuSeconds, 'number');
246  assert.strictEqual(typeof usage.cpuConsumptionPercent, 'number');
247  assert.strictEqual(typeof usage.userCpuConsumptionPercent, 'number');
248  assert.strictEqual(typeof usage.kernelCpuConsumptionPercent, 'number');
249  assert(typeof usage.rss, 'string');
250  assert(typeof usage.maxRss, 'string');
251  assert(typeof usage.free_memory, 'string');
252  assert(typeof usage.total_memory, 'string');
253  assert(typeof usage.available_memory, 'string');
254  // This field may not exsit
255  if (report.resourceUsage.constrained_memory) {
256    assert(typeof report.resourceUsage.constrained_memory, 'string');
257  }
258  assert(typeof usage.pageFaults === 'object' && usage.pageFaults !== null);
259  checkForUnknownFields(usage.pageFaults, ['IORequired', 'IONotRequired']);
260  assert(Number.isSafeInteger(usage.pageFaults.IORequired));
261  assert(Number.isSafeInteger(usage.pageFaults.IONotRequired));
262  assert(typeof usage.fsActivity === 'object' && usage.fsActivity !== null);
263  checkForUnknownFields(usage.fsActivity, ['reads', 'writes']);
264  assert(Number.isSafeInteger(usage.fsActivity.reads));
265  assert(Number.isSafeInteger(usage.fsActivity.writes));
266
267  // Verify the format of the uvthreadResourceUsage section, if present.
268  if (report.uvthreadResourceUsage) {
269    const usage = report.uvthreadResourceUsage;
270    const threadUsageFields = ['userCpuSeconds', 'kernelCpuSeconds',
271                               'cpuConsumptionPercent', 'fsActivity',
272                               'userCpuConsumptionPercent',
273                               'kernelCpuConsumptionPercent'];
274    checkForUnknownFields(usage, threadUsageFields);
275    assert.strictEqual(typeof usage.userCpuSeconds, 'number');
276    assert.strictEqual(typeof usage.kernelCpuSeconds, 'number');
277    assert.strictEqual(typeof usage.cpuConsumptionPercent, 'number');
278    assert.strictEqual(typeof usage.userCpuConsumptionPercent, 'number');
279    assert.strictEqual(typeof usage.kernelCpuConsumptionPercent, 'number');
280    assert(typeof usage.fsActivity === 'object' && usage.fsActivity !== null);
281    checkForUnknownFields(usage.fsActivity, ['reads', 'writes']);
282    assert(Number.isSafeInteger(usage.fsActivity.reads));
283    assert(Number.isSafeInteger(usage.fsActivity.writes));
284  }
285
286  // Verify the format of the libuv section.
287  assert(Array.isArray(report.libuv));
288  report.libuv.forEach((resource) => {
289    assert.strictEqual(typeof resource.type, 'string');
290    assert.strictEqual(typeof resource.address, 'string');
291    assert.match(resource.address, /^0x[0-9a-f]+$/);
292    assert.strictEqual(typeof resource.is_active, 'boolean');
293    assert.strictEqual(typeof resource.is_referenced,
294                       resource.type === 'loop' ? 'undefined' : 'boolean');
295  });
296
297  // Verify the format of the environmentVariables section.
298  for (const [key, value] of Object.entries(report.environmentVariables)) {
299    assert.strictEqual(typeof key, 'string');
300    assert.strictEqual(typeof value, 'string');
301  }
302
303  // Verify the format of the userLimits section on non-Windows platforms.
304  if (!isWindows) {
305    const userLimitsFields = ['core_file_size_blocks', 'data_seg_size_kbytes',
306                              'file_size_blocks', 'max_locked_memory_bytes',
307                              'max_memory_size_kbytes', 'open_files',
308                              'stack_size_bytes', 'cpu_time_seconds',
309                              'max_user_processes', 'virtual_memory_kbytes'];
310    checkForUnknownFields(report.userLimits, userLimitsFields);
311    for (const [type, limits] of Object.entries(report.userLimits)) {
312      assert.strictEqual(typeof type, 'string');
313      assert(typeof limits === 'object' && limits !== null);
314      checkForUnknownFields(limits, ['soft', 'hard']);
315      assert(typeof limits.soft === 'number' || limits.soft === 'unlimited',
316             `Invalid ${type} soft limit of ${limits.soft}`);
317      assert(typeof limits.hard === 'number' || limits.hard === 'unlimited',
318             `Invalid ${type} hard limit of ${limits.hard}`);
319    }
320  }
321
322  // Verify the format of the sharedObjects section.
323  assert(Array.isArray(report.sharedObjects));
324  report.sharedObjects.forEach((sharedObject) => {
325    assert.strictEqual(typeof sharedObject, 'string');
326  });
327
328  // Verify the format of the workers section.
329  assert(Array.isArray(report.workers));
330  report.workers.forEach((worker) => _validateContent(worker));
331}
332
333function checkForUnknownFields(actual, expected) {
334  Object.keys(actual).forEach((field) => {
335    assert(expected.includes(field), `'${field}' not expected in ${expected}`);
336  });
337}
338
339module.exports = { findReports, validate, validateContent };
340