api-crash-reporter-spec.js 15 KB

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