UNPKG

@boost/debug

Version:

Lightweight debugging and crash reporting.

238 lines (193 loc) 5.12 kB
/* eslint-disable no-magic-numbers */ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { execaSync } from 'execa'; import glob from 'fast-glob'; import { type FilePath, json, type PackageStructure, type PortablePath, toArray, } from '@boost/common'; import { debug } from './debug'; function run(command: string, args: string[]): string { let cmd = command; if (command === 'where' && os.platform() === 'win32') { cmd += '.exe'; } return String(execaSync(cmd, args, { preferLocal: true }).stdout); } function resolveHome(filePath: FilePath): string { return filePath.replace(process.env.HOME!, '~'); } function extractVersion(value: string): string { const match = value.match(/\d+\.\d+\.\d+([.-a-z0-9])?/u); return match ? match[0] : ''; } export class CrashReporter { contents: string = ''; /** * Add a label with a value, or multiple values, to the last added section. */ add(label: string, ...messages: (PortablePath | number | string)[]): this { this.contents += `${label}:\n`; this.contents += ` ${messages.map(String).join(' - ')}\n`; return this; } /** * Start a new section with a title. */ addSection(title: string): this { this.contents += `\n${title.toUpperCase()}\n`; this.contents += `${'='.repeat(title.length)}\n\n`; debug('Reporting crash with %s', title); return this; } /** * Report Node.js related binary versions and paths. */ reportBinaries(): this { this.addSection('Binaries'); const bins = { node: 'Node', npm: 'npm', yarn: 'Yarn', }; Object.keys(bins).forEach((bin) => { try { this.add( bins[bin as keyof typeof bins], extractVersion(run(bin, ['--version'])), resolveHome(run('where', [bin])), ); } catch { // Ignore } }); return this; } /** * Report all environment variables. */ reportEnvVars(): this { this.addSection('Environment'); const keys = Object.keys(process.env).sort(); keys.forEach((key) => { this.add(key, process.env[key]!); }); return this; } /** * Report common programming language versions and paths */ reportLanguages(): this { this.addSection('Languages'); const languages: Record<string, string> = { bash: 'Bash', go: 'Go', javac: 'Java', perl: 'Perl', php: 'PHP', python: 'Python', ruby: 'Ruby', rustup: 'Rust', }; // When running on OSX and Java is not installed, // OSX will interrupt the process with a prompt to install Java. // This is super annoying, so let's not disrupt consumers. // istanbul ignore next if (os.platform() === 'darwin') { delete languages.javac; } Object.keys(languages).forEach((bin) => { let version; try { version = extractVersion(run(bin, ['--version'])); if (!version) { version = extractVersion(run(bin, ['version'])); } if (version) { this.add(languages[bin], version, resolveHome(run('where', [bin]))); } } catch { // Ignore } }); return this; } /** * Report information about the current `process`. */ reportProcess(): this { this.addSection('Process'); this.add('ID', process.pid); this.add('Title', process.title); this.add('Timestamp', new Date().toISOString()); this.add('CWD', process.cwd()); this.add('ARGV', process.argv.map((v) => `- ${v}`).join('\n ')); return this; } /** * Report the stack trace for a defined `Error`. */ reportStackTrace(error: Error): this { this.addSection('Stack Trace'); this.contents += String(error.stack); this.contents += '\n'; return this; } /** * Report information about the platform and operating system. */ reportSystem(): this { this.addSection('System'); this.add('OS', os.platform()); this.add('Architecture', os.arch()); this.add('CPUs', os.cpus().length); this.add('Uptime (sec)', os.uptime()); this.add( 'Memory usage', `${Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100} MB`, ); // istanbul ignore next if (process.platform !== 'win32') { this.add('Group ID', process.getgid!()); this.add('User ID', process.getuid!()); } return this; } /** * Report npm package versions for all that match the defined pattern. * Only searches in the root node modules folder and _will not_ work with PnP. */ reportPackageVersions(patterns: string[] | string, title: string = 'Packages'): this { this.addSection(title); const map = new Map<string, string>(); glob .sync( toArray(patterns).map((pattern) => path.join('./node_modules', pattern)), { absolute: true, onlyDirectories: true, onlyFiles: false, }, ) .forEach((pkgPath) => { const pkg = json.load<PackageStructure>(path.join(pkgPath, 'package.json')); map.set(pkg.name, pkg.version); }); map.forEach((version, name) => { this.add(name, version); }); return this; } /** * Write the reported content to the defined file path. */ write(filePath: PortablePath): this { fs.writeFileSync(String(filePath), this.contents.trim(), 'utf8'); return this; } }