api-crash-reporter-spec.js 16 KB

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