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