api-crash-reporter-spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. const assert = require('assert')
  2. const childProcess = require('child_process')
  3. const fs = require('fs')
  4. const http = require('http')
  5. const multiparty = require('multiparty')
  6. const path = require('path')
  7. const temp = require('temp').track()
  8. const url = require('url')
  9. const {closeWindow} = require('./window-helpers')
  10. const {remote} = require('electron')
  11. const isCI = remote.getGlobal('isCi')
  12. const {app, BrowserWindow, crashReporter} = remote.require('electron')
  13. describe('crashReporter module', function () {
  14. if (process.mas) {
  15. return
  16. }
  17. // FIXME internal Linux CI is failing when it detects a process crashes
  18. // which is a false positive here since crashes are explicitly triggered
  19. if (isCI && process.platform === 'linux') {
  20. return
  21. }
  22. var originalTempDirectory = null
  23. var tempDirectory = null
  24. before(function () {
  25. tempDirectory = temp.mkdirSync('electronCrashReporterSpec-')
  26. originalTempDirectory = app.getPath('temp')
  27. app.setPath('temp', tempDirectory)
  28. })
  29. after(function () {
  30. app.setPath('temp', originalTempDirectory)
  31. })
  32. var fixtures = path.resolve(__dirname, 'fixtures')
  33. const generateSpecs = (description, browserWindowOpts) => {
  34. describe(description, function () {
  35. var w = null
  36. var stopServer = null
  37. beforeEach(function () {
  38. stopServer = null
  39. w = new BrowserWindow(Object.assign({
  40. show: false
  41. }, browserWindowOpts))
  42. })
  43. afterEach(function () {
  44. return closeWindow(w).then(function () { w = null })
  45. })
  46. afterEach(function () {
  47. stopCrashService()
  48. })
  49. afterEach(function (done) {
  50. if (stopServer != null) {
  51. stopServer(done)
  52. } else {
  53. done()
  54. }
  55. })
  56. it('should send minidump when renderer crashes', function (done) {
  57. if (process.env.APPVEYOR === 'True') return done()
  58. if (process.env.TRAVIS === 'true') return done()
  59. this.timeout(120000)
  60. stopServer = startServer({
  61. callback (port) {
  62. const crashUrl = url.format({
  63. protocol: 'file',
  64. pathname: path.join(fixtures, 'api', 'crash.html'),
  65. search: '?port=' + port
  66. })
  67. w.loadURL(crashUrl)
  68. },
  69. processType: 'renderer',
  70. done: done
  71. })
  72. })
  73. it('should send minidump when node processes crash', function (done) {
  74. if (process.env.APPVEYOR === 'True') return done()
  75. if (process.env.TRAVIS === 'true') return done()
  76. this.timeout(120000)
  77. stopServer = startServer({
  78. callback (port) {
  79. const crashesDir = path.join(app.getPath('temp'), `${process.platform === 'win32' ? 'Zombies' : app.getName()} Crashes`)
  80. const version = app.getVersion()
  81. const crashPath = path.join(fixtures, 'module', 'crash.js')
  82. if (process.platform === 'win32') {
  83. const crashServiceProcess = childProcess.spawn(process.execPath, [
  84. `--reporter-url=http://127.0.0.1:${port}`,
  85. '--application-name=Zombies',
  86. `--crashes-directory=${crashesDir}`
  87. ], {
  88. env: {
  89. ELECTRON_INTERNAL_CRASH_SERVICE: 1
  90. },
  91. detached: true
  92. })
  93. remote.process.crashServicePid = crashServiceProcess.pid
  94. }
  95. childProcess.fork(crashPath, [port, version, crashesDir], {silent: true})
  96. },
  97. processType: 'browser',
  98. done: done
  99. })
  100. })
  101. it('should not send minidump if uploadToServer is false', function (done) {
  102. this.timeout(120000)
  103. let dumpFile
  104. let crashesDir = crashReporter.getCrashesDirectory()
  105. const existingDumpFiles = new Set()
  106. if (process.platform === 'darwin') {
  107. // crashpad puts the dump files in the "completed" subdirectory
  108. crashesDir = path.join(crashesDir, 'completed')
  109. crashReporter.setUploadToServer(false)
  110. }
  111. const testDone = (uploaded) => {
  112. if (uploaded) {
  113. return done(new Error('Uploaded crash report'))
  114. }
  115. if (process.platform === 'darwin') {
  116. crashReporter.setUploadToServer(true)
  117. }
  118. assert(fs.existsSync(dumpFile))
  119. done()
  120. }
  121. let pollInterval
  122. const pollDumpFile = () => {
  123. fs.readdir(crashesDir, (err, files) => {
  124. if (err) {
  125. return
  126. }
  127. const dumps = files.filter((file) => /\.dmp$/.test(file) && !existingDumpFiles.has(file))
  128. if (!dumps.length) {
  129. return
  130. }
  131. assert.equal(1, dumps.length)
  132. dumpFile = path.join(crashesDir, dumps[0])
  133. clearInterval(pollInterval)
  134. // dump file should not be deleted when not uploading, so we wait
  135. // 1s and assert it still exists in `testDone`
  136. setTimeout(testDone, 1000)
  137. })
  138. }
  139. remote.ipcMain.once('list-existing-dumps', (event) => {
  140. fs.readdir(crashesDir, (err, files) => {
  141. if (!err) {
  142. for (const file of files) {
  143. if (/\.dmp$/.test(file)) {
  144. existingDumpFiles.add(file)
  145. }
  146. }
  147. }
  148. event.returnValue = null // allow the renderer to crash
  149. pollInterval = setInterval(pollDumpFile, 100)
  150. })
  151. })
  152. stopServer = startServer({
  153. callback (port) {
  154. const crashUrl = url.format({
  155. protocol: 'file',
  156. pathname: path.join(fixtures, 'api', 'crash.html'),
  157. search: `?port=${port}&skipUpload=1`
  158. })
  159. w.loadURL(crashUrl)
  160. },
  161. processType: 'renderer',
  162. done: testDone.bind(null, true)
  163. })
  164. })
  165. it('should send minidump with updated extra parameters', function (done) {
  166. if (process.env.APPVEYOR === 'True') return done()
  167. if (process.env.TRAVIS === 'true') return done()
  168. this.timeout(120000)
  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. generateSpecs('with sandbox', {
  186. webPreferences: {
  187. sandbox: true,
  188. preload: path.join(fixtures, 'module', 'preload-sandbox.js')
  189. }
  190. })
  191. describe('.start(options)', function () {
  192. it('requires that the companyName and submitURL options be specified', function () {
  193. assert.throws(function () {
  194. crashReporter.start({
  195. companyName: 'Missing submitURL'
  196. })
  197. }, /submitURL is a required option to crashReporter\.start/)
  198. assert.throws(function () {
  199. crashReporter.start({
  200. submitURL: 'Missing companyName'
  201. })
  202. }, /companyName is a required option to crashReporter\.start/)
  203. })
  204. it('can be called multiple times', function () {
  205. assert.doesNotThrow(function () {
  206. crashReporter.start({
  207. companyName: 'Umbrella Corporation',
  208. submitURL: 'http://127.0.0.1/crashes'
  209. })
  210. crashReporter.start({
  211. companyName: 'Umbrella Corporation 2',
  212. submitURL: 'http://127.0.0.1/more-crashes'
  213. })
  214. })
  215. })
  216. })
  217. describe('.get/setUploadToServer', function () {
  218. it('throws an error when called from the renderer process', function () {
  219. assert.throws(() => require('electron').crashReporter.getUploadToServer())
  220. })
  221. it('can be read/set from the main process', function () {
  222. if (process.platform === 'darwin') {
  223. crashReporter.start({
  224. companyName: 'Umbrella Corporation',
  225. submitURL: 'http://127.0.0.1/crashes',
  226. uploadToServer: true
  227. })
  228. assert.equal(crashReporter.getUploadToServer(), true)
  229. crashReporter.setUploadToServer(false)
  230. assert.equal(crashReporter.getUploadToServer(), false)
  231. } else {
  232. assert.equal(crashReporter.getUploadToServer(), true)
  233. }
  234. })
  235. })
  236. })
  237. const waitForCrashReport = () => {
  238. return new Promise((resolve, reject) => {
  239. let times = 0
  240. const checkForReport = () => {
  241. if (crashReporter.getLastCrashReport() != null) {
  242. resolve()
  243. } else if (times >= 10) {
  244. reject(new Error('No crash report available'))
  245. } else {
  246. times++
  247. setTimeout(checkForReport, 100)
  248. }
  249. }
  250. checkForReport()
  251. })
  252. }
  253. const startServer = ({callback, processType, done}) => {
  254. var called = false
  255. var server = http.createServer((req, res) => {
  256. var form = new multiparty.Form()
  257. form.parse(req, (error, fields) => {
  258. if (error) throw error
  259. if (called) return
  260. called = true
  261. assert.equal(fields.prod, 'Electron')
  262. assert.equal(fields.ver, process.versions.electron)
  263. assert.equal(fields.process_type, processType)
  264. assert.equal(fields.platform, process.platform)
  265. assert.equal(fields.extra1, 'extra1')
  266. assert.equal(fields.extra2, 'extra2')
  267. assert.equal(fields.extra3, undefined)
  268. assert.equal(fields._productName, 'Zombies')
  269. assert.equal(fields._companyName, 'Umbrella Corporation')
  270. assert.equal(fields._version, app.getVersion())
  271. const reportId = 'abc-123-def-456-abc-789-abc-123-abcd'
  272. res.end(reportId, () => {
  273. waitForCrashReport().then(() => {
  274. assert.equal(crashReporter.getLastCrashReport().id, reportId)
  275. assert.notEqual(crashReporter.getUploadedReports().length, 0)
  276. assert.equal(crashReporter.getUploadedReports()[0].id, reportId)
  277. req.socket.destroy()
  278. done()
  279. }, done)
  280. })
  281. })
  282. })
  283. const activeConnections = new Set()
  284. server.on('connection', (connection) => {
  285. activeConnections.add(connection)
  286. connection.once('close', () => {
  287. activeConnections.delete(connection)
  288. })
  289. })
  290. let {port} = remote.process
  291. server.listen(port, '127.0.0.1', () => {
  292. port = server.address().port
  293. remote.process.port = port
  294. if (process.platform === 'darwin') {
  295. crashReporter.start({
  296. companyName: 'Umbrella Corporation',
  297. submitURL: 'http://127.0.0.1:' + port
  298. })
  299. }
  300. callback(port)
  301. })
  302. return function stopServer (done) {
  303. for (const connection of activeConnections) {
  304. connection.destroy()
  305. }
  306. server.close(function () {
  307. done()
  308. })
  309. }
  310. }
  311. const stopCrashService = () => {
  312. const {crashServicePid} = remote.process
  313. if (crashServicePid) {
  314. remote.process.crashServicePid = 0
  315. try {
  316. process.kill(crashServicePid)
  317. } catch (error) {
  318. if (error.code !== 'ESRCH') {
  319. throw error
  320. }
  321. }
  322. }
  323. }