api-crash-reporter-spec.js 17 KB

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