api-crash-reporter-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import { expect } from 'chai'
  2. import * as childProcess from 'child_process'
  3. import * as fs from 'fs'
  4. import * as http from 'http'
  5. import * as multiparty from 'multiparty'
  6. import * as path from 'path'
  7. import { ifdescribe, ifit } from './spec-helpers'
  8. import * as temp from 'temp'
  9. import * as url from 'url'
  10. import { ipcMain, app, BrowserWindow, crashReporter, BrowserWindowConstructorOptions } from 'electron'
  11. import { AddressInfo } from 'net'
  12. import { closeWindow, closeAllWindows } from './window-helpers'
  13. import { EventEmitter } from 'events'
  14. temp.track()
  15. const afterTest: ((() => void) | (() => Promise<void>))[] = []
  16. async function cleanup() {
  17. for (const cleanup of afterTest) {
  18. const r = cleanup()
  19. if (r instanceof Promise)
  20. await r
  21. }
  22. afterTest.length = 0
  23. }
  24. // TODO(alexeykuzmin): [Ch66] This test fails on Linux. Fix it and enable back.
  25. ifdescribe(!process.mas && !process.env.DISABLE_CRASH_REPORTER_TESTS && process.platform !== 'linux')('crashReporter module', function () {
  26. let originalTempDirectory: string
  27. let tempDirectory = null
  28. const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures')
  29. before(() => {
  30. tempDirectory = temp.mkdirSync('electronCrashReporterSpec-')
  31. originalTempDirectory = app.getPath('temp')
  32. app.setPath('temp', tempDirectory)
  33. })
  34. after(() => {
  35. app.setPath('temp', originalTempDirectory)
  36. try {
  37. temp.cleanupSync()
  38. } catch (e) {
  39. // ignore.
  40. console.warn(e.stack)
  41. }
  42. })
  43. afterEach(cleanup)
  44. it('should send minidump when node processes crash', async () => {
  45. const { port, waitForCrash } = await startServer()
  46. const crashesDir = path.join(app.getPath('temp'), `${app.name} Crashes`)
  47. const version = app.getVersion()
  48. const crashPath = path.join(fixtures, 'module', 'crash.js')
  49. childProcess.fork(crashPath, [port.toString(), version, crashesDir], { silent: true })
  50. const crash = await waitForCrash()
  51. checkCrash('node', crash)
  52. })
  53. const generateSpecs = (description: string, browserWindowOpts: BrowserWindowConstructorOptions) => {
  54. describe(description, () => {
  55. let w: BrowserWindow
  56. beforeEach(() => {
  57. w = new BrowserWindow(Object.assign({ show: false }, browserWindowOpts))
  58. })
  59. afterEach(async () => {
  60. await closeWindow(w)
  61. w = null as unknown as BrowserWindow
  62. })
  63. it('should send minidump when renderer crashes', async () => {
  64. const { port, waitForCrash } = await startServer()
  65. w.loadFile(path.join(fixtures, 'api', 'crash.html'), { query: { port: port.toString() } })
  66. const crash = await waitForCrash()
  67. checkCrash('renderer', crash)
  68. })
  69. ifit(!browserWindowOpts.webPreferences!.sandbox)('should send minidump when node processes crash', async function () {
  70. const { port, waitForCrash } = await startServer()
  71. const crashesDir = path.join(app.getPath('temp'), `${app.name} Crashes`)
  72. const version = app.getVersion()
  73. const crashPath = path.join(fixtures, 'module', 'crash.js')
  74. w.loadFile(path.join(fixtures, 'api', 'crash_child.html'), { query: { port: port.toString(), crashesDir, crashPath, version } })
  75. const crash = await waitForCrash()
  76. expect(String((crash as any).newExtra)).to.equal('newExtra')
  77. expect((crash as any).removeExtra).to.be.undefined()
  78. checkCrash('node', crash)
  79. })
  80. describe('when uploadToServer is false', () => {
  81. after(() => { crashReporter.setUploadToServer(true) })
  82. it('should not send minidump', async () => {
  83. const { port, getCrashes } = await startServer()
  84. crashReporter.setUploadToServer(false)
  85. let crashesDir = crashReporter.getCrashesDirectory()
  86. const existingDumpFiles = new Set()
  87. // crashpad puts the dump files in the "completed" subdirectory
  88. if (process.platform === 'darwin') {
  89. crashesDir = path.join(crashesDir, 'completed')
  90. } else {
  91. crashesDir = path.join(crashesDir, 'reports')
  92. }
  93. const crashUrl = url.format({
  94. protocol: 'file',
  95. pathname: path.join(fixtures, 'api', 'crash.html'),
  96. search: `?port=${port}&skipUpload=1`
  97. })
  98. w.loadURL(crashUrl)
  99. await new Promise(resolve => {
  100. ipcMain.once('list-existing-dumps', (event) => {
  101. fs.readdir(crashesDir, (err, files) => {
  102. if (!err) {
  103. for (const file of files) {
  104. if (/\.dmp$/.test(file)) {
  105. existingDumpFiles.add(file)
  106. }
  107. }
  108. }
  109. event.returnValue = null // allow the renderer to crash
  110. resolve()
  111. })
  112. })
  113. })
  114. const dumpFileCreated = async () => {
  115. async function getDumps() {
  116. const files = await fs.promises.readdir(crashesDir)
  117. return files.filter((file) => /\.dmp$/.test(file) && !existingDumpFiles.has(file))
  118. }
  119. for (let i = 0; i < 30; i++) {
  120. const dumps = await getDumps()
  121. if (dumps.length) {
  122. return path.join(crashesDir, dumps[0])
  123. }
  124. await new Promise(resolve => setTimeout(resolve, 1000))
  125. }
  126. }
  127. const dumpFile = await dumpFileCreated()
  128. expect(dumpFile).to.be.a('string')
  129. // dump file should not be deleted when not uploading, so we wait
  130. // 1s and assert it still exists
  131. await new Promise(resolve => setTimeout(resolve, 1000))
  132. expect(fs.existsSync(dumpFile!)).to.be.true()
  133. // the server should not have received any crashes.
  134. expect(getCrashes()).to.be.empty()
  135. })
  136. })
  137. it('should send minidump with updated extra parameters', async function () {
  138. const { port, waitForCrash } = await startServer()
  139. const crashUrl = url.format({
  140. protocol: 'file',
  141. pathname: path.join(fixtures, 'api', 'crash-restart.html'),
  142. search: `?port=${port}`
  143. })
  144. w.loadURL(crashUrl)
  145. const crash = await waitForCrash()
  146. checkCrash('renderer', crash)
  147. })
  148. })
  149. }
  150. generateSpecs('without sandbox', {
  151. webPreferences: {
  152. nodeIntegration: true
  153. }
  154. })
  155. generateSpecs('with sandbox', {
  156. webPreferences: {
  157. sandbox: true,
  158. preload: path.join(fixtures, 'module', 'preload-sandbox.js')
  159. }
  160. })
  161. describe('start(options)', () => {
  162. it('requires that the companyName and submitURL options be specified', () => {
  163. expect(() => {
  164. crashReporter.start({ companyName: 'Missing submitURL' } as any)
  165. }).to.throw('submitURL is a required option to crashReporter.start')
  166. expect(() => {
  167. crashReporter.start({ submitURL: 'Missing companyName' } as any)
  168. }).to.throw('companyName is a required option to crashReporter.start')
  169. })
  170. it('can be called multiple times', () => {
  171. expect(() => {
  172. crashReporter.start({
  173. companyName: 'Umbrella Corporation',
  174. submitURL: 'http://127.0.0.1/crashes'
  175. })
  176. crashReporter.start({
  177. companyName: 'Umbrella Corporation 2',
  178. submitURL: 'http://127.0.0.1/more-crashes'
  179. })
  180. }).to.not.throw()
  181. })
  182. })
  183. describe('getCrashesDirectory', () => {
  184. it('correctly returns the directory', () => {
  185. const crashesDir = crashReporter.getCrashesDirectory()
  186. const dir = path.join(app.getPath('temp'), 'Electron Test Main Crashes')
  187. expect(crashesDir).to.equal(dir)
  188. })
  189. })
  190. describe('getUploadedReports', () => {
  191. it('returns an array of reports', () => {
  192. const reports = crashReporter.getUploadedReports()
  193. expect(reports).to.be.an('array')
  194. })
  195. })
  196. // TODO(alexeykuzmin): This suite should explicitly
  197. // generate several crash reports instead of hoping
  198. // that there will be enough of them already.
  199. describe('getLastCrashReport', () => {
  200. it('correctly returns the most recent report', () => {
  201. const reports = crashReporter.getUploadedReports()
  202. expect(reports).to.be.an('array')
  203. expect(reports).to.have.lengthOf.at.least(2,
  204. 'There are not enough reports for this test')
  205. const lastReport = crashReporter.getLastCrashReport()
  206. expect(lastReport).to.be.an('object')
  207. expect(lastReport.date).to.be.an.instanceOf(Date)
  208. // Let's find the newest report.
  209. const { report: newestReport } = reports.reduce((acc, cur) => {
  210. const timestamp = new Date(cur.date).getTime()
  211. return (timestamp > acc.timestamp)
  212. ? { report: cur, timestamp: timestamp }
  213. : acc
  214. }, { timestamp: -Infinity } as { timestamp: number, report?: any })
  215. expect(newestReport).to.be.an('object')
  216. expect(lastReport.date.getTime()).to.be.equal(
  217. newestReport.date.getTime(),
  218. 'Last report is not the newest.')
  219. })
  220. })
  221. describe('getUploadToServer()', () => {
  222. it('returns true when uploadToServer is set to true', function () {
  223. crashReporter.start({
  224. companyName: 'Umbrella Corporation',
  225. submitURL: 'http://127.0.0.1/crashes',
  226. uploadToServer: true
  227. })
  228. expect(crashReporter.getUploadToServer()).to.be.true()
  229. })
  230. it('returns false when uploadToServer is set to false', function () {
  231. crashReporter.start({
  232. companyName: 'Umbrella Corporation',
  233. submitURL: 'http://127.0.0.1/crashes',
  234. uploadToServer: true
  235. })
  236. crashReporter.setUploadToServer(false)
  237. expect(crashReporter.getUploadToServer()).to.be.false()
  238. })
  239. })
  240. describe('setUploadToServer(uploadToServer)', () => {
  241. afterEach(closeAllWindows)
  242. it('throws an error when called from the renderer process', async () => {
  243. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
  244. w.loadURL('about:blank')
  245. await expect(
  246. w.webContents.executeJavaScript(`require('electron').crashReporter.setUploadToServer(true)`)
  247. ).to.eventually.be.rejected()
  248. await expect(
  249. w.webContents.executeJavaScript(`require('electron').crashReporter.getUploadToServer()`)
  250. ).to.eventually.be.rejected()
  251. })
  252. it('sets uploadToServer false when called with false', function () {
  253. crashReporter.start({
  254. companyName: 'Umbrella Corporation',
  255. submitURL: 'http://127.0.0.1/crashes',
  256. uploadToServer: true
  257. })
  258. crashReporter.setUploadToServer(false)
  259. expect(crashReporter.getUploadToServer()).to.be.false()
  260. })
  261. it('sets uploadToServer true when called with true', function () {
  262. crashReporter.start({
  263. companyName: 'Umbrella Corporation',
  264. submitURL: 'http://127.0.0.1/crashes',
  265. uploadToServer: false
  266. })
  267. crashReporter.setUploadToServer(true)
  268. expect(crashReporter.getUploadToServer()).to.be.true()
  269. })
  270. })
  271. describe('Parameters', () => {
  272. it('returns all of the current parameters', () => {
  273. crashReporter.start({
  274. companyName: 'Umbrella Corporation',
  275. submitURL: 'http://127.0.0.1/crashes'
  276. })
  277. const parameters = crashReporter.getParameters()
  278. expect(parameters).to.be.an('object')
  279. })
  280. it('adds a parameter to current parameters', function () {
  281. crashReporter.start({
  282. companyName: 'Umbrella Corporation',
  283. submitURL: 'http://127.0.0.1/crashes'
  284. })
  285. crashReporter.addExtraParameter('hello', 'world')
  286. expect(crashReporter.getParameters()).to.have.property('hello')
  287. })
  288. it('removes a parameter from current parameters', function () {
  289. crashReporter.start({
  290. companyName: 'Umbrella Corporation',
  291. submitURL: 'http://127.0.0.1/crashes'
  292. })
  293. crashReporter.addExtraParameter('hello', 'world')
  294. expect(crashReporter.getParameters()).to.have.property('hello')
  295. crashReporter.removeExtraParameter('hello')
  296. expect(crashReporter.getParameters()).to.not.have.property('hello')
  297. })
  298. })
  299. describe('when not started', () => {
  300. it('does not prevent process from crashing', (done) => {
  301. const appPath = path.join(fixtures, 'api', 'cookie-app')
  302. const appProcess = childProcess.spawn(process.execPath, [appPath])
  303. appProcess.once('close', () => {
  304. done()
  305. })
  306. })
  307. })
  308. })
  309. type CrashInfo = {
  310. prod: string
  311. ver: string
  312. process_type: string
  313. platform: string
  314. extra1: string
  315. extra2: string
  316. extra3: undefined
  317. _productName: string
  318. _companyName: string
  319. _version: string
  320. }
  321. async function waitForCrashReport() {
  322. for (let times = 0; times < 10; times++) {
  323. if (crashReporter.getLastCrashReport() != null) {
  324. return
  325. }
  326. await new Promise(resolve => setTimeout(resolve, 100))
  327. }
  328. throw new Error('No crash report available')
  329. }
  330. async function checkReport(reportId: string) {
  331. await waitForCrashReport()
  332. expect(crashReporter.getLastCrashReport().id).to.equal(reportId)
  333. expect(crashReporter.getUploadedReports()).to.be.an('array').that.is.not.empty()
  334. expect(crashReporter.getUploadedReports()[0].id).to.equal(reportId)
  335. }
  336. function checkCrash(expectedProcessType: string, fields: CrashInfo) {
  337. expect(String(fields.prod)).to.equal('Electron')
  338. expect(String(fields.ver)).to.equal(process.versions.electron)
  339. expect(String(fields.process_type)).to.equal(expectedProcessType)
  340. expect(String(fields.platform)).to.equal(process.platform)
  341. expect(String(fields.extra1)).to.equal('extra1')
  342. expect(String(fields.extra2)).to.equal('extra2')
  343. expect(fields.extra3).to.be.undefined()
  344. expect(String(fields._productName)).to.equal('Zombies')
  345. expect(String(fields._companyName)).to.equal('Umbrella Corporation')
  346. expect(String(fields._version)).to.equal(app.getVersion())
  347. }
  348. let crashReporterPort = 0
  349. const startServer = async () => {
  350. const crashes: CrashInfo[] = []
  351. function getCrashes() { return crashes }
  352. const emitter = new EventEmitter
  353. function waitForCrash(): Promise<CrashInfo> {
  354. return new Promise(resolve => {
  355. emitter.once('crash', (crash) => {
  356. resolve(crash)
  357. })
  358. })
  359. }
  360. const server = http.createServer((req, res) => {
  361. const form = new multiparty.Form()
  362. form.parse(req, (error, fields) => {
  363. crashes.push(fields)
  364. if (error) throw error
  365. const reportId = 'abc-123-def-456-abc-789-abc-123-abcd'
  366. res.end(reportId, async () => {
  367. await checkReport(reportId)
  368. req.socket.destroy()
  369. emitter.emit('crash', fields)
  370. })
  371. })
  372. })
  373. await new Promise(resolve => {
  374. server.listen(crashReporterPort, '127.0.0.1', () => { resolve() })
  375. })
  376. const port = (server.address() as AddressInfo).port
  377. if (crashReporterPort === 0) {
  378. // We can only start the crash reporter once, and after that these
  379. // parameters are fixed.
  380. crashReporter.start({
  381. companyName: 'Umbrella Corporation',
  382. submitURL: 'http://127.0.0.1:' + port
  383. })
  384. crashReporterPort = port
  385. }
  386. afterTest.push(() => { server.close() })
  387. return { getCrashes, port, waitForCrash }
  388. }