api-crash-reporter-spec.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. const assert = require('assert')
  2. const childProcess = require('child_process')
  3. const {expect} = require('chai')
  4. const fs = require('fs')
  5. const http = require('http')
  6. const multiparty = require('multiparty')
  7. const path = require('path')
  8. const temp = require('temp').track()
  9. const url = require('url')
  10. const {closeWindow} = require('./window-helpers')
  11. const {remote} = require('electron')
  12. const {app, BrowserWindow, crashReporter} = remote.require('electron')
  13. describe('crashReporter module', () => {
  14. if (process.mas || process.env.DISABLE_CRASH_REPORTER_TESTS) return
  15. // TODO(alexeykuzmin): [Ch66] Fails. Fix it and enable back.
  16. if (process.platform === 'linux') return
  17. let originalTempDirectory = null
  18. let tempDirectory = null
  19. before(() => {
  20. tempDirectory = temp.mkdirSync('electronCrashReporterSpec-')
  21. originalTempDirectory = app.getPath('temp')
  22. app.setPath('temp', tempDirectory)
  23. })
  24. after(() => {
  25. app.setPath('temp', originalTempDirectory)
  26. })
  27. const fixtures = path.resolve(__dirname, 'fixtures')
  28. const generateSpecs = (description, browserWindowOpts) => {
  29. describe(description, () => {
  30. let w = null
  31. let stopServer = null
  32. beforeEach(() => {
  33. stopServer = null
  34. w = new BrowserWindow(Object.assign({ show: false }, browserWindowOpts))
  35. })
  36. afterEach(() => closeWindow(w).then(() => { w = null }))
  37. afterEach(() => {
  38. stopCrashService()
  39. })
  40. afterEach((done) => {
  41. if (stopServer != null) {
  42. stopServer(done)
  43. } else {
  44. done()
  45. }
  46. })
  47. it('should send minidump when renderer crashes', function (done) {
  48. // TODO(alexeykuzmin): Skip the test instead of marking it as passed.
  49. if (process.env.APPVEYOR === 'True') return done()
  50. this.timeout(180000)
  51. stopServer = startServer({
  52. callback (port) {
  53. const crashUrl = url.format({
  54. protocol: 'file',
  55. pathname: path.join(fixtures, 'api', 'crash.html'),
  56. search: '?port=' + port
  57. })
  58. w.loadURL(crashUrl)
  59. },
  60. processType: 'renderer',
  61. done: done
  62. })
  63. })
  64. it('should send minidump when node processes crash', function (done) {
  65. // TODO(alexeykuzmin): Skip the test instead of marking it as passed.
  66. if (process.env.APPVEYOR === 'True') return done()
  67. this.timeout(180000)
  68. stopServer = startServer({
  69. callback (port) {
  70. const crashesDir = path.join(app.getPath('temp'), `${process.platform === 'win32' ? 'Zombies' : app.getName()} Crashes`)
  71. const version = app.getVersion()
  72. const crashPath = path.join(fixtures, 'module', 'crash.js')
  73. if (process.platform === 'win32') {
  74. const crashServiceProcess = childProcess.spawn(process.execPath, [
  75. `--reporter-url=http://127.0.0.1:${port}`,
  76. '--application-name=Zombies',
  77. `--crashes-directory=${crashesDir}`
  78. ], {
  79. env: {
  80. ELECTRON_INTERNAL_CRASH_SERVICE: 1
  81. },
  82. detached: true
  83. })
  84. remote.process.crashServicePid = crashServiceProcess.pid
  85. }
  86. childProcess.fork(crashPath, [port, version, crashesDir], {silent: true})
  87. },
  88. processType: 'browser',
  89. done: done
  90. })
  91. })
  92. it('should not send minidump if uploadToServer is false', function (done) {
  93. this.timeout(180000)
  94. let dumpFile
  95. let crashesDir = crashReporter.getCrashesDirectory()
  96. const existingDumpFiles = new Set()
  97. if (process.platform === 'darwin') {
  98. // crashpad puts the dump files in the "completed" subdirectory
  99. crashesDir = path.join(crashesDir, 'completed')
  100. crashReporter.setUploadToServer(false)
  101. }
  102. const testDone = (uploaded) => {
  103. if (uploaded) return done(new Error('Uploaded crash report'))
  104. if (process.platform === 'darwin') crashReporter.setUploadToServer(true)
  105. assert(fs.existsSync(dumpFile))
  106. done()
  107. }
  108. let pollInterval
  109. const pollDumpFile = () => {
  110. fs.readdir(crashesDir, (err, files) => {
  111. if (err) return
  112. const dumps = files.filter((file) => /\.dmp$/.test(file) && !existingDumpFiles.has(file))
  113. if (!dumps.length) return
  114. assert.equal(1, dumps.length)
  115. dumpFile = path.join(crashesDir, dumps[0])
  116. clearInterval(pollInterval)
  117. // dump file should not be deleted when not uploading, so we wait
  118. // 1s and assert it still exists in `testDone`
  119. setTimeout(testDone, 1000)
  120. })
  121. }
  122. remote.ipcMain.once('list-existing-dumps', (event) => {
  123. fs.readdir(crashesDir, (err, files) => {
  124. if (!err) {
  125. for (const file of files) {
  126. if (/\.dmp$/.test(file)) {
  127. existingDumpFiles.add(file)
  128. }
  129. }
  130. }
  131. event.returnValue = null // allow the renderer to crash
  132. pollInterval = setInterval(pollDumpFile, 100)
  133. })
  134. })
  135. stopServer = startServer({
  136. callback (port) {
  137. const crashUrl = url.format({
  138. protocol: 'file',
  139. pathname: path.join(fixtures, 'api', 'crash.html'),
  140. search: `?port=${port}&skipUpload=1`
  141. })
  142. w.loadURL(crashUrl)
  143. },
  144. processType: 'renderer',
  145. done: testDone.bind(null, true)
  146. })
  147. })
  148. it('should send minidump with updated extra parameters', function (done) {
  149. // TODO(alexeykuzmin): Skip the test instead of marking it as passed.
  150. if (process.env.APPVEYOR === 'True') return done()
  151. this.timeout(180000)
  152. stopServer = startServer({
  153. callback (port) {
  154. const crashUrl = url.format({
  155. protocol: 'file',
  156. pathname: path.join(fixtures, 'api', 'crash-restart.html'),
  157. search: `?port=${port}`
  158. })
  159. w.loadURL(crashUrl)
  160. },
  161. processType: 'renderer',
  162. done: done
  163. })
  164. })
  165. })
  166. }
  167. generateSpecs('without sandbox', {})
  168. generateSpecs('with sandbox', {
  169. webPreferences: {
  170. sandbox: true,
  171. preload: path.join(fixtures, 'module', 'preload-sandbox.js')
  172. }
  173. })
  174. describe('getProductName', () => {
  175. it('returns the product name if one is specified', () => {
  176. const name = crashReporter.getProductName()
  177. const expectedName = (process.platform === 'darwin') ? 'Electron Test' : 'Zombies'
  178. assert.equal(name, expectedName)
  179. })
  180. })
  181. describe('getTempDirectory', () => {
  182. it('returns temp directory for app if one is specified', () => {
  183. const tempDir = crashReporter.getTempDirectory()
  184. assert.equal(tempDir, app.getPath('temp'))
  185. })
  186. })
  187. describe('start(options)', () => {
  188. it('requires that the companyName and submitURL options be specified', () => {
  189. assert.throws(() => {
  190. crashReporter.start({companyName: 'Missing submitURL'})
  191. }, /submitURL is a required option to crashReporter\.start/)
  192. assert.throws(() => {
  193. crashReporter.start({submitURL: 'Missing companyName'})
  194. }, /companyName is a required option to crashReporter\.start/)
  195. })
  196. it('can be called multiple times', () => {
  197. assert.doesNotThrow(() => {
  198. crashReporter.start({
  199. companyName: 'Umbrella Corporation',
  200. submitURL: 'http://127.0.0.1/crashes'
  201. })
  202. crashReporter.start({
  203. companyName: 'Umbrella Corporation 2',
  204. submitURL: 'http://127.0.0.1/more-crashes'
  205. })
  206. })
  207. })
  208. })
  209. describe('getCrashesDirectory', () => {
  210. it('correctly returns the directory', () => {
  211. const crashesDir = crashReporter.getCrashesDirectory()
  212. let dir
  213. if (process.platform === 'win32') {
  214. dir = `${app.getPath('temp')}/Zombies Crashes`
  215. } else {
  216. dir = `${app.getPath('temp')}/Electron Test Crashes`
  217. }
  218. assert.equal(crashesDir, dir)
  219. })
  220. })
  221. describe('getUploadedReports', () => {
  222. it('returns an array of reports', () => {
  223. const reports = crashReporter.getUploadedReports()
  224. assert(typeof reports === 'object')
  225. })
  226. })
  227. // TODO(alexeykuzmin): This suite should explicitly
  228. // generate several crash reports instead of hoping
  229. // that there will be enough of them already.
  230. describe('getLastCrashReport', () => {
  231. it('correctly returns the most recent report', () => {
  232. const reports = crashReporter.getUploadedReports()
  233. expect(reports).to.be.an('array')
  234. expect(reports).to.have.lengthOf.at.least(2,
  235. 'There are not enough reports for this test')
  236. const lastReport = crashReporter.getLastCrashReport()
  237. expect(lastReport).to.be.an('object').that.includes.a.key('date')
  238. // Let's find the newest report.
  239. const {report: newestReport} = reports.reduce((acc, cur) => {
  240. const timestamp = new Date(cur.date).getTime()
  241. return (timestamp > acc.timestamp)
  242. ? { report: cur, timestamp: timestamp }
  243. : acc
  244. }, { timestamp: -Infinity })
  245. assert(newestReport, 'Hey!')
  246. expect(lastReport.date.getTime()).to.be.equal(
  247. newestReport.date.getTime(),
  248. 'Last report is not the newest.')
  249. })
  250. })
  251. describe('getUploadToServer()', () => {
  252. it('throws an error when called from the renderer process', () => {
  253. assert.throws(() => require('electron').crashReporter.getUploadToServer())
  254. })
  255. it('returns true when uploadToServer is set to true', function () {
  256. if (process.platform !== 'darwin') {
  257. // FIXME(alexeykuzmin): Skip the test.
  258. // this.skip()
  259. return
  260. }
  261. crashReporter.start({
  262. companyName: 'Umbrella Corporation',
  263. submitURL: 'http://127.0.0.1/crashes',
  264. uploadToServer: true
  265. })
  266. assert.equal(crashReporter.getUploadToServer(), true)
  267. })
  268. it('returns false when uploadToServer is set to false', function () {
  269. if (process.platform !== 'darwin') {
  270. // FIXME(alexeykuzmin): Skip the test.
  271. // this.skip()
  272. return
  273. }
  274. crashReporter.start({
  275. companyName: 'Umbrella Corporation',
  276. submitURL: 'http://127.0.0.1/crashes',
  277. uploadToServer: true
  278. })
  279. crashReporter.setUploadToServer(false)
  280. assert.equal(crashReporter.getUploadToServer(), false)
  281. })
  282. })
  283. describe('setUploadToServer(uploadToServer)', () => {
  284. it('throws an error when called from the renderer process', () => {
  285. assert.throws(() => require('electron').crashReporter.setUploadToServer('arg'))
  286. })
  287. it('sets uploadToServer false when called with false', function () {
  288. if (process.platform !== 'darwin') {
  289. // FIXME(alexeykuzmin): Skip the test.
  290. // this.skip()
  291. return
  292. }
  293. crashReporter.start({
  294. companyName: 'Umbrella Corporation',
  295. submitURL: 'http://127.0.0.1/crashes',
  296. uploadToServer: true
  297. })
  298. crashReporter.setUploadToServer(false)
  299. assert.equal(crashReporter.getUploadToServer(), false)
  300. })
  301. it('sets uploadToServer true when called with true', function () {
  302. if (process.platform !== 'darwin') {
  303. // FIXME(alexeykuzmin): Skip the test.
  304. // this.skip()
  305. return
  306. }
  307. crashReporter.start({
  308. companyName: 'Umbrella Corporation',
  309. submitURL: 'http://127.0.0.1/crashes',
  310. uploadToServer: false
  311. })
  312. crashReporter.setUploadToServer(true)
  313. assert.equal(crashReporter.getUploadToServer(), true)
  314. })
  315. })
  316. describe('Parameters', () => {
  317. it('returns all of the current parameters', () => {
  318. crashReporter.start({
  319. companyName: 'Umbrella Corporation',
  320. submitURL: 'http://127.0.0.1/crashes'
  321. })
  322. const parameters = crashReporter.getParameters()
  323. assert(typeof parameters === 'object')
  324. })
  325. it('adds a parameter to current parameters', function () {
  326. if (process.platform !== 'darwin') {
  327. // FIXME(alexeykuzmin): Skip the test.
  328. // this.skip()
  329. return
  330. }
  331. crashReporter.start({
  332. companyName: 'Umbrella Corporation',
  333. submitURL: 'http://127.0.0.1/crashes'
  334. })
  335. crashReporter.addExtraParameter('hello', 'world')
  336. assert('hello' in crashReporter.getParameters())
  337. })
  338. it('removes a parameter from current parameters', function () {
  339. if (process.platform !== 'darwin') {
  340. // FIXME(alexeykuzmin): Skip the test.
  341. // this.skip()
  342. return
  343. }
  344. crashReporter.start({
  345. companyName: 'Umbrella Corporation',
  346. submitURL: 'http://127.0.0.1/crashes'
  347. })
  348. crashReporter.addExtraParameter('hello', 'world')
  349. assert('hello' in crashReporter.getParameters())
  350. crashReporter.removeExtraParameter('hello')
  351. assert(!('hello' in crashReporter.getParameters()))
  352. })
  353. })
  354. })
  355. const waitForCrashReport = () => {
  356. return new Promise((resolve, reject) => {
  357. let times = 0
  358. const checkForReport = () => {
  359. if (crashReporter.getLastCrashReport() != null) {
  360. resolve()
  361. } else if (times >= 10) {
  362. reject(new Error('No crash report available'))
  363. } else {
  364. times++
  365. setTimeout(checkForReport, 100)
  366. }
  367. }
  368. checkForReport()
  369. })
  370. }
  371. const startServer = ({callback, processType, done}) => {
  372. let called = false
  373. let server = http.createServer((req, res) => {
  374. const form = new multiparty.Form()
  375. form.parse(req, (error, fields) => {
  376. if (error) throw error
  377. if (called) return
  378. called = true
  379. assert.equal(fields.prod, 'Electron')
  380. assert.equal(fields.ver, process.versions.electron)
  381. assert.equal(fields.process_type, processType)
  382. assert.equal(fields.platform, process.platform)
  383. assert.equal(fields.extra1, 'extra1')
  384. assert.equal(fields.extra2, 'extra2')
  385. assert.equal(fields.extra3, undefined)
  386. assert.equal(fields._productName, 'Zombies')
  387. assert.equal(fields._companyName, 'Umbrella Corporation')
  388. assert.equal(fields._version, app.getVersion())
  389. const reportId = 'abc-123-def-456-abc-789-abc-123-abcd'
  390. res.end(reportId, () => {
  391. waitForCrashReport().then(() => {
  392. assert.equal(crashReporter.getLastCrashReport().id, reportId)
  393. assert.notEqual(crashReporter.getUploadedReports().length, 0)
  394. assert.equal(crashReporter.getUploadedReports()[0].id, reportId)
  395. req.socket.destroy()
  396. done()
  397. }, done)
  398. })
  399. })
  400. })
  401. const activeConnections = new Set()
  402. server.on('connection', (connection) => {
  403. activeConnections.add(connection)
  404. connection.once('close', () => {
  405. activeConnections.delete(connection)
  406. })
  407. })
  408. let {port} = remote.process
  409. server.listen(port, '127.0.0.1', () => {
  410. port = server.address().port
  411. remote.process.port = port
  412. if (process.platform === 'darwin') {
  413. crashReporter.start({
  414. companyName: 'Umbrella Corporation',
  415. submitURL: 'http://127.0.0.1:' + port
  416. })
  417. }
  418. callback(port)
  419. })
  420. return function stopServer (done) {
  421. for (const connection of activeConnections) {
  422. connection.destroy()
  423. }
  424. server.close(() => {
  425. done()
  426. })
  427. }
  428. }
  429. const stopCrashService = () => {
  430. const {crashServicePid} = remote.process
  431. if (crashServicePid) {
  432. remote.process.crashServicePid = 0
  433. try {
  434. process.kill(crashServicePid)
  435. } catch (error) {
  436. if (error.code !== 'ESRCH') {
  437. throw error
  438. }
  439. }
  440. }
  441. }