api-crash-reporter-spec.js 11 KB

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