api-crash-reporter-spec.js 15 KB

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