diff --git a/package.json b/package.json index 06b109e2f5..287c554e11 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@octokit/rest": "^16.3.2", "@primer/octicons": "^9.1.1", "@types/basic-auth": "^1.1.3", + "@types/busboy": "^0.2.3", "@types/chai": "^4.2.11", "@types/chai-as-promised": "^7.1.2", "@types/dirty-chai": "^2.0.2", @@ -139,7 +140,6 @@ ] }, "dependencies": { - "@types/multiparty": "^0.0.32", "@types/temp": "^0.8.34" } -} \ No newline at end of file +} diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index dd219a92b0..0eb79c11e7 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -1995,7 +1995,7 @@ describe('BrowserWindow module', () => { } } - const preload = path.join(fixtures, 'module', 'preload-sandbox.js'); + const preload = path.join(path.resolve(__dirname, 'fixtures'), 'module', 'preload-sandbox.js'); let server: http.Server = null as unknown as http.Server; let serverUrl: string = null as unknown as string; diff --git a/spec-main/api-crash-reporter-spec.ts b/spec-main/api-crash-reporter-spec.ts index 2abce6796d..628e5b7158 100644 --- a/spec-main/api-crash-reporter-spec.ts +++ b/spec-main/api-crash-reporter-spec.ts @@ -1,17 +1,16 @@ import { expect } from 'chai'; import * as childProcess from 'child_process'; -import * as fs from 'fs'; import * as http from 'http'; -import * as multiparty from 'multiparty'; +import * as Busboy from 'busboy'; import * as path from 'path'; -import { ifdescribe, ifit } from './spec-helpers'; +import { ifdescribe } from './spec-helpers'; import * as temp from 'temp'; -import * as url from 'url'; -import { ipcMain, app, BrowserWindow, BrowserWindowConstructorOptions } from 'electron/main'; +import { app } from 'electron/main'; import { crashReporter } from 'electron/common'; import { AddressInfo } from 'net'; -import { closeWindow, closeAllWindows } from './window-helpers'; import { EventEmitter } from 'events'; +import * as fs from 'fs'; +import * as v8 from 'v8'; temp.track(); @@ -24,330 +23,6 @@ async function cleanup () { afterTest.length = 0; } -// TODO(alexeykuzmin): [Ch66] This test fails on Linux. Fix it and enable back. -ifdescribe(!process.mas && !process.env.DISABLE_CRASH_REPORTER_TESTS && process.platform !== 'linux')('crashReporter module', function () { - let originalTempDirectory: string; - let tempDirectory = null; - const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures'); - - before(() => { - tempDirectory = temp.mkdirSync('electronCrashReporterSpec-'); - originalTempDirectory = app.getPath('temp'); - app.setPath('temp', tempDirectory); - }); - - after(() => { - app.setPath('temp', originalTempDirectory); - try { - temp.cleanupSync(); - } catch (e) { - // ignore. - console.warn(e.stack); - } - }); - - afterEach(cleanup); - - it('should send minidump when node processes crash', async () => { - const { port, waitForCrash } = await startServer(); - - const crashesDir = path.join(app.getPath('temp'), `${app.name} Crashes`); - const version = app.getVersion(); - const crashPath = path.join(fixtures, 'module', 'crash.js'); - childProcess.fork(crashPath, [port.toString(), version, crashesDir], { silent: true }); - const crash = await waitForCrash(); - checkCrash('node', crash); - }); - - const generateSpecs = (description: string, browserWindowOpts: BrowserWindowConstructorOptions) => { - describe(description, () => { - let w: BrowserWindow; - - beforeEach(() => { - w = new BrowserWindow(Object.assign({ show: false }, browserWindowOpts)); - }); - - afterEach(async () => { - await closeWindow(w); - w = null as unknown as BrowserWindow; - }); - - it('should send minidump when renderer crashes', async () => { - const { port, waitForCrash } = await startServer(); - w.loadFile(path.join(fixtures, 'api', 'crash.html'), { query: { port: port.toString() } }); - const crash = await waitForCrash(); - checkCrash('renderer', crash); - }); - - ifit(!browserWindowOpts.webPreferences!.sandbox)('should send minidump when node processes crash', async function () { - const { port, waitForCrash } = await startServer(); - const crashesDir = path.join(app.getPath('temp'), `${app.name} Crashes`); - const version = app.getVersion(); - const crashPath = path.join(fixtures, 'module', 'crash.js'); - w.loadFile(path.join(fixtures, 'api', 'crash_child.html'), { query: { port: port.toString(), crashesDir, crashPath, version } }); - const crash = await waitForCrash(); - expect(String((crash as any).newExtra)).to.equal('newExtra'); - expect((crash as any).removeExtra).to.be.undefined(); - checkCrash('node', crash); - }); - - describe('when uploadToServer is false', () => { - after(() => { crashReporter.setUploadToServer(true); }); - - it('should not send minidump', async () => { - const { port, getCrashes } = await startServer(); - crashReporter.setUploadToServer(false); - - let crashesDir = crashReporter.getCrashesDirectory(); - const existingDumpFiles = new Set(); - // crashpad puts the dump files in the "completed" subdirectory - if (process.platform === 'darwin') { - crashesDir = path.join(crashesDir, 'completed'); - } else { - crashesDir = path.join(crashesDir, 'reports'); - } - - const crashUrl = url.format({ - protocol: 'file', - pathname: path.join(fixtures, 'api', 'crash.html'), - search: `?port=${port}&skipUpload=1` - }); - w.loadURL(crashUrl); - - await new Promise(resolve => { - ipcMain.once('list-existing-dumps', (event) => { - fs.readdir(crashesDir, (err, files) => { - if (!err) { - for (const file of files) { - if (/\.dmp$/.test(file)) { - existingDumpFiles.add(file); - } - } - } - event.returnValue = null; // allow the renderer to crash - resolve(); - }); - }); - }); - - const dumpFileCreated = async () => { - async function getDumps () { - const files = await fs.promises.readdir(crashesDir); - return files.filter((file) => /\.dmp$/.test(file) && !existingDumpFiles.has(file)); - } - for (let i = 0; i < 30; i++) { - const dumps = await getDumps(); - if (dumps.length) { - return path.join(crashesDir, dumps[0]); - } - await new Promise(resolve => setTimeout(resolve, 1000)); - } - }; - - const dumpFile = await dumpFileCreated(); - expect(dumpFile).to.be.a('string'); - - // dump file should not be deleted when not uploading, so we wait - // 1s and assert it still exists - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(fs.existsSync(dumpFile!)).to.be.true(); - - // the server should not have received any crashes. - expect(getCrashes()).to.be.empty(); - }); - }); - - it('should send minidump with updated extra parameters', async function () { - const { port, waitForCrash } = await startServer(); - - const crashUrl = url.format({ - protocol: 'file', - pathname: path.join(fixtures, 'api', 'crash-restart.html'), - search: `?port=${port}` - }); - w.loadURL(crashUrl); - const crash = await waitForCrash(); - checkCrash('renderer', crash); - }); - }); - }; - - generateSpecs('without sandbox', { - webPreferences: { - nodeIntegration: true - } - }); - generateSpecs('with sandbox', { - webPreferences: { - sandbox: true, - preload: path.join(fixtures, 'module', 'preload-sandbox.js') - } - }); - - describe('start(options)', () => { - it('requires that the companyName and submitURL options be specified', () => { - expect(() => { - crashReporter.start({ companyName: 'Missing submitURL' } as any); - }).to.throw('submitURL is a required option to crashReporter.start'); - expect(() => { - crashReporter.start({ submitURL: 'Missing companyName' } as any); - }).to.throw('companyName is a required option to crashReporter.start'); - }); - it('can be called multiple times', () => { - expect(() => { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }); - - crashReporter.start({ - companyName: 'Umbrella Corporation 2', - submitURL: 'http://127.0.0.1/more-crashes' - }); - }).to.not.throw(); - }); - }); - - describe('getCrashesDirectory', () => { - it('correctly returns the directory', () => { - const crashesDir = crashReporter.getCrashesDirectory(); - const dir = path.join(app.getPath('temp'), 'Electron Test Main Crashes'); - expect(crashesDir).to.equal(dir); - }); - }); - - describe('getUploadedReports', () => { - it('returns an array of reports', () => { - const reports = crashReporter.getUploadedReports(); - expect(reports).to.be.an('array'); - }); - }); - - // TODO(alexeykuzmin): This suite should explicitly - // generate several crash reports instead of hoping - // that there will be enough of them already. - describe('getLastCrashReport', () => { - it('correctly returns the most recent report', () => { - const reports = crashReporter.getUploadedReports(); - expect(reports).to.be.an('array'); - expect(reports).to.have.lengthOf.at.least(2, - 'There are not enough reports for this test'); - - const lastReport = crashReporter.getLastCrashReport(); - expect(lastReport).to.be.an('object'); - expect(lastReport.date).to.be.an.instanceOf(Date); - - // Let's find the newest report. - const { report: newestReport } = reports.reduce((acc, cur) => { - const timestamp = new Date(cur.date).getTime(); - return (timestamp > acc.timestamp) - ? { report: cur, timestamp: timestamp } - : acc; - }, { timestamp: -Infinity } as { timestamp: number, report?: any }); - expect(newestReport).to.be.an('object'); - - expect(lastReport.date.getTime()).to.be.equal( - newestReport.date.getTime(), - 'Last report is not the newest.'); - }); - }); - - describe('getUploadToServer()', () => { - it('returns true when uploadToServer is set to true', function () { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: true - }); - expect(crashReporter.getUploadToServer()).to.be.true(); - }); - it('returns false when uploadToServer is set to false', function () { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: true - }); - crashReporter.setUploadToServer(false); - expect(crashReporter.getUploadToServer()).to.be.false(); - }); - }); - - describe('setUploadToServer(uploadToServer)', () => { - afterEach(closeAllWindows); - it('throws an error when called from the renderer process', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); - w.loadURL('about:blank'); - await expect( - w.webContents.executeJavaScript('require(\'electron\').crashReporter.setUploadToServer(true)') - ).to.eventually.be.rejected(); - await expect( - w.webContents.executeJavaScript('require(\'electron\').crashReporter.getUploadToServer()') - ).to.eventually.be.rejected(); - }); - it('sets uploadToServer false when called with false', function () { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: true - }); - crashReporter.setUploadToServer(false); - expect(crashReporter.getUploadToServer()).to.be.false(); - }); - it('sets uploadToServer true when called with true', function () { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes', - uploadToServer: false - }); - crashReporter.setUploadToServer(true); - expect(crashReporter.getUploadToServer()).to.be.true(); - }); - }); - - describe('Parameters', () => { - it('returns all of the current parameters', () => { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }); - - const parameters = crashReporter.getParameters(); - expect(parameters).to.be.an('object'); - }); - it('adds a parameter to current parameters', function () { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }); - - crashReporter.addExtraParameter('hello', 'world'); - expect(crashReporter.getParameters()).to.have.property('hello'); - }); - it('removes a parameter from current parameters', function () { - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1/crashes' - }); - - crashReporter.addExtraParameter('hello', 'world'); - expect(crashReporter.getParameters()).to.have.property('hello'); - - crashReporter.removeExtraParameter('hello'); - expect(crashReporter.getParameters()).to.not.have.property('hello'); - }); - }); - - describe('when not started', () => { - it('does not prevent process from crashing', (done) => { - const appPath = path.join(fixtures, 'api', 'cookie-app'); - const appProcess = childProcess.spawn(process.execPath, [appPath]); - appProcess.once('exit', () => { - done(); - }); - }); - }); -}); - type CrashInfo = { prod: string ver: string @@ -359,23 +34,7 @@ type CrashInfo = { _productName: string _companyName: string _version: string -} - -async function waitForCrashReport () { - for (let times = 0; times < 10; times++) { - if (crashReporter.getLastCrashReport() != null) { - return; - } - await new Promise(resolve => setTimeout(resolve, 100)); - } - throw new Error('No crash report available'); -} - -async function checkReport (reportId: string) { - await waitForCrashReport(); - expect(crashReporter.getLastCrashReport().id).to.equal(reportId); - expect(crashReporter.getUploadedReports()).to.be.an('array').that.is.not.empty(); - expect(crashReporter.getUploadedReports()[0].id).to.equal(reportId); + upload_file_minidump: Buffer // eslint-disable-line camelcase } function checkCrash (expectedProcessType: string, fields: CrashInfo) { @@ -383,15 +42,56 @@ function checkCrash (expectedProcessType: string, fields: CrashInfo) { expect(String(fields.ver)).to.equal(process.versions.electron); expect(String(fields.process_type)).to.equal(expectedProcessType); expect(String(fields.platform)).to.equal(process.platform); - expect(String(fields.extra1)).to.equal('extra1'); - expect(String(fields.extra2)).to.equal('extra2'); - expect(fields.extra3).to.be.undefined(); expect(String(fields._productName)).to.equal('Zombies'); expect(String(fields._companyName)).to.equal('Umbrella Corporation'); expect(String(fields._version)).to.equal(app.getVersion()); + expect(fields.upload_file_minidump).to.be.an.instanceOf(Buffer); + expect(fields.upload_file_minidump.length).to.be.greaterThan(0); } -let crashReporterPort = 0; +function checkCrashExtra (fields: CrashInfo) { + expect(String(fields.extra1)).to.equal('extra1'); + expect(String(fields.extra2)).to.equal('extra2'); + expect(fields.extra3).to.be.undefined(); +} + +const startRemoteControlApp = async () => { + const appPath = path.join(__dirname, 'fixtures', 'apps', 'remote-control'); + const appProcess = childProcess.spawn(process.execPath, [appPath]); + const port = await new Promise(resolve => { + appProcess.stdout.on('data', d => { + const m = /Listening: (\d+)/.exec(d.toString()); + if (m && m[1] != null) { + resolve(Number(m[1])); + } + }); + }); + function remoteEval (js: string): any { + return new Promise((resolve, reject) => { + const req = http.request({ + host: '127.0.0.1', + port, + method: 'POST' + }, res => { + const chunks = [] as Buffer[]; + res.on('data', chunk => { chunks.push(chunk); }); + res.on('end', () => { + const ret = v8.deserialize(Buffer.concat(chunks)); + if (Object.prototype.hasOwnProperty.call(ret, 'error')) { + reject(new Error(`remote error: ${ret.error}\n\nTriggered at:`)); + } else { + resolve(ret.result); + } + }); + }); + req.write(js); + req.end(); + }); + } + afterTest.push(() => { appProcess.kill('SIGINT'); }); + return { remoteEval }; +}; + const startServer = async () => { const crashes: CrashInfo[] = []; function getCrashes () { return crashes; } @@ -405,36 +105,258 @@ const startServer = async () => { } const server = http.createServer((req, res) => { - const form = new multiparty.Form(); - form.parse(req, (error, fields) => { - crashes.push(fields); - if (error) throw error; - const reportId = 'abc-123-def-456-abc-789-abc-123-abcd'; - res.end(reportId, async () => { - await checkReport(reportId); - req.socket.destroy(); - emitter.emit('crash', fields); + const busboy = new Busboy({ headers: req.headers }); + const fields = {} as Record; + const files = {} as Record; + busboy.on('file', (fieldname, file) => { + const chunks = [] as Array; + file.on('data', (chunk) => { + chunks.push(chunk); + }); + file.on('end', () => { + files[fieldname] = Buffer.concat(chunks); }); }); + busboy.on('field', (fieldname, val) => { + fields[fieldname] = val; + }); + busboy.on('finish', () => { + const reportId = 'abc-123-def-456-abc-789-abc-123-abcd'; + res.end(reportId, async () => { + req.socket.destroy(); + emitter.emit('crash', { ...fields, ...files }); + }); + }); + req.pipe(busboy); }); await new Promise(resolve => { - server.listen(crashReporterPort, '127.0.0.1', () => { resolve(); }); + server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as AddressInfo).port; - if (crashReporterPort === 0) { - // We can only start the crash reporter once, and after that these - // parameters are fixed. - crashReporter.start({ - companyName: 'Umbrella Corporation', - submitURL: 'http://127.0.0.1:' + port - }); - crashReporterPort = port; - } - afterTest.push(() => { server.close(); }); return { getCrashes, port, waitForCrash }; }; + +function runApp (appPath: string, args: Array = []) { + const appProcess = childProcess.spawn(process.execPath, [appPath, ...args]); + return new Promise(resolve => { + appProcess.once('exit', resolve); + }); +} + +function runCrashApp (crashType: string, port: number, extraArgs: Array = []) { + const appPath = path.join(__dirname, 'fixtures', 'apps', 'crash'); + return runApp(appPath, [ + `--crash-type=${crashType}`, + `--crash-reporter-url=http://127.0.0.1:${port}`, + ...extraArgs + ]); +} + +function waitForNewFileInDir (dir: string): Promise { + const initialFiles = fs.readdirSync(dir); + return new Promise(resolve => { + const ivl = setInterval(() => { + const newCrashFiles = fs.readdirSync(dir).filter(f => !initialFiles.includes(f)); + if (newCrashFiles.length) { + clearInterval(ivl); + resolve(newCrashFiles); + } + }, 1000); + }); +} + +// TODO(alexeykuzmin): [Ch66] This test fails on Linux. Fix it and enable back. +ifdescribe(!process.mas && !process.env.DISABLE_CRASH_REPORTER_TESTS && process.platform !== 'linux')('crashReporter module', function () { + afterEach(cleanup); + + it('should send minidump when renderer crashes', async () => { + const { port, waitForCrash } = await startServer(); + runCrashApp('renderer', port); + const crash = await waitForCrash(); + checkCrash('renderer', crash); + }); + + it('should send minidump when sandboxed renderer crashes', async () => { + const { port, waitForCrash } = await startServer(); + runCrashApp('sandboxed-renderer', port); + const crash = await waitForCrash(); + checkCrash('renderer', crash); + checkCrashExtra(crash); + }); + + it('should send minidump with updated parameters when renderer crashes', async () => { + const { port, waitForCrash } = await startServer(); + runCrashApp('renderer', port, ['--set-extra-parameters-in-renderer']); + const crash = await waitForCrash(); + checkCrash('renderer', crash); + expect(crash.extra1).to.be.undefined(); + expect(crash.extra2).to.equal('extra2'); + expect(crash.extra3).to.equal('added'); + }); + + it('should send minidump with updated parameters when sandboxed renderer crashes', async () => { + const { port, waitForCrash } = await startServer(); + runCrashApp('sandboxed-renderer', port, ['--set-extra-parameters-in-renderer']); + const crash = await waitForCrash(); + checkCrash('renderer', crash); + expect(crash.extra1).to.be.undefined(); + expect(crash.extra2).to.equal('extra2'); + expect(crash.extra3).to.equal('added'); + }); + + it('should send minidump when main process crashes', async () => { + const { port, waitForCrash } = await startServer(); + runCrashApp('main', port); + const crash = await waitForCrash(); + checkCrash('browser', crash); + checkCrashExtra(crash); + }); + + it('should send minidump when a node process crashes', async () => { + const { port, waitForCrash } = await startServer(); + runCrashApp('node', port); + const crash = await waitForCrash(); + checkCrash('node', crash); + checkCrashExtra(crash); + }); + + it('should not send a minidump when uploadToServer is false', async () => { + const { port, getCrashes } = await startServer(); + const crashesDir = path.join(app.getPath('temp'), 'Zombies Crashes'); + const completedCrashesDir = path.join(crashesDir, 'completed'); + const crashAppeared = waitForNewFileInDir(completedCrashesDir); + await runCrashApp('renderer', port, ['--no-upload']); + await crashAppeared; + // wait a sec in case crashpad is about to upload a crash + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(getCrashes()).to.have.length(0); + }); + + describe('start() option validation', () => { + it('requires that the companyName option be specified', () => { + expect(() => { + crashReporter.start({ companyName: 'dummy' } as any); + }).to.throw('submitURL is a required option to crashReporter.start'); + }); + + it('requires that the submitURL option be specified', () => { + expect(() => { + crashReporter.start({ submitURL: 'dummy' } as any); + }).to.throw('companyName is a required option to crashReporter.start'); + }); + + it('can be called twice', async () => { + const { remoteEval } = await startRemoteControlApp(); + + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1"})`); + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1"})`); + }); + }); + + describe('getCrashesDirectory', () => { + it('correctly returns the directory', async () => { + const { remoteEval } = await startRemoteControlApp(); + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1"})`); + const crashesDir = await remoteEval(`require('electron').crashReporter.getCrashesDirectory()`); + const dir = path.join(app.getPath('temp'), 'remote-control Crashes'); + expect(crashesDir).to.equal(dir); + }); + }); + + describe('getUploadedReports', () => { + it('returns an array of reports', async () => { + const { remoteEval } = await startRemoteControlApp(); + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1"})`); + const reports = await remoteEval(`require('electron').crashReporter.getUploadedReports()`); + expect(reports).to.be.an('array'); + }); + }); + + describe('getLastCrashReport', () => { + it('returns the last uploaded report', async () => { + const { remoteEval } = await startRemoteControlApp(); + const { port, waitForCrash } = await startServer(); + + // 0. clear the crash reports directory. + const dir = path.join(app.getPath('temp'), 'remote-control Crashes'); + try { + fs.rmdirSync(dir, { recursive: true }); + } catch (e) { /* ignore */ } + + // 1. start the crash reporter. + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1:${port}", ignoreSystemCrashHandler: true})`); + // 2. generate a crash. + remoteEval(`(function() { const {BrowserWindow} = require('electron'); const bw = new BrowserWindow({show: false, webPreferences: {nodeIntegration: true}}); bw.loadURL('about:blank'); bw.webContents.executeJavaScript('process.crash()') })()`); + await waitForCrash(); + // 3. get the crash from getLastCrashReport. + const firstReport = await remoteEval(`require('electron').crashReporter.getLastCrashReport()`); + expect(firstReport).to.not.be.null(); + expect(firstReport.date).to.be.an.instanceOf(Date); + }); + }); + + describe('getUploadToServer()', () => { + it('returns true when uploadToServer is set to true (by default)', async () => { + const { remoteEval } = await startRemoteControlApp(); + + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1"})`); + const uploadToServer = await remoteEval(`require('electron').crashReporter.getUploadToServer()`); + expect(uploadToServer).to.be.true(); + }); + + it('returns false when uploadToServer is set to false in init', async () => { + const { remoteEval } = await startRemoteControlApp(); + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1", uploadToServer: false})`); + const uploadToServer = await remoteEval(`require('electron').crashReporter.getUploadToServer()`); + expect(uploadToServer).to.be.false(); + }); + + it('is updated by setUploadToServer', async () => { + const { remoteEval } = await startRemoteControlApp(); + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1"})`); + await remoteEval(`require('electron').crashReporter.setUploadToServer(false)`); + expect(await remoteEval(`require('electron').crashReporter.getUploadToServer()`)).to.be.false(); + await remoteEval(`require('electron').crashReporter.setUploadToServer(true)`); + expect(await remoteEval(`require('electron').crashReporter.getUploadToServer()`)).to.be.true(); + }); + }); + + describe('Parameters', () => { + it('returns all of the current parameters', async () => { + const { remoteEval } = await startRemoteControlApp(); + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1", extra: {"extra1": "hi"}})`); + const parameters = await remoteEval(`require('electron').crashReporter.getParameters()`); + expect(parameters).to.be.an('object'); + expect(parameters.extra1).to.equal('hi'); + }); + + it('adds and removes parameters', async () => { + const { remoteEval } = await startRemoteControlApp(); + await remoteEval(`require('electron').crashReporter.start({companyName: "Umbrella Corporation", submitURL: "http://127.0.0.1"})`); + await remoteEval(`require('electron').crashReporter.addExtraParameter('hello', 'world')`); + { + const parameters = await remoteEval(`require('electron').crashReporter.getParameters()`); + expect(parameters).to.have.property('hello'); + expect(parameters.hello).to.equal('world'); + } + + { + await remoteEval(`require('electron').crashReporter.removeExtraParameter('hello')`); + const parameters = await remoteEval(`require('electron').crashReporter.getParameters()`); + expect(parameters).not.to.have.property('hello'); + } + }); + }); + + describe('when not started', () => { + it('does not prevent process from crashing', async () => { + const appPath = path.join(__dirname, '..', 'spec', 'fixtures', 'api', 'cookie-app'); + await runApp(appPath); + }); + }); +}); diff --git a/spec-main/fixtures/apps/crash/main.js b/spec-main/fixtures/apps/crash/main.js new file mode 100644 index 0000000000..32ee7ff8b2 --- /dev/null +++ b/spec-main/fixtures/apps/crash/main.js @@ -0,0 +1,72 @@ +const { app, BrowserWindow, crashReporter } = require('electron'); +const path = require('path'); +const childProcess = require('child_process'); + +app.setVersion('0.1.0'); + +const url = app.commandLine.getSwitchValue('crash-reporter-url'); +const uploadToServer = !app.commandLine.hasSwitch('no-upload'); +const setExtraParameters = app.commandLine.hasSwitch('set-extra-parameters-in-renderer'); + +crashReporter.start({ + productName: 'Zombies', + companyName: 'Umbrella Corporation', + uploadToServer, + submitURL: url, + ignoreSystemCrashHandler: true, + extra: { + extra1: 'extra1', + extra2: 'extra2' + } +}); + +app.whenReady().then(() => { + const crashType = app.commandLine.getSwitchValue('crash-type'); + + if (crashType === 'main') { + process.crash(); + } else if (crashType === 'renderer') { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript(`require('electron').crashReporter.start({ + productName: 'Zombies', + companyName: 'Umbrella Corporation', + uploadToServer: true, + ignoreSystemCrashHandler: true, + submitURL: '', + extra: { + 'extra1': 'extra1', + 'extra2': 'extra2', + } + })`); + if (setExtraParameters) { + w.webContents.executeJavaScript(` + require('electron').crashReporter.addExtraParameter('extra3', 'added'); + require('electron').crashReporter.removeExtraParameter('extra1'); + `); + } + w.webContents.executeJavaScript('process.crash()'); + w.webContents.on('crashed', () => process.exit(0)); + } else if (crashType === 'sandboxed-renderer') { + const w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + preload: path.resolve(__dirname, 'sandbox-preload.js') + } + }); + w.loadURL(`about:blank?set_extra=${setExtraParameters ? 1 : 0}`); + w.webContents.on('crashed', () => process.exit(0)); + } else if (crashType === 'node') { + const crashesDir = path.join(app.getPath('temp'), `${app.name} Crashes`); + const version = app.getVersion(); + const crashPath = path.join(__dirname, 'node-crash.js'); + const child = childProcess.fork(crashPath, [url, version, crashesDir], { silent: true }); + child.on('exit', () => process.exit(0)); + } else { + console.error(`Unrecognized crash type: '${crashType}'`); + process.exit(1); + } +}); + +setTimeout(() => app.exit(), 30000); diff --git a/spec/fixtures/module/crash.js b/spec-main/fixtures/apps/crash/node-crash.js similarity index 88% rename from spec/fixtures/module/crash.js rename to spec-main/fixtures/apps/crash/node-crash.js index a550fc4649..0df7a31848 100644 --- a/spec/fixtures/module/crash.js +++ b/spec-main/fixtures/apps/crash/node-crash.js @@ -2,7 +2,8 @@ process.crashReporter.start({ productName: 'Zombies', companyName: 'Umbrella Corporation', crashesDirectory: process.argv[4], - submitURL: `http://127.0.0.1:${process.argv[2]}`, + submitURL: process.argv[2], + ignoreSystemCrashHandler: true, extra: { extra1: 'extra1', extra2: 'extra2', diff --git a/spec-main/fixtures/apps/crash/package.json b/spec-main/fixtures/apps/crash/package.json new file mode 100644 index 0000000000..78aa596256 --- /dev/null +++ b/spec-main/fixtures/apps/crash/package.json @@ -0,0 +1,4 @@ +{ + "name": "crash", + "main": "main.js" +} diff --git a/spec-main/fixtures/apps/crash/sandbox-preload.js b/spec-main/fixtures/apps/crash/sandbox-preload.js new file mode 100644 index 0000000000..432806cf70 --- /dev/null +++ b/spec-main/fixtures/apps/crash/sandbox-preload.js @@ -0,0 +1,18 @@ +const { crashReporter } = require('electron'); +crashReporter.start({ + productName: 'Zombies', + companyName: 'Umbrella Corporation', + uploadToServer: true, + ignoreSystemCrashHandler: true, + submitURL: '', + extra: { + 'extra1': 'extra1', + 'extra2': 'extra2' + } +}); +const params = new URLSearchParams(location.search); +if (params.get('set_extra') === '1') { + crashReporter.addExtraParameter('extra3', 'added'); + crashReporter.removeExtraParameter('extra1'); +} +process.crash(); diff --git a/spec-main/fixtures/apps/remote-control/main.js b/spec-main/fixtures/apps/remote-control/main.js new file mode 100644 index 0000000000..22fa110c65 --- /dev/null +++ b/spec-main/fixtures/apps/remote-control/main.js @@ -0,0 +1,22 @@ +const http = require('http'); +const v8 = require('v8'); + +const server = http.createServer((req, res) => { + const chunks = []; + req.on('data', chunk => { chunks.push(chunk); }); + req.on('end', () => { + const js = Buffer.concat(chunks).toString('utf8'); + try { + const result = eval(js); // eslint-disable-line no-eval + res.end(v8.serialize({ result })); + } catch (e) { + res.end(v8.serialize({ error: e.stack })); + } + }); +}).listen(0, '127.0.0.1', () => { + process.stdout.write(`Listening: ${server.address().port}\n`); +}); + +setTimeout(() => { + process.exit(0); +}, 30000); diff --git a/spec-main/fixtures/apps/remote-control/package.json b/spec-main/fixtures/apps/remote-control/package.json new file mode 100644 index 0000000000..738eef5e46 --- /dev/null +++ b/spec-main/fixtures/apps/remote-control/package.json @@ -0,0 +1,4 @@ +{ + "name": "remote-control", + "main": "main.js" +} diff --git a/spec/fixtures/module/preload-sandbox.js b/spec-main/fixtures/module/preload-sandbox.js similarity index 100% rename from spec/fixtures/module/preload-sandbox.js rename to spec-main/fixtures/module/preload-sandbox.js diff --git a/spec-main/index.js b/spec-main/index.js index cf9fa06212..4c91ecb983 100644 --- a/spec-main/index.js +++ b/spec-main/index.js @@ -90,7 +90,7 @@ app.whenReady().then(() => { walker.on('end', () => { testFiles.sort(); - sortToEnd(testFiles, f => f.includes('crash-reporter')).forEach((file) => { + testFiles.forEach((file) => { if (!argv.files || argv.files.includes(path.relative(baseElectronDir, file))) { mocha.addFile(file); } @@ -117,8 +117,3 @@ function partition (xs, f) { xs.forEach(x => (f(x) ? trues : falses).push(x)); return [trues, falses]; } - -function sortToEnd (xs, f) { - const [end, beginning] = partition(xs, f); - return beginning.concat(end); -} diff --git a/spec-main/package.json b/spec-main/package.json index 8e65c4879f..042bc9b30b 100644 --- a/spec-main/package.json +++ b/spec-main/package.json @@ -5,6 +5,7 @@ "version": "0.1.0", "devDependencies": { "@types/ws": "^7.2.0", + "busboy": "^0.3.1", "echo": "file:fixtures/native-addon/echo", "q": "^1.5.1", "ws": "^7.2.1" diff --git a/spec-main/yarn.lock b/spec-main/yarn.lock index af35c32e55..0d89278219 100644 --- a/spec-main/yarn.lock +++ b/spec-main/yarn.lock @@ -34,6 +34,13 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +busboy@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" + integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== + dependencies: + dicer "0.3.0" + chai-as-promised@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" @@ -46,6 +53,13 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= +dicer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" + integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== + dependencies: + streamsearch "0.1.2" + dirty-chai@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/dirty-chai/-/dirty-chai-2.0.1.tgz#6b2162ef17f7943589da840abc96e75bda01aff3" @@ -126,6 +140,11 @@ schema-utils@^0.4.0: ajv "^6.1.0" ajv-keywords "^3.1.0" +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" diff --git a/spec/fixtures/api/crash-restart.html b/spec/fixtures/api/crash-restart.html deleted file mode 100644 index 0ee4ad5350..0000000000 --- a/spec/fixtures/api/crash-restart.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - diff --git a/spec/fixtures/api/crash.html b/spec/fixtures/api/crash.html deleted file mode 100644 index bcce825426..0000000000 --- a/spec/fixtures/api/crash.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - diff --git a/spec/fixtures/api/crash_child.html b/spec/fixtures/api/crash_child.html deleted file mode 100644 index cda0080bbf..0000000000 --- a/spec/fixtures/api/crash_child.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/spec/package.json b/spec/package.json index 3ebe235d94..0aedce766c 100644 --- a/spec/package.json +++ b/spec/package.json @@ -19,7 +19,6 @@ "mocha": "^5.2.0", "mocha-junit-reporter": "^1.18.0", "mocha-multi-reporters": "^1.1.7", - "multiparty": "^4.2.1", "send": "^0.16.2", "split": "^1.0.1", "temp": "^0.9.0", diff --git a/spec/yarn.lock b/spec/yarn.lock index 572aa87f55..849f089572 100644 --- a/spec/yarn.lock +++ b/spec/yarn.lock @@ -7,9 +7,10 @@ resolved "https://registry.yarnpkg.com/@nornagon/put/-/put-0.0.8.tgz#9d497ec46c9364acc3f8b59aa3cf8ee4134ae337" integrity sha512-ugvXJjwF5ldtUpa7D95kruNJ41yFQDEKyF5CW4TgKJnh+W/zmlBzXXeKTyqIgwMFrkePN2JqOBqcF0M0oOunow== -"abstract-socket@github:saghul/node-abstractsocket#35b1b1491fabc04899bde5be3428abf5cf9cd528": - version "2.1.0" - resolved "https://codeload.github.com/saghul/node-abstractsocket/tar.gz/35b1b1491fabc04899bde5be3428abf5cf9cd528" +abstract-socket@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/abstract-socket/-/abstract-socket-2.1.1.tgz#243a7e6e6ff65bb9eab16a22fa90699b91e528f7" + integrity sha512-YZJizsvS1aBua5Gd01woe4zuyYBGgSMeqDOB6/ChwdTI904KP6QGtJswXl4hcqWxbz86hQBe++HWV0hF1aGUtA== dependencies: bindings "^1.2.1" nan "^2.12.1" @@ -381,13 +382,6 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fd-slicer@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -525,17 +519,6 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-errors@~1.7.0: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -791,16 +774,6 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -multiparty@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.1.tgz#d9b6c46d8b8deab1ee70c734b0af771dd46e0b13" - integrity sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA== - dependencies: - fd-slicer "1.1.0" - http-errors "~1.7.0" - safe-buffer "5.1.2" - uid-safe "2.1.5" - nan@2.x, nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -920,11 +893,6 @@ pause-stream@^0.0.11: dependencies: through "~2.3" -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -958,11 +926,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -random-bytes@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" - integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= - range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -1073,11 +1036,6 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -1117,7 +1075,7 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2": +"statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -1190,11 +1148,6 @@ through@2, through@^2.3.8, through@~2.3, through@~2.3.4: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -1220,13 +1173,6 @@ type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -uid-safe@2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" - integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== - dependencies: - random-bytes "~1.0.0" - uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" diff --git a/yarn.lock b/yarn.lock index 8ca6952b11..bcde86be32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -155,6 +155,13 @@ "@types/connect" "*" "@types/node" "*" +"@types/busboy@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@types/busboy/-/busboy-0.2.3.tgz#6697ad29873246c530f09a3ff5a40861824230d5" + integrity sha1-ZpetKYcyRsUw8Jo/9aQIYYJCMNU= + dependencies: + "@types/node" "*" + "@types/chai-as-promised@*": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.1.tgz#004c27a4ac640e9590e25d8b0980cb0a6609bfd8" @@ -274,13 +281,6 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w== -"@types/multiparty@^0.0.32": - version "0.0.32" - resolved "https://registry.yarnpkg.com/@types/multiparty/-/multiparty-0.0.32.tgz#99226df6050dae605fa6f9489d6c0b5ab61fdc00" - integrity sha512-zuQEcL9pHiW3u1fgkOWE6T/RwskrFZ3W63aKQPDs7EokIjtbsGL7aVlI4tI86qjL4B3hcFxDRtIGxwRwMTS2Dw== - dependencies: - "@types/node" "*" - "@types/node@*": version "12.6.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.1.tgz#d5544f6de0aae03eefbb63d5120f6c8be0691946"