From 73c0ab33de93626dda599c70d2e75f074d677632 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 3 Feb 2017 11:18:34 -0800 Subject: [PATCH] Initial commit --- .gitignore | 2 + .npmignore | 2 + README.md | 27 +++++++ cli.js | 13 ++++ index.js | 102 ++++++++++++++++++++++++++ package.json | 32 ++++++++ test/fixtures/framework-addresses.txt | 2 + test/fixtures/framework-symbols.txt | 2 + test/fixtures/node-addresses.txt | 2 + test/fixtures/node-symbols.txt | 2 + test/test.js | 36 +++++++++ 11 files changed, 222 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100755 cli.js create mode 100644 index.js create mode 100644 package.json create mode 100644 test/fixtures/framework-addresses.txt create mode 100644 test/fixtures/framework-symbols.txt create mode 100644 test/fixtures/node-addresses.txt create mode 100644 test/fixtures/node-symbols.txt create mode 100644 test/test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f912fc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cache +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b9dbbae --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +cache +test diff --git a/README.md b/README.md new file mode 100644 index 0000000..504f1f4 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# electron-atos + +Symbolicate an [Electron](http://electron.atom.io) macOS crash report that is +missing symbols. + +## Usage + +- Copy the lines missing symbols from a crash report to a local `crash.txt` file: + +``` +0 com.github.electron.framework 0x000000010d01fad3 0x10c497000 + 12094163 +1 com.github.electron.framework 0x000000010d095014 0x10c497000 + 12574740 +``` + +- Run `electron-atos` and specify the path to the file and the version of + Electron that was being used. + +```sh +electron-atos --file /path/to/crash.txt --version 1.4.14 +``` + +- The symbols of the given address will be printed out: + +``` +content::RenderProcessHostImpl::Cleanup() (in Electron Framework) (render_process_host_impl.cc:1908) +content::ServiceWorkerProcessManager::Shutdown() (in Electron Framework) (__tree:165) +``` diff --git a/cli.js b/cli.js new file mode 100755 index 0000000..b363f4a --- /dev/null +++ b/cli.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +const {argv} = require('yargs') +const atos = require('./index') + +atos(argv, (error, symbols) => { + if (error != null) { + console.error(error.stack || error.message) + process.exit(1) + } else { + console.log(symbols.join('\n')) + } +}) diff --git a/index.js b/index.js new file mode 100644 index 0000000..8ce59cc --- /dev/null +++ b/index.js @@ -0,0 +1,102 @@ +const ChildProcess = require('child_process') +const electronDownload = require('electron-download') +const extractZip = require('extract-zip') +const fs = require('fs') +const path = require('path') + +module.exports = (options, callback) => { + const {version, quiet, file, force} = options + const directory = path.join(__dirname, 'cache', version) + + const addresses = getAddresses(file) + + download({version, quiet, directory, force}, (error) => { + if (error != null) return callback(error) + + const symbols = [] + const symbolicateNextAddress = () => { + const address = addresses.shift() + if (address == null) return callback(null, symbols) + symbolicate({directory, address}, (error, symbol) => { + if (error != null) return callback(error) + symbols.push(symbol) + symbolicateNextAddress() + }) + } + symbolicateNextAddress() + }) +} + +const download = (options, callback) => { + const {version, quiet, directory, force} = options + + if (fs.existsSync(directory) && !force) return callback() + + electronDownload({ + version: version, + dsym: true, + platform: 'darwin', + arch: 'x64', + quiet: quiet, + force: force + }, (error, zipPath) => { + if (error != null) return callback(error) + extractZip(zipPath, {dir: directory}, callback) + }) +} + +const symbolicate = (options, callback) => { + const {directory, address} = options + + const command = 'atos' + const args = [ + '-o', + getLibraryPath(directory, address.library), + '-l', + address.image + ] + const atos = ChildProcess.spawn(command, args) + let output = '' + let error = '' + atos.on('close', (code) => { + if (code === 0) { + callback(null, output.trim()) + } else { + error = `atos exited with ${code}: ${error}` + callback(new Error(error)) + } + }) + atos.stdout.on('data', (data) => { + output += data.toString() + }) + atos.stderr.on('data', (data) => { + error += data.toString() + }) + atos.stdin.write(address.address) + atos.stdin.end() +} + +const getAddresses = (file) => { + const content = fs.readFileSync(file, 'utf8') + const addresses = [] + content.split('\n').forEach((line) => { + const segments = line.split(/\s+/) + const index = parseInt(segments[0]) + if (!isFinite(index)) return + + const library = segments[1] + const address = segments[2] + const image = segments[3] + addresses.push({library, image, address}) + }) + return addresses +} + +const getLibraryPath = (rootDirectory, library) => { + switch (library) { + case 'com.github.electron.framework': + return path.join(rootDirectory, 'Electron framework.framework.dSYM', 'Contents', 'Resources', 'DWARF', 'Electron Framework') + case 'libnode.dylib': + return path.join(rootDirectory, 'libnode.dylib.dSYM', 'Contents', 'Resources', 'DWARF', 'libnode.dylib') + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1337f5a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "electron-atos", + "version": "1.0.0", + "description": "Symbolicate Electron macOS crashes", + "main": "index.js", + "bin": { + "electron-atos": "./cli.js" + }, + "scripts": { + "test": "mocha test" + }, + "keywords": [ + "atos", + "electron" + ], + "author": "Kevin Sawicki", + "license": "ISC", + "dependencies": { + "extract-zip": "^1.6.0", + "yargs": "^6.6.0" + }, + "os": [ + "darwin" + ], + "engines": { + "node": ">=7" + }, + "devDependencies": { + "mocha": "^3.2.0", + "standard": "^8.6.0" + } +} diff --git a/test/fixtures/framework-addresses.txt b/test/fixtures/framework-addresses.txt new file mode 100644 index 0000000..9ad1110 --- /dev/null +++ b/test/fixtures/framework-addresses.txt @@ -0,0 +1,2 @@ +0 com.github.electron.framework 0x000000010d01fad3 0x10c497000 + 12094163 +1 com.github.electron.framework 0x000000010d095014 0x10c497000 + 12574740 diff --git a/test/fixtures/framework-symbols.txt b/test/fixtures/framework-symbols.txt new file mode 100644 index 0000000..2ac004e --- /dev/null +++ b/test/fixtures/framework-symbols.txt @@ -0,0 +1,2 @@ +content::RenderProcessHostImpl::Cleanup() (in Electron Framework) (render_process_host_impl.cc:1908) +content::ServiceWorkerProcessManager::Shutdown() (in Electron Framework) (__tree:165) diff --git a/test/fixtures/node-addresses.txt b/test/fixtures/node-addresses.txt new file mode 100644 index 0000000..15d7402 --- /dev/null +++ b/test/fixtures/node-addresses.txt @@ -0,0 +1,2 @@ +3 libnode.dylib 0x000000010ab5c383 0x10aa09000 + 1389443 +4 libnode.dylib 0x000000010ab678e9 0x10aa09000 + 1435881 diff --git a/test/fixtures/node-symbols.txt b/test/fixtures/node-symbols.txt new file mode 100644 index 0000000..6d6ff4f --- /dev/null +++ b/test/fixtures/node-symbols.txt @@ -0,0 +1,2 @@ +worker (in libnode.dylib) (threadpool.c:76) +uv__thread_start (in libnode.dylib) (thread.c:54) diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..70d3403 --- /dev/null +++ b/test/test.js @@ -0,0 +1,36 @@ +const assert = require('assert') +const atos = require('..') +const fs = require('fs') +const path = require('path') + +const {describe, it} = global + +describe('atos', function () { + this.timeout(60000) + + const fixtures = path.join(__dirname, 'fixtures') + + it('returns an array of symbols for an Electron framework address', (done) => { + atos({ + file: path.join(fixtures, 'framework-addresses.txt'), + version: '1.4.14' + }, (error, symbols) => { + if (error != null) return done(error) + + assert.equal(symbols.join('\n'), fs.readFileSync(path.join(fixtures, 'framework-symbols.txt'), 'utf8').trim()) + done() + }) + }) + + it('returns an array of symbols for a node address', (done) => { + atos({ + file: path.join(fixtures, 'node-addresses.txt'), + version: '1.4.14' + }, (error, symbols) => { + if (error != null) return done(error) + + assert.equal(symbols.join('\n'), fs.readFileSync(path.join(fixtures, 'node-symbols.txt'), 'utf8').trim()) + done() + }) + }) +})