api-crash-reporter-spec.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  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
  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. webPreferences: {
  164. nodeIntegration: true
  165. }
  166. })
  167. generateSpecs('with sandbox', {
  168. webPreferences: {
  169. sandbox: true,
  170. preload: path.join(fixtures, 'module', 'preload-sandbox.js')
  171. }
  172. })
  173. generateSpecs('with remote module disabled', {
  174. webPreferences: {
  175. nodeIntegration: true,
  176. enableRemoteModule: false
  177. }
  178. })
  179. describe('getProductName', () => {
  180. it('returns the product name if one is specified', () => {
  181. const name = crashReporter.getProductName()
  182. const expectedName = (process.platform === 'darwin') ? 'Electron Test' : 'Zombies'
  183. assert.strictEqual(name, expectedName)
  184. })
  185. })
  186. describe('start(options)', () => {
  187. it('requires that the companyName and submitURL options be specified', () => {
  188. assert.throws(() => {
  189. crashReporter.start({ companyName: 'Missing submitURL' })
  190. }, /submitURL is a required option to crashReporter\.start/)
  191. assert.throws(() => {
  192. crashReporter.start({ submitURL: 'Missing companyName' })
  193. }, /companyName is a required option to crashReporter\.start/)
  194. })
  195. it('can be called multiple times', () => {
  196. assert.doesNotThrow(() => {
  197. crashReporter.start({
  198. companyName: 'Umbrella Corporation',
  199. submitURL: 'http://127.0.0.1/crashes'
  200. })
  201. crashReporter.start({
  202. companyName: 'Umbrella Corporation 2',
  203. submitURL: 'http://127.0.0.1/more-crashes'
  204. })
  205. })
  206. })
  207. })
  208. describe('getCrashesDirectory', () => {
  209. it('correctly returns the directory', () => {
  210. const crashesDir = crashReporter.getCrashesDirectory()
  211. let dir
  212. if (process.platform === 'win32') {
  213. dir = `${app.getPath('temp')}/Zombies Crashes`
  214. } else {
  215. dir = `${app.getPath('temp')}/Electron Test Crashes`
  216. }
  217. assert.strictEqual(crashesDir, dir)
  218. })
  219. })
  220. describe('getUploadedReports', () => {
  221. it('returns an array of reports', () => {
  222. const reports = crashReporter.getUploadedReports()
  223. assert(typeof reports === 'object')
  224. })
  225. })
  226. // TODO(alexeykuzmin): This suite should explicitly
  227. // generate several crash reports instead of hoping
  228. // that there will be enough of them already.
  229. describe('getLastCrashReport', () => {
  230. it('correctly returns the most recent report', () => {
  231. const reports = crashReporter.getUploadedReports()
  232. expect(reports).to.be.an('array')
  233. expect(reports).to.have.lengthOf.at.least(2,
  234. 'There are not enough reports for this test')
  235. const lastReport = crashReporter.getLastCrashReport()
  236. expect(lastReport).to.be.an('object').that.includes.a.key('date')
  237. // Let's find the newest report.
  238. const { report: newestReport } = reports.reduce((acc, cur) => {
  239. const timestamp = new Date(cur.date).getTime()
  240. return (timestamp > acc.timestamp)
  241. ? { report: cur, timestamp: timestamp }
  242. : acc
  243. }, { timestamp: -Infinity })
  244. assert(newestReport, 'Hey!')
  245. expect(lastReport.date.getTime()).to.be.equal(
  246. newestReport.date.getTime(),
  247. 'Last report is not the newest.')
  248. })
  249. })
  250. describe('getUploadToServer()', () => {
  251. it('throws an error when called from the renderer process', () => {
  252. assert.throws(() => require('electron').crashReporter.getUploadToServer())
  253. })
  254. it('returns true when uploadToServer is set to true', function () {
  255. if (process.platform !== 'darwin') {
  256. // FIXME(alexeykuzmin): Skip the test.
  257. // this.skip()
  258. return
  259. }
  260. crashReporter.start({
  261. companyName: 'Umbrella Corporation',
  262. submitURL: 'http://127.0.0.1/crashes',
  263. uploadToServer: true
  264. })
  265. assert.strictEqual(crashReporter.getUploadToServer(), true)
  266. })
  267. it('returns false when uploadToServer is set to false', function () {
  268. if (process.platform !== 'darwin') {
  269. // FIXME(alexeykuzmin): Skip the test.
  270. // this.skip()
  271. return
  272. }
  273. crashReporter.start({
  274. companyName: 'Umbrella Corporation',
  275. submitURL: 'http://127.0.0.1/crashes',
  276. uploadToServer: true
  277. })
  278. crashReporter.setUploadToServer(false)
  279. assert.strictEqual(crashReporter.getUploadToServer(), false)
  280. })
  281. })
  282. describe('setUploadToServer(uploadToServer)', () => {
  283. it('throws an error when called from the renderer process', () => {
  284. assert.throws(() => require('electron').crashReporter.setUploadToServer('arg'))
  285. })
  286. it('sets uploadToServer false when called with false', function () {
  287. if (process.platform !== 'darwin') {
  288. // FIXME(alexeykuzmin): Skip the test.
  289. // this.skip()
  290. return
  291. }
  292. crashReporter.start({
  293. companyName: 'Umbrella Corporation',
  294. submitURL: 'http://127.0.0.1/crashes',
  295. uploadToServer: true
  296. })
  297. crashReporter.setUploadToServer(false)
  298. assert.strictEqual(crashReporter.getUploadToServer(), false)
  299. })
  300. it('sets uploadToServer true when called with true', function () {
  301. if (process.platform !== 'darwin') {
  302. // FIXME(alexeykuzmin): Skip the test.
  303. // this.skip()
  304. return
  305. }
  306. crashReporter.start({
  307. companyName: 'Umbrella Corporation',
  308. submitURL: 'http://127.0.0.1/crashes',
  309. uploadToServer: false
  310. })
  311. crashReporter.setUploadToServer(true)
  312. assert.strictEqual(crashReporter.getUploadToServer(), true)
  313. })
  314. })
  315. describe('Parameters', () => {
  316. it('returns all of the current parameters', () => {
  317. crashReporter.start({
  318. companyName: 'Umbrella Corporation',
  319. submitURL: 'http://127.0.0.1/crashes'
  320. })
  321. const parameters = crashReporter.getParameters()
  322. assert(typeof parameters === 'object')
  323. })
  324. it('adds a parameter to current parameters', function () {
  325. if (process.platform !== 'darwin') {
  326. // FIXME(alexeykuzmin): Skip the test.
  327. // this.skip()
  328. return
  329. }
  330. crashReporter.start({
  331. companyName: 'Umbrella Corporation',
  332. submitURL: 'http://127.0.0.1/crashes'
  333. })
  334. crashReporter.addExtraParameter('hello', 'world')
  335. assert('hello' in crashReporter.getParameters())
  336. })
  337. it('removes a parameter from current parameters', function () {
  338. if (process.platform !== 'darwin') {
  339. // FIXME(alexeykuzmin): Skip the test.
  340. // this.skip()
  341. return
  342. }
  343. crashReporter.start({
  344. companyName: 'Umbrella Corporation',
  345. submitURL: 'http://127.0.0.1/crashes'
  346. })
  347. crashReporter.addExtraParameter('hello', 'world')
  348. assert('hello' in crashReporter.getParameters())
  349. crashReporter.removeExtraParameter('hello')
  350. assert(!('hello' in crashReporter.getParameters()))
  351. })
  352. })
  353. })
  354. const waitForCrashReport = () => {
  355. return new Promise((resolve, reject) => {
  356. let times = 0
  357. const checkForReport = () => {
  358. if (crashReporter.getLastCrashReport() != null) {
  359. resolve()
  360. } else if (times >= 10) {
  361. reject(new Error('No crash report available'))
  362. } else {
  363. times++
  364. setTimeout(checkForReport, 100)
  365. }
  366. }
  367. checkForReport()
  368. })
  369. }
  370. const startServer = ({ callback, processType, done }) => {
  371. let called = false
  372. const server = http.createServer((req, res) => {
  373. const form = new multiparty.Form()
  374. form.parse(req, (error, fields) => {
  375. if (error) throw error
  376. if (called) return
  377. called = true
  378. assert.deepStrictEqual(String(fields.prod), 'Electron')
  379. assert.strictEqual(String(fields.ver), process.versions.electron)
  380. assert.strictEqual(String(fields.process_type), processType)
  381. assert.strictEqual(String(fields.platform), process.platform)
  382. assert.strictEqual(String(fields.extra1), 'extra1')
  383. assert.strictEqual(String(fields.extra2), 'extra2')
  384. assert.strictEqual(fields.extra3, undefined)
  385. assert.strictEqual(String(fields._productName), 'Zombies')
  386. assert.strictEqual(String(fields._companyName), 'Umbrella Corporation')
  387. assert.strictEqual(String(fields._version), app.getVersion())
  388. const reportId = 'abc-123-def-456-abc-789-abc-123-abcd'
  389. res.end(reportId, () => {
  390. waitForCrashReport().then(() => {
  391. assert.strictEqual(crashReporter.getLastCrashReport().id, reportId)
  392. assert.notStrictEqual(crashReporter.getUploadedReports().length, 0)
  393. assert.strictEqual(crashReporter.getUploadedReports()[0].id, reportId)
  394. req.socket.destroy()
  395. done()
  396. }, done)
  397. })
  398. })
  399. })
  400. const activeConnections = new Set()
  401. server.on('connection', (connection) => {
  402. activeConnections.add(connection)
  403. connection.once('close', () => {
  404. activeConnections.delete(connection)
  405. })
  406. })
  407. let { port } = remote.process
  408. server.listen(port, '127.0.0.1', () => {
  409. port = server.address().port
  410. remote.process.port = port
  411. if (process.platform === 'darwin') {
  412. crashReporter.start({
  413. companyName: 'Umbrella Corporation',
  414. submitURL: 'http://127.0.0.1:' + port
  415. })
  416. }
  417. callback(port)
  418. })
  419. return function stopServer (done) {
  420. for (const connection of activeConnections) {
  421. connection.destroy()
  422. }
  423. server.close(() => {
  424. done()
  425. })
  426. }
  427. }
  428. const stopCrashService = () => {
  429. const { crashServicePid } = remote.process
  430. if (crashServicePid) {
  431. remote.process.crashServicePid = 0
  432. try {
  433. process.kill(crashServicePid)
  434. } catch (error) {
  435. if (error.code !== 'ESRCH') {
  436. throw error
  437. }
  438. }
  439. }
  440. }