Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: bump engines to Node.js >=22.12.0 #70

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 20.x
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install
run: yarn install --frozen-lockfile
Expand Down
9 changes: 1 addition & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,16 @@ jobs:
strategy:
matrix:
node-version:
- '20.9'
- '18.17'
- '16.20'
- '14.16'
- 22.12.x
runs-on: macos-latest
steps:
- name: Install Rosetta
if: ${{ matrix.node-version == '14.16' }}
run: /usr/sbin/softwareupdate --install-rosetta --agree-to-license
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "${{ matrix.node-version }}"
cache: 'yarn'
architecture: ${{ matrix.node-version == '14.16' && 'x64' || env.RUNNER_ARCH }}
- name: Install
run: yarn install --frozen-lockfile
- name: Test
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.12
164 changes: 67 additions & 97 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
#!/usr/bin/env node

const path = require('path')
const fs = require('fs')
const stream = require('stream')
const { promisify } = require('util')
import fs from 'node:fs'
import path from 'node:path'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import { createGunzip } from 'node:zlib';

const { symbolicateFrames } = require('@indutny/breakpad');
const got = require('got')
const mkdirp = require('mkdirp')
const yargs = require('yargs')
import { symbolicateFrames } from '@indutny/breakpad'

const symbolicate = async (options) => {
import { parseAddressLine } from './parsing.js'

export const symbolicate = async (options) => {
const {force, file} = options
const cacheDirectory = path.join(__dirname, 'cache', 'breakpad_symbols')
const cacheDirectory = path.join(path.dirname(fileURLToPath(import.meta.url)), 'cache', 'breakpad_symbols')

const dumpText = await fs.promises.readFile(file, 'utf8')
const images = binaryImages(dumpText)
Expand Down Expand Up @@ -128,62 +130,6 @@ const binaryImages = (dumpText) => {
return images
}

function parseAddressLine(line) {
return parseAsCrashReportLine(line) || parseAsSpindumpLine(line) || parseAsSamplingLine(line)
}
function parseAsCrashReportLine(line) {
// from a system crash report:
// 1 dyld 0x00007fff69ed6975 ImageLoaderMachO::validateFirstPages(linkedit_data_command const*, int, unsigned char const*, unsigned long, long long, ImageLoader::LinkContext const&) + 145
// 13 com.github.Electron.framework 0x000000010cfa931d node::binding::get_linked_module(char const*) + 3549
// 1 com.github.Electron.framework 0x000000010c99e684 -[ElectronNSWindowDelegate windowWillClose:] + 36 (electron_ns_window_delegate.mm:251)
// 15 com.github.Electron.framework 0x00000001118b1a86 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) + 10125238
// 0 Electron Framework 0x1104881e7 node::AsyncResource::get_async_id() const + 9674519
const m = /^(\s*\d+\s+(.+)\s+0x([0-9a-f]+)\s+)(.+? \+ \d+)/.exec(line)
if (m) {
const [, prefix, libraryId, address, symbolWithOffset] = m
return {
libraryId: libraryId.trim(),
address: parseInt(address, 16),
replace: { from: prefix.length, length: symbolWithOffset.length }
}
}
}
function parseAsSpindumpLine(line) {
// from spindump
// 1000 ElectronMain + 134 (Electron Framework + 114470) [0x10e3f8f26]
// 1000 electron::fuses::IsRunAsNodeEnabled() + 5717170 (Electron Framework + 7609010) [0x10eb1eab2]
// 1000 electron::fuses::IsRunAsNodeEnabled() + 5716934 (Electron Framework + 7608774) [0x10eb1e9c6]
// 54 electron::fuses::IsRunAsNodeEnabled() + 5722152 (Electron Framework + 7600776) [0x1129dfa88]
// *54 ipc_mqueue_receive_continue + 0 (kernel + 389664) [0xffffff800026f220]
// 54 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) + 2928702 (Electron Framework + 19826782) [0x11358885e]
const m = /(?<=\d+\s+)(\S.*? \+ \d+|\?\?\?) \((.+?) \+ (\d+)\) \[0x([0-9a-f]+)\]/.exec(line)
if (m) {
const [, toReplace, libraryBaseName, offset, address] = m
return {
address: parseInt(address, 16),
libraryBaseName,
offset: parseInt(offset, 10),
replace: { from: m.index, length: toReplace.length }
}
}
}
function parseAsSamplingLine(line) {
// from sampling
// + 2433 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) (in Electron Framework) + 2917380 [0x10f6c5a64]
// + ! 2426 __CFRunLoopServiceMachPort (in CoreFoundation) + 247 [0x7fff395789d5]
// + ! : | 4 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) (in Electron Framework) + 13325775 [0x1100b2c2f]
// 9 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) (in Electron Framework) + 9711676 [0x10fd4069c]
const m = /(?<=\d+\s+)(\S.*?)\s+\(in (.+?)\).*?\[0x([0-9a-f]+)\]/.exec(line)
if (m) {
const [, symbol, libraryBaseName, address] = m
return {
address: parseInt(address, 16),
libraryBaseName,
replace: { from: m.index, length: symbol.length }
}
}
}

const SYMBOL_BASE_URLS = [
'https://symbols.mozilla.org/try',
'https://symbols.electronjs.org',
Expand All @@ -192,45 +138,69 @@ const SYMBOL_BASE_URLS = [
async function fetchSymbol(directory, baseUrl, pdb, id, symbolFileName) {
const url = `${baseUrl}/${encodeURIComponent(pdb)}/${id}/${encodeURIComponent(symbolFileName)}`
const symbolPath = path.join(directory, pdb, id, symbolFileName)
const pipeline = promisify(stream.pipeline)

// ensure path is created
await mkdirp(path.dirname(symbolPath))

// decompress the gzip
try {
const str = got.stream(url, {
decompress: true,
followRedirect: true,
})
await fs.promises.mkdir(path.dirname(symbolPath), { recursive: true })

const response = await fetch(url, { headers: { 'Accept-Encoding': 'gzip' } })

if (response.ok) {
const readable = Readable.fromWeb(response.body)
const output = fs.createWriteStream(symbolPath)
// create symbol
await pipeline(str, fs.createWriteStream(symbolPath))
} catch (err) {
if (err.message.startsWith('Response code 404')) {
return false
if (response.headers['content-encoding'] === 'gzip') {
// decompress the gzip
await pipeline(readable, createGunzip(), output)
} else {
throw err
await pipeline(readable, output)
}
} else if (response.status === 404) {
return false
} else {
throw new Error(`Response code ${response.status} (${response.statusText})`)
}

return true
}

module.exports = { symbolicate, testing: { parseAddress: parseAddressLine } }

if (require.main === module) {
const argv = yargs
.command('$0 <file>', 'symbolicate a textual crash dump', (yargs) => {
return yargs
.positional('file', {
describe: 'path to crash dump',
})
.option('force', {
describe: 'redownload symbols if present in cache',
type: 'boolean'
})
})
.help()
.argv
symbolicate(argv).then(console.log, console.error)
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const {
positionals,
values: { force, help, version },
} = parseArgs({
allowPositionals: true,
options: {
force: {
type: 'boolean',
},
help: {
type: 'boolean',
},
version: {
type: 'boolean',
},
},
});

if (positionals.length !== 1 || help) {
console.log(`electron-symbolicate-mac <file>

symbolicate a textual crash dump

Positionals:
file path to crash dump

Options:
--force Redownload symbols if present in cache
--help Show help
--version Show version number`)
process.exit(0)
}

if (version) {
console.log(JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url))).version)
process.exit(0)
}

symbolicate({ file: positionals[0], force }).then(console.log, console.error)
}
17 changes: 8 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "@electron/symbolicate-mac",
"version": "0.0.0-development",
"description": "Symbolicate textual Electron macOS crashes",
"main": "index.js",
"type": "module",
"exports": "./dist/index.js",
"repository": "https://github.com/electron/symbolicate-mac",
"publishConfig": {
"provenance": true
Expand All @@ -11,10 +12,11 @@
"electron-symbolicate-mac": "./index.js"
},
"files": [
"index.js"
"index.js",
"parsing.js"
],
"scripts": {
"test": "jest"
"test": "vitest run"
},
"keywords": [
"electron",
Expand All @@ -27,15 +29,12 @@
],
"license": "ISC",
"dependencies": {
"@indutny/breakpad": "^1.2.0",
"got": "^11.8.2",
"mkdirp": "^1.0.4",
"yargs": "^17.0.1"
"@indutny/breakpad": "^1.2.0"
},
"engines": {
"node": ">=4"
"node": ">=22.12.0"
},
"devDependencies": {
"jest": "^29.0.0"
"vitest": "^3.0.6"
}
}
59 changes: 59 additions & 0 deletions parsing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

export function parseAddressLine(line) {
return parseAsCrashReportLine(line) || parseAsSpindumpLine(line) || parseAsSamplingLine(line)
}

function parseAsCrashReportLine(line) {
// from a system crash report:
// 1 dyld 0x00007fff69ed6975 ImageLoaderMachO::validateFirstPages(linkedit_data_command const*, int, unsigned char const*, unsigned long, long long, ImageLoader::LinkContext const&) + 145
// 13 com.github.Electron.framework 0x000000010cfa931d node::binding::get_linked_module(char const*) + 3549
// 1 com.github.Electron.framework 0x000000010c99e684 -[ElectronNSWindowDelegate windowWillClose:] + 36 (electron_ns_window_delegate.mm:251)
// 15 com.github.Electron.framework 0x00000001118b1a86 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) + 10125238
// 0 Electron Framework 0x1104881e7 node::AsyncResource::get_async_id() const + 9674519
const m = /^(\s*\d+\s+(.+)\s+0x([0-9a-f]+)\s+)(.+? \+ \d+)/.exec(line)
if (m) {
const [, prefix, libraryId, address, symbolWithOffset] = m
return {
libraryId: libraryId.trim(),
address: parseInt(address, 16),
replace: { from: prefix.length, length: symbolWithOffset.length }
}
}
}

function parseAsSpindumpLine(line) {
// from spindump
// 1000 ElectronMain + 134 (Electron Framework + 114470) [0x10e3f8f26]
// 1000 electron::fuses::IsRunAsNodeEnabled() + 5717170 (Electron Framework + 7609010) [0x10eb1eab2]
// 1000 electron::fuses::IsRunAsNodeEnabled() + 5716934 (Electron Framework + 7608774) [0x10eb1e9c6]
// 54 electron::fuses::IsRunAsNodeEnabled() + 5722152 (Electron Framework + 7600776) [0x1129dfa88]
// *54 ipc_mqueue_receive_continue + 0 (kernel + 389664) [0xffffff800026f220]
// 54 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) + 2928702 (Electron Framework + 19826782) [0x11358885e]
const m = /(?<=\d+\s+)(\S.*? \+ \d+|\?\?\?) \((.+?) \+ (\d+)\) \[0x([0-9a-f]+)\]/.exec(line)
if (m) {
const [, toReplace, libraryBaseName, offset, address] = m
return {
address: parseInt(address, 16),
libraryBaseName,
offset: parseInt(offset, 10),
replace: { from: m.index, length: toReplace.length }
}
}
}

function parseAsSamplingLine(line) {
// from sampling
// + 2433 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) (in Electron Framework) + 2917380 [0x10f6c5a64]
// + ! 2426 __CFRunLoopServiceMachPort (in CoreFoundation) + 247 [0x7fff395789d5]
// + ! : | 4 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) (in Electron Framework) + 13325775 [0x1100b2c2f]
// 9 v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) (in Electron Framework) + 9711676 [0x10fd4069c]
const m = /(?<=\d+\s+)(\S.*?)\s+\(in (.+?)\).*?\[0x([0-9a-f]+)\]/.exec(line)
if (m) {
const [, symbol, libraryBaseName, address] = m
return {
address: parseInt(address, 16),
libraryBaseName,
replace: { from: m.index, length: symbol.length }
}
}
}
10 changes: 5 additions & 5 deletions spec/__snapshots__/symbolication.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`symbolication symbolicates a crash report 1`] = `
exports[`symbolication > symbolicates a crash report 1`] = `
"Process: Slack [2214]
Path: /Applications/Slack.app/Contents/MacOS/Slack
Identifier: com.tinyspeck.slackmacgap
Expand Down Expand Up @@ -560,7 +560,7 @@ Binary Images:
"
`;

exports[`symbolication symbolicates a sampling report 1`] = `
exports[`symbolication > symbolicates a sampling report 1`] = `
"Sampling process 2252 for 3 seconds with 1 millisecond of run time between samples
Sampling completed, processing symbols...
Analysis of sampling Slack (pid 2252) every 1 millisecond
Expand Down Expand Up @@ -1537,7 +1537,7 @@ Sample analysis of process 2252 written to file /dev/stdout
"
`;

exports[`symbolication symbolicates a spindump 1`] = `
exports[`symbolication > symbolicates a spindump 1`] = `
"Date/Time: 2021-07-22 14:06:09 -0700
End time: 2021-07-22 14:06:31 -0700
OS Version: Mac OS X 10.15.7 (Build 19H1030)
Expand Down Expand Up @@ -2040,7 +2040,7 @@ Note: 1 idle work queue thread omitted
"
`;

exports[`symbolication symbolicates a windows crash 1`] = `
exports[`symbolication > symbolicates a windows crash 1`] = `
"0 electron.exe.pdb 0x00007ff68e2d395b v8::base::OS::Abort()
1 electron.exe.pdb 0x00007ff691a5711f _tailMerge_winusb.dll
2 electron.exe.pdb 0x00007ff691a570c7 _tailMerge_winusb.dll
Expand Down
4 changes: 3 additions & 1 deletion spec/parsing.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const {parseAddress} = require('..').testing
import { describe, expect, it } from 'vitest'

import { parseAddressLine as parseAddress } from '../parsing.js'

describe('address parsing', () => {
it('parses an address from a crash dump', () => {
Expand Down
Loading
Loading