Browse Source

feat: support crashpad on linux (#29719)

Jeremy Rose 3 years ago
parent
commit
c9ba0d02d7

+ 1 - 1
.circleci/config.yml

@@ -45,7 +45,7 @@ executors:
         type: enum
         enum: ["medium", "xlarge", "2xlarge+"]
     docker:
-      - image: electron.azurecr.io/build:4fc81b50f9c0980699d329bc32062fac20a26701
+      - image: electron.azurecr.io/build:fe71f448c9b00708c7a8a67a0210bcef5055ac64
     resource_class: << parameters.size >>
 
   macos:

+ 0 - 6
build/zip.py

@@ -31,12 +31,6 @@ PATHS_TO_SKIP = [
   # //chrome/browser/resources/ssl/ssl_error_assistant, but we don't need to
   # ship it.
   'pyproto',
-  # On Windows, this binary doesn't exist (the crashpad handler is built-in).
-  # On MacOS, the binary is called 'chrome_crashpad_handler' and is inside the
-  # app bundle.
-  # On Linux, we don't use crashpad, but this binary is still built for some
-  # reason. Exclude it from the zip.
-  './crashpad_handler',
   # Skip because these are outputs that we don't need.
   'resources/inspector',
   'gen/third_party/devtools-frontend/src',

+ 26 - 0
patches/chromium/crash_allow_setting_more_options.patch

@@ -74,6 +74,32 @@ index 39557cce474439238255ecd28030215085db0c81..5b3f980837911c710686ab91a2a81c31
  #if defined(OS_ANDROID)
    // Used by WebView to sample crashes without generating the unwanted dumps. If
    // the returned value is less than 100, crash dumping will be sampled to that
+diff --git a/components/crash/core/app/crashpad_linux.cc b/components/crash/core/app/crashpad_linux.cc
+index 5f97c1ef00d9c63a7b16265cc97d9f145adae550..8c3028f228373b5e1145fe3235dc06663f8b087f 100644
+--- a/components/crash/core/app/crashpad_linux.cc
++++ b/components/crash/core/app/crashpad_linux.cc
+@@ -165,6 +165,7 @@ base::FilePath PlatformCrashpadInitialization(
+     // where crash_reporter provides it's own values for lsb-release.
+     annotations["lsb-release"] = base::GetLinuxDistro();
+ #endif
++    crash_reporter_client->GetProcessSimpleAnnotations(&annotations);
+ 
+     std::vector<std::string> arguments;
+     if (crash_reporter_client->ShouldMonitorCrashHandlerExpensively()) {
+@@ -186,6 +187,13 @@ base::FilePath PlatformCrashpadInitialization(
+     }
+ #endif
+ 
++    if (!crash_reporter_client->GetShouldRateLimit()) {
++      arguments.push_back("--no-rate-limit");
++    }
++    if (!crash_reporter_client->GetShouldCompressUploads()) {
++      arguments.push_back("--no-upload-gzip");
++    }
++
+     bool result =
+         client.StartHandler(handler_path, database_path, metrics_path, url,
+                             annotations, arguments, false, false);
 diff --git a/components/crash/core/app/crashpad_mac.mm b/components/crash/core/app/crashpad_mac.mm
 index e3fc1fb2bcab31d6a7cb325a892acb26dc00d4e4..fd654d6e514de416457c283caeb1895dba6286e1 100644
 --- a/components/crash/core/app/crashpad_mac.mm

+ 1 - 0
script/zip_manifests/dist_zip.linux.arm.manifest

@@ -3,6 +3,7 @@ LICENSES.chromium.html
 chrome-sandbox
 chrome_100_percent.pak
 chrome_200_percent.pak
+crashpad_handler
 electron
 icudtl.dat
 libEGL.so

+ 1 - 0
script/zip_manifests/dist_zip.linux.arm64.manifest

@@ -3,6 +3,7 @@ LICENSES.chromium.html
 chrome-sandbox
 chrome_100_percent.pak
 chrome_200_percent.pak
+crashpad_handler
 electron
 icudtl.dat
 libEGL.so

+ 1 - 0
script/zip_manifests/dist_zip.linux.x64.manifest

@@ -3,6 +3,7 @@ LICENSES.chromium.html
 chrome-sandbox
 chrome_100_percent.pak
 chrome_200_percent.pak
+crashpad_handler
 electron
 icudtl.dat
 libEGL.so

+ 1 - 0
script/zip_manifests/dist_zip.linux.x86.manifest

@@ -3,6 +3,7 @@ LICENSES.chromium.html
 chrome-sandbox
 chrome_100_percent.pak
 chrome_200_percent.pak
+crashpad_handler
 electron
 icudtl.dat
 libEGL.so

+ 3 - 7
shell/app/electron_crash_reporter_client.cc

@@ -153,6 +153,9 @@ bool ElectronCrashReporterClient::GetCrashDumpLocation(
     base::FilePath* crash_dir) {
   bool result = base::PathService::Get(electron::DIR_CRASH_DUMPS, crash_dir);
   {
+    // If the DIR_CRASH_DUMPS path is overridden with
+    // app.setPath('crashDumps', ...) then the directory might not have been
+    // created.
     base::ThreadRestrictions::ScopedAllowIO allow_io;
     if (result && !base::PathExists(*crash_dir)) {
       return base::CreateDirectory(*crash_dir);
@@ -162,13 +165,6 @@ bool ElectronCrashReporterClient::GetCrashDumpLocation(
 }
 #endif
 
-#if defined(OS_MAC) || defined(OS_LINUX)
-bool ElectronCrashReporterClient::GetCrashMetricsLocation(
-    base::FilePath* metrics_dir) {
-  return base::PathService::Get(chrome::DIR_USER_DATA, metrics_dir);
-}
-#endif  // OS_MAC || OS_LINUX
-
 bool ElectronCrashReporterClient::IsRunningUnattended() {
   return !collect_stats_consent_;
 }

+ 0 - 4
shell/app/electron_crash_reporter_client.h

@@ -52,10 +52,6 @@ class ElectronCrashReporterClient : public crash_reporter::CrashReporterClient {
   bool GetCrashDumpLocation(base::FilePath* crash_dir) override;
 #endif
 
-#if defined(OS_MAC) || defined(OS_LINUX)
-  bool GetCrashMetricsLocation(base::FilePath* metrics_dir) override;
-#endif
-
   bool IsRunningUnattended() override;
 
   bool GetCollectStatsConsent() override;

+ 23 - 3
shell/app/electron_main_delegate.cc

@@ -58,7 +58,8 @@
 #endif
 
 #if !defined(MAS_BUILD)
-#include "components/crash/core/app/crashpad.h"  // nogncheck
+#include "components/crash/core/app/crash_switches.h"  // nogncheck
+#include "components/crash/core/app/crashpad.h"        // nogncheck
 #include "components/crash/core/common/crash_key.h"
 #include "components/crash/core/common/crash_keys.h"
 #include "shell/app/electron_crash_reporter_client.h"
@@ -369,9 +370,19 @@ void ElectronMainDelegate::PreSandboxStartup() {
 #endif
 
 #if defined(OS_LINUX)
+  // Zygote needs to call InitCrashReporter() in RunZygote().
   if (process_type != ::switches::kZygoteProcess && !process_type.empty()) {
     ElectronCrashReporterClient::Create();
-    breakpad::InitCrashReporter(process_type);
+    if (crash_reporter::IsCrashpadEnabled()) {
+      if (command_line->HasSwitch(
+              crash_reporter::switches::kCrashpadHandlerPid)) {
+        crash_reporter::InitializeCrashpad(false, process_type);
+        crash_reporter::SetFirstChanceExceptionHandler(
+            v8::TryHandleWebAssemblyTrapPosix);
+      }
+    } else {
+      breakpad::InitCrashReporter(process_type);
+    }
   }
 #endif
 
@@ -466,7 +477,16 @@ void ElectronMainDelegate::ZygoteForked() {
       base::CommandLine::ForCurrentProcess();
   std::string process_type =
       command_line->GetSwitchValueASCII(::switches::kProcessType);
-  breakpad::InitCrashReporter(process_type);
+  if (crash_reporter::IsCrashpadEnabled()) {
+    if (command_line->HasSwitch(
+            crash_reporter::switches::kCrashpadHandlerPid)) {
+      crash_reporter::InitializeCrashpad(false, process_type);
+      crash_reporter::SetFirstChanceExceptionHandler(
+          v8::TryHandleWebAssemblyTrapPosix);
+    }
+  } else {
+    breakpad::InitCrashReporter(process_type);
+  }
 
   // Reset the command line for the newly spawned process.
   crash_keys::SetCrashKeysFromCommandLine(*command_line);

+ 37 - 10
shell/browser/api/electron_api_crash_reporter.cc

@@ -44,6 +44,7 @@
 #include "base/guid.h"
 #include "components/crash/core/app/breakpad_linux.h"
 #include "components/crash/core/common/crash_keys.h"
+#include "components/upload_list/combining_upload_list.h"
 #include "v8/include/v8-wasm-trap-handler-posix.h"
 #include "v8/include/v8.h"
 #endif
@@ -150,16 +151,29 @@ void Start(const std::string& submit_url,
           ? "node"
           : command_line->GetSwitchValueASCII(::switches::kProcessType);
 #if defined(OS_LINUX)
-  ::crash_keys::SetMetricsClientIdFromGUID(GetClientId());
-  auto& global_crash_keys = GetGlobalCrashKeysMutable();
-  for (const auto& pair : global_extra) {
-    global_crash_keys[pair.first] = pair.second;
+  if (::crash_reporter::IsCrashpadEnabled()) {
+    for (const auto& pair : extra)
+      electron::crash_keys::SetCrashKey(pair.first, pair.second);
+    {
+      base::ThreadRestrictions::ScopedAllowIO allow_io;
+      ::crash_reporter::InitializeCrashpad(process_type.empty(), process_type);
+    }
+    if (ignore_system_crash_handler) {
+      crashpad::CrashpadInfo::GetCrashpadInfo()
+          ->set_system_crash_reporter_forwarding(crashpad::TriState::kDisabled);
+    }
+  } else {
+    ::crash_keys::SetMetricsClientIdFromGUID(GetClientId());
+    auto& global_crash_keys = GetGlobalCrashKeysMutable();
+    for (const auto& pair : global_extra) {
+      global_crash_keys[pair.first] = pair.second;
+    }
+    for (const auto& pair : extra)
+      electron::crash_keys::SetCrashKey(pair.first, pair.second);
+    for (const auto& pair : global_extra)
+      electron::crash_keys::SetCrashKey(pair.first, pair.second);
+    breakpad::InitCrashReporter(process_type);
   }
-  for (const auto& pair : extra)
-    electron::crash_keys::SetCrashKey(pair.first, pair.second);
-  for (const auto& pair : global_extra)
-    electron::crash_keys::SetCrashKey(pair.first, pair.second);
-  breakpad::InitCrashReporter(process_type);
 #elif defined(OS_MAC)
   for (const auto& pair : extra)
     electron::crash_keys::SetCrashKey(pair.first, pair.second);
@@ -203,7 +217,20 @@ scoped_refptr<UploadList> CreateCrashUploadList() {
   base::PathService::Get(electron::DIR_CRASH_DUMPS, &crash_dir_path);
   base::FilePath upload_log_path =
       crash_dir_path.AppendASCII(CrashUploadList::kReporterLogFilename);
-  return base::MakeRefCounted<TextLogUploadList>(upload_log_path);
+  scoped_refptr<UploadList> result =
+      base::MakeRefCounted<TextLogUploadList>(upload_log_path);
+  if (crash_reporter::IsCrashpadEnabled()) {
+    // Crashpad keeps the records of C++ crashes (segfaults, etc) in its
+    // internal database. The JavaScript error reporter writes JS error upload
+    // records to the older text format. Combine the two to present a complete
+    // list to the user.
+    // TODO(nornagon): what is "The JavaScript error reporter", and do we care
+    // about it?
+    std::vector<scoped_refptr<UploadList>> uploaders = {
+        base::MakeRefCounted<CrashUploadListCrashpad>(), std::move(result)};
+    result = base::MakeRefCounted<CombiningUploadList>(std::move(uploaders));
+  }
+  return result;
 #endif  // defined(OS_MAC) || defined(OS_WIN)
 }
 

+ 31 - 8
shell/browser/electron_browser_client.cc

@@ -298,6 +298,12 @@ breakpad::CrashHandlerHostLinux* CreateCrashHandlerHost(
 }
 
 int GetCrashSignalFD(const base::CommandLine& command_line) {
+  if (crash_reporter::IsCrashpadEnabled()) {
+    int fd;
+    pid_t pid;
+    return crash_reporter::GetHandlerSocket(&fd, &pid) ? fd : -1;
+  }
+
   // Extensions have the same process type as renderers.
   if (command_line.HasSwitch(extensions::switches::kExtensionProcess)) {
     static breakpad::CrashHandlerHostLinux* crash_handler = nullptr;
@@ -526,20 +532,37 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
 
 #if defined(OS_LINUX)
   bool enable_crash_reporter = false;
-  enable_crash_reporter = breakpad::IsCrashReporterEnabled();
+  if (crash_reporter::IsCrashpadEnabled()) {
+    command_line->AppendSwitch(::switches::kEnableCrashpad);
+    enable_crash_reporter = true;
+
+    int fd;
+    pid_t pid;
+
+    if (crash_reporter::GetHandlerSocket(&fd, &pid)) {
+      command_line->AppendSwitchASCII(
+          crash_reporter::switches::kCrashpadHandlerPid,
+          base::NumberToString(pid));
+    }
+  } else {
+    enable_crash_reporter = breakpad::IsCrashReporterEnabled();
+  }
+
   if (enable_crash_reporter) {
     std::string switch_value =
         api::crash_reporter::GetClientId() + ",no_channel";
     command_line->AppendSwitchASCII(::switches::kEnableCrashReporter,
                                     switch_value);
-    for (const auto& pair : api::crash_reporter::GetGlobalCrashKeys()) {
-      if (!switch_value.empty())
-        switch_value += ",";
-      switch_value += pair.first;
-      switch_value += "=";
-      switch_value += pair.second;
+    if (!crash_reporter::IsCrashpadEnabled()) {
+      for (const auto& pair : api::crash_reporter::GetGlobalCrashKeys()) {
+        if (!switch_value.empty())
+          switch_value += ",";
+        switch_value += pair.first;
+        switch_value += "=";
+        switch_value += pair.second;
+      }
+      command_line->AppendSwitchASCII(switches::kGlobalCrashKeys, switch_value);
     }
-    command_line->AppendSwitchASCII(switches::kGlobalCrashKeys, switch_value);
   }
 #endif
 

+ 414 - 408
spec-main/api-crash-reporter-spec.ts

@@ -138,224 +138,455 @@ function waitForNewFileInDir (dir: string): Promise<string[]> {
 
 // TODO(nornagon): Fix tests on linux/arm.
 ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_TESTS)('crashReporter module', function () {
-  describe('should send minidump', () => {
-    it('when renderer crashes', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('renderer', port);
-      const crash = await waitForCrash();
-      checkCrash('renderer', crash);
-      expect(crash.mainProcessSpecific).to.be.undefined();
-    });
+  for (const withLinuxCrashpad of (process.platform === 'linux' ? [false, true] : [false])) {
+    const crashpadExtraArgs = withLinuxCrashpad ? ['--enable-crashpad'] : [];
+    describe(withLinuxCrashpad ? '(with crashpad)' : '', () => {
+      describe('should send minidump', () => {
+        it('when renderer crashes', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('renderer', port, crashpadExtraArgs);
+          const crash = await waitForCrash();
+          checkCrash('renderer', crash);
+          expect(crash.mainProcessSpecific).to.be.undefined();
+        });
 
-    it('when sandboxed renderer crashes', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('sandboxed-renderer', port);
-      const crash = await waitForCrash();
-      checkCrash('renderer', crash);
-      expect(crash.mainProcessSpecific).to.be.undefined();
-    });
+        it('when sandboxed renderer crashes', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('sandboxed-renderer', port, crashpadExtraArgs);
+          const crash = await waitForCrash();
+          checkCrash('renderer', crash);
+          expect(crash.mainProcessSpecific).to.be.undefined();
+        });
 
-    // TODO(nornagon): Minidump generation in main/node process on Linux/Arm is
-    // broken (//components/crash prints "Failed to generate minidump"). Figure
-    // out why.
-    ifit(!isLinuxOnArm)('when main process crashes', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('main', port);
-      const crash = await waitForCrash();
-      checkCrash('browser', crash);
-      expect(crash.mainProcessSpecific).to.equal('mps');
-    });
+        // TODO(nornagon): Minidump generation in main/node process on Linux/Arm is
+        // broken (//components/crash prints "Failed to generate minidump"). Figure
+        // out why.
+        ifit(!isLinuxOnArm)('when main process crashes', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('main', port, crashpadExtraArgs);
+          const crash = await waitForCrash();
+          checkCrash('browser', crash);
+          expect(crash.mainProcessSpecific).to.equal('mps');
+        });
 
-    ifit(!isLinuxOnArm)('when a node process crashes', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('node', port);
-      const crash = await waitForCrash();
-      checkCrash('node', crash);
-      expect(crash.mainProcessSpecific).to.be.undefined();
-      expect(crash.rendererSpecific).to.be.undefined();
-    });
+        ifit(!isLinuxOnArm)('when a node process crashes', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('node', port, crashpadExtraArgs);
+          const crash = await waitForCrash();
+          checkCrash('node', crash);
+          expect(crash.mainProcessSpecific).to.be.undefined();
+          expect(crash.rendererSpecific).to.be.undefined();
+        });
+
+        describe('with guid', () => {
+          for (const processType of ['main', 'renderer', 'sandboxed-renderer']) {
+            it(`when ${processType} crashes`, async () => {
+              const { port, waitForCrash } = await startServer();
+              runCrashApp(processType, port, crashpadExtraArgs);
+              const crash = await waitForCrash();
+              expect(crash.guid).to.be.a('string');
+            });
+          }
+
+          it('is a consistent id', async () => {
+            let crash1Guid;
+            let crash2Guid;
+            {
+              const { port, waitForCrash } = await startServer();
+              runCrashApp('main', port, crashpadExtraArgs);
+              const crash = await waitForCrash();
+              crash1Guid = crash.guid;
+            }
+            {
+              const { port, waitForCrash } = await startServer();
+              runCrashApp('main', port, crashpadExtraArgs);
+              const crash = await waitForCrash();
+              crash2Guid = crash.guid;
+            }
+            expect(crash2Guid).to.equal(crash1Guid);
+          });
+        });
+
+        describe('with extra parameters', () => {
+          it('when renderer crashes', async () => {
+            const { port, waitForCrash } = await startServer();
+            runCrashApp('renderer', port, ['--set-extra-parameters-in-renderer', ...crashpadExtraArgs]);
+            const crash = await waitForCrash();
+            checkCrash('renderer', crash);
+            expect(crash.mainProcessSpecific).to.be.undefined();
+            expect(crash.rendererSpecific).to.equal('rs');
+            expect(crash.addedThenRemoved).to.be.undefined();
+          });
+
+          it('when sandboxed renderer crashes', async () => {
+            const { port, waitForCrash } = await startServer();
+            runCrashApp('sandboxed-renderer', port, ['--set-extra-parameters-in-renderer', ...crashpadExtraArgs]);
+            const crash = await waitForCrash();
+            checkCrash('renderer', crash);
+            expect(crash.mainProcessSpecific).to.be.undefined();
+            expect(crash.rendererSpecific).to.equal('rs');
+            expect(crash.addedThenRemoved).to.be.undefined();
+          });
+
+          it('contains v8 crash keys when a v8 crash occurs', async () => {
+            const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+            const { port, waitForCrash } = await startServer();
+
+            await remotely((port: number) => {
+              require('electron').crashReporter.start({
+                submitURL: `http://127.0.0.1:${port}`,
+                compress: false,
+                ignoreSystemCrashHandler: true
+              });
+            }, [port]);
+
+            remotely(() => {
+              const { BrowserWindow } = require('electron');
+              const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
+              bw.loadURL('about:blank');
+              bw.webContents.executeJavaScript('process._linkedBinding(\'electron_common_v8_util\').triggerFatalErrorForTesting()');
+            });
+
+            const crash = await waitForCrash();
+            expect(crash.prod).to.equal('Electron');
+            expect(crash._productName).to.equal('electron-test-remote-control');
+            expect(crash.process_type).to.equal('renderer');
+            expect(crash['electron.v8-fatal.location']).to.equal('v8::Context::New()');
+            expect(crash['electron.v8-fatal.message']).to.equal('Circular extension dependency');
+          });
+        });
+      });
 
-    describe('with guid', () => {
-      for (const processType of ['main', 'renderer', 'sandboxed-renderer']) {
-        it(`when ${processType} crashes`, async () => {
+      ifdescribe(!isLinuxOnArm)('extra parameter limits', () => {
+        function stitchLongCrashParam (crash: any, paramKey: string) {
+          if (crash[paramKey]) return crash[paramKey];
+          let chunk = 1;
+          let stitched = '';
+          while (crash[`${paramKey}__${chunk}`]) {
+            stitched += crash[`${paramKey}__${chunk}`];
+            chunk++;
+          }
+          return stitched;
+        }
+
+        it('should truncate extra values longer than 5 * 4096 characters', async () => {
           const { port, waitForCrash } = await startServer();
-          runCrashApp(processType, port);
+          const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+          remotely((port: number) => {
+            require('electron').crashReporter.start({
+              submitURL: `http://127.0.0.1:${port}`,
+              compress: false,
+              ignoreSystemCrashHandler: true,
+              extra: { longParam: 'a'.repeat(100000) }
+            });
+            setTimeout(() => process.crash());
+          }, port);
           const crash = await waitForCrash();
-          expect(crash.guid).to.be.a('string');
+          expect(stitchLongCrashParam(crash, 'longParam')).to.have.lengthOf(160 * 127 + (withLinuxCrashpad ? 159 : 0), 'crash should have truncated longParam');
         });
-      }
 
-      it('is a consistent id', async () => {
-        let crash1Guid;
-        let crash2Guid;
-        {
+        it('should omit extra keys with names longer than the maximum', async () => {
+          const kKeyLengthMax = 39;
           const { port, waitForCrash } = await startServer();
-          runCrashApp('main', port);
+          const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+          remotely((port: number, kKeyLengthMax: number) => {
+            require('electron').crashReporter.start({
+              submitURL: `http://127.0.0.1:${port}`,
+              compress: false,
+              ignoreSystemCrashHandler: true,
+              extra: {
+                ['a'.repeat(kKeyLengthMax + 10)]: 'value',
+                ['b'.repeat(kKeyLengthMax)]: 'value',
+                'not-long': 'not-long-value'
+              }
+            });
+            require('electron').crashReporter.addExtraParameter('c'.repeat(kKeyLengthMax + 10), 'value');
+            setTimeout(() => process.crash());
+          }, port, kKeyLengthMax);
           const crash = await waitForCrash();
-          crash1Guid = crash.guid;
-        }
-        {
+          expect(crash).not.to.have.property('a'.repeat(kKeyLengthMax + 10));
+          expect(crash).not.to.have.property('a'.repeat(kKeyLengthMax));
+          expect(crash).to.have.property('b'.repeat(kKeyLengthMax), 'value');
+          expect(crash).to.have.property('not-long', 'not-long-value');
+          expect(crash).not.to.have.property('c'.repeat(kKeyLengthMax + 10));
+          expect(crash).not.to.have.property('c'.repeat(kKeyLengthMax));
+        });
+      });
+
+      describe('globalExtra', () => {
+        ifit(!isLinuxOnArm)('should be sent with main process dumps', async () => {
           const { port, waitForCrash } = await startServer();
-          runCrashApp('main', port);
+          runCrashApp('main', port, ['--add-global-param=globalParam:globalValue', ...crashpadExtraArgs]);
           const crash = await waitForCrash();
-          crash2Guid = crash.guid;
-        }
-        expect(crash2Guid).to.equal(crash1Guid);
+          expect(crash.globalParam).to.equal('globalValue');
+        });
+
+        it('should be sent with renderer process dumps', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('renderer', port, ['--add-global-param=globalParam:globalValue', ...crashpadExtraArgs]);
+          const crash = await waitForCrash();
+          expect(crash.globalParam).to.equal('globalValue');
+        });
+
+        it('should be sent with sandboxed renderer process dumps', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('sandboxed-renderer', port, ['--add-global-param=globalParam:globalValue', ...crashpadExtraArgs]);
+          const crash = await waitForCrash();
+          expect(crash.globalParam).to.equal('globalValue');
+        });
+
+        ifit(!isLinuxOnArm)('should not be overridden by extra in main process', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('main', port, ['--add-global-param=mainProcessSpecific:global', ...crashpadExtraArgs]);
+          const crash = await waitForCrash();
+          expect(crash.mainProcessSpecific).to.equal('global');
+        });
+
+        ifit(!isLinuxOnArm)('should not be overridden by extra in renderer process', async () => {
+          const { port, waitForCrash } = await startServer();
+          runCrashApp('main', port, ['--add-global-param=rendererSpecific:global', ...crashpadExtraArgs]);
+          const crash = await waitForCrash();
+          expect(crash.rendererSpecific).to.equal('global');
+        });
       });
-    });
 
-    describe('with extra parameters', () => {
-      it('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.mainProcessSpecific).to.be.undefined();
-        expect(crash.rendererSpecific).to.equal('rs');
-        expect(crash.addedThenRemoved).to.be.undefined();
+      // TODO(nornagon): also test crashing main / sandboxed renderers.
+      ifit(!isWindowsOnArm)('should not send a minidump when uploadToServer is false', async () => {
+        const { port, waitForCrash, getCrashes } = await startServer();
+        waitForCrash().then(() => expect.fail('expected not to receive a dump'));
+        await runCrashApp('renderer', port, ['--no-upload', ...crashpadExtraArgs]);
+        // wait a sec in case the crash reporter is about to upload a crash
+        await delay(1000);
+        expect(getCrashes()).to.have.length(0);
       });
 
-      it('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.mainProcessSpecific).to.be.undefined();
-        expect(crash.rendererSpecific).to.equal('rs');
-        expect(crash.addedThenRemoved).to.be.undefined();
+      describe('getUploadedReports', () => {
+        it('returns an array of reports', async () => {
+          const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+          await remotely(() => {
+            require('electron').crashReporter.start({ submitURL: 'http://127.0.0.1' });
+          });
+          const reports = await remotely(() => require('electron').crashReporter.getUploadedReports());
+          expect(reports).to.be.an('array');
+        });
       });
 
-      it('contains v8 crash keys when a v8 crash occurs', async () => {
-        const { remotely } = await startRemoteControlApp();
-        const { port, waitForCrash } = await startServer();
+      // TODO(nornagon): re-enable on woa
+      ifdescribe(!isWindowsOnArm)('getLastCrashReport', () => {
+        it('returns the last uploaded report', async () => {
+          const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+          const { port, waitForCrash } = await startServer();
 
-        await remotely((port: number) => {
-          require('electron').crashReporter.start({
-            submitURL: `http://127.0.0.1:${port}`,
-            compress: false,
-            ignoreSystemCrashHandler: true
+          // 0. clear the crash reports directory.
+          const dir = await remotely(() => require('electron').app.getPath('crashDumps'));
+          try {
+            fs.rmdirSync(dir, { recursive: true });
+            fs.mkdirSync(dir);
+          } catch (e) { /* ignore */ }
+
+          // 1. start the crash reporter.
+          await remotely((port: number) => {
+            require('electron').crashReporter.start({
+              submitURL: `http://127.0.0.1:${port}`,
+              compress: false,
+              ignoreSystemCrashHandler: true
+            });
+          }, [port]);
+          // 2. generate a crash in the renderer.
+          remotely(() => {
+            const { BrowserWindow } = require('electron');
+            const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
+            bw.loadURL('about:blank');
+            bw.webContents.executeJavaScript('process.crash()');
           });
-        }, [port]);
+          await waitForCrash();
+          // 3. get the crash from getLastCrashReport.
+          const firstReport = await remotely(() => require('electron').crashReporter.getLastCrashReport());
+          expect(firstReport).to.not.be.null();
+          expect(firstReport.date).to.be.an.instanceOf(Date);
+          expect((+new Date()) - (+firstReport.date)).to.be.lessThan(30000);
+        });
+      });
 
-        remotely(() => {
-          const { BrowserWindow } = require('electron');
-          const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
-          bw.loadURL('about:blank');
-          bw.webContents.executeJavaScript('process._linkedBinding(\'electron_common_v8_util\').triggerFatalErrorForTesting()');
+      describe('getParameters', () => {
+        it('returns all of the current parameters', async () => {
+          const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+          await remotely(() => {
+            require('electron').crashReporter.start({
+              submitURL: 'http://127.0.0.1',
+              extra: { extra1: 'hi' }
+            });
+          });
+          const parameters = await remotely(() => require('electron').crashReporter.getParameters());
+          expect(parameters).to.have.property('extra1', 'hi');
         });
 
-        const crash = await waitForCrash();
-        expect(crash.prod).to.equal('Electron');
-        expect(crash._productName).to.equal('electron-test-remote-control');
-        expect(crash.process_type).to.equal('renderer');
-        expect(crash['electron.v8-fatal.location']).to.equal('v8::Context::New()');
-        expect(crash['electron.v8-fatal.message']).to.equal('Circular extension dependency');
-      });
-    });
-  });
+        it('reflects added and removed parameters', async () => {
+          const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+          await remotely(() => {
+            require('electron').crashReporter.start({ submitURL: 'http://127.0.0.1' });
+            require('electron').crashReporter.addExtraParameter('hello', 'world');
+          });
+          {
+            const parameters = await remotely(() => require('electron').crashReporter.getParameters());
+            expect(parameters).to.have.property('hello', 'world');
+          }
 
-  ifdescribe(!isLinuxOnArm)('extra parameter limits', () => {
-    function stitchLongCrashParam (crash: any, paramKey: string) {
-      if (crash[paramKey]) return crash[paramKey];
-      let chunk = 1;
-      let stitched = '';
-      while (crash[`${paramKey}__${chunk}`]) {
-        stitched += crash[`${paramKey}__${chunk}`];
-        chunk++;
-      }
-      return stitched;
-    }
+          await remotely(() => { require('electron').crashReporter.removeExtraParameter('hello'); });
 
-    it('should truncate extra values longer than 5 * 4096 characters', async () => {
-      const { port, waitForCrash } = await startServer();
-      const { remotely } = await startRemoteControlApp();
-      remotely((port: number) => {
-        require('electron').crashReporter.start({
-          submitURL: `http://127.0.0.1:${port}`,
-          compress: false,
-          ignoreSystemCrashHandler: true,
-          extra: { longParam: 'a'.repeat(100000) }
+          {
+            const parameters = await remotely(() => require('electron').crashReporter.getParameters());
+            expect(parameters).not.to.have.property('hello');
+          }
         });
-        setTimeout(() => process.crash());
-      }, port);
-      const crash = await waitForCrash();
-      expect(stitchLongCrashParam(crash, 'longParam')).to.have.lengthOf(160 * 127, 'crash should have truncated longParam');
-    });
 
-    it('should omit extra keys with names longer than the maximum', async () => {
-      const kKeyLengthMax = 39;
-      const { port, waitForCrash } = await startServer();
-      const { remotely } = await startRemoteControlApp();
-      remotely((port: number, kKeyLengthMax: number) => {
-        require('electron').crashReporter.start({
-          submitURL: `http://127.0.0.1:${port}`,
-          compress: false,
-          ignoreSystemCrashHandler: true,
-          extra: {
-            ['a'.repeat(kKeyLengthMax + 10)]: 'value',
-            ['b'.repeat(kKeyLengthMax)]: 'value',
-            'not-long': 'not-long-value'
+        it('can be called in the renderer', async () => {
+          const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+          const rendererParameters = await remotely(async () => {
+            const { crashReporter, BrowserWindow } = require('electron');
+            crashReporter.start({ submitURL: 'http://' });
+            const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
+            bw.loadURL('about:blank');
+            await bw.webContents.executeJavaScript('require(\'electron\').crashReporter.addExtraParameter(\'hello\', \'world\')');
+            return bw.webContents.executeJavaScript('require(\'electron\').crashReporter.getParameters()');
+          });
+          if (process.platform === 'linux') {
+            // On Linux, 'getParameters' will also include the global parameters,
+            // because breakpad doesn't support global parameters.
+            expect(rendererParameters).to.have.property('hello', 'world');
+          } else {
+            expect(rendererParameters).to.deep.equal({ hello: 'world' });
           }
         });
-        require('electron').crashReporter.addExtraParameter('c'.repeat(kKeyLengthMax + 10), 'value');
-        setTimeout(() => process.crash());
-      }, port, kKeyLengthMax);
-      const crash = await waitForCrash();
-      expect(crash).not.to.have.property('a'.repeat(kKeyLengthMax + 10));
-      expect(crash).not.to.have.property('a'.repeat(kKeyLengthMax));
-      expect(crash).to.have.property('b'.repeat(kKeyLengthMax), 'value');
-      expect(crash).to.have.property('not-long', 'not-long-value');
-      expect(crash).not.to.have.property('c'.repeat(kKeyLengthMax + 10));
-      expect(crash).not.to.have.property('c'.repeat(kKeyLengthMax));
-    });
-  });
 
-  describe('globalExtra', () => {
-    ifit(!isLinuxOnArm)('should be sent with main process dumps', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('main', port, ['--add-global-param=globalParam:globalValue']);
-      const crash = await waitForCrash();
-      expect(crash.globalParam).to.equal('globalValue');
-    });
+        it('can be called in a node child process', async () => {
+          function slurp (stream: NodeJS.ReadableStream): Promise<string> {
+            return new Promise((resolve, reject) => {
+              const chunks: Buffer[] = [];
+              stream.on('data', chunk => { chunks.push(chunk); });
+              stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
+              stream.on('error', e => reject(e));
+            });
+          }
+          // TODO(nornagon): how to enable crashpad in a node child process...?
+          const child = childProcess.fork(path.join(__dirname, 'fixtures', 'module', 'print-crash-parameters.js'), [], { silent: true });
+          const output = await slurp(child.stdout!);
+          expect(JSON.parse(output)).to.deep.equal({ hello: 'world' });
+        });
+      });
 
-    it('should be sent with renderer process dumps', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('renderer', port, ['--add-global-param=globalParam:globalValue']);
-      const crash = await waitForCrash();
-      expect(crash.globalParam).to.equal('globalValue');
-    });
+      describe('crash dumps directory', () => {
+        it('is set by default', () => {
+          expect(app.getPath('crashDumps')).to.be.a('string');
+        });
 
-    it('should be sent with sandboxed renderer process dumps', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('sandboxed-renderer', port, ['--add-global-param=globalParam:globalValue']);
-      const crash = await waitForCrash();
-      expect(crash.globalParam).to.equal('globalValue');
-    });
+        it('is inside the user data dir', () => {
+          expect(app.getPath('crashDumps')).to.include(app.getPath('userData'));
+        });
 
-    ifit(!isLinuxOnArm)('should not be overridden by extra in main process', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('main', port, ['--add-global-param=mainProcessSpecific:global']);
-      const crash = await waitForCrash();
-      expect(crash.mainProcessSpecific).to.equal('global');
-    });
+        function crash (processType: string, remotely: Function) {
+          if (processType === 'main') {
+            return remotely(() => {
+              setTimeout(() => { process.crash(); });
+            });
+          } else if (processType === 'renderer') {
+            return remotely(() => {
+              const { BrowserWindow } = require('electron');
+              const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
+              bw.loadURL('about:blank');
+              bw.webContents.executeJavaScript('process.crash()');
+            });
+          } else if (processType === 'sandboxed-renderer') {
+            const preloadPath = path.join(__dirname, 'fixtures', 'apps', 'crash', 'sandbox-preload.js');
+            return remotely((preload: string) => {
+              const { BrowserWindow } = require('electron');
+              const bw = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload, contextIsolation: false } });
+              bw.loadURL('about:blank');
+            }, preloadPath);
+          } else if (processType === 'node') {
+            const crashScriptPath = path.join(__dirname, 'fixtures', 'apps', 'crash', 'node-crash.js');
+            return remotely((crashScriptPath: string) => {
+              const { app } = require('electron');
+              const childProcess = require('child_process');
+              const version = app.getVersion();
+              const url = 'http://127.0.0.1';
+              childProcess.fork(crashScriptPath, [url, version], { silent: true });
+            }, crashScriptPath);
+          }
+        }
 
-    ifit(!isLinuxOnArm)('should not be overridden by extra in renderer process', async () => {
-      const { port, waitForCrash } = await startServer();
-      runCrashApp('main', port, ['--add-global-param=rendererSpecific:global']);
-      const crash = await waitForCrash();
-      expect(crash.rendererSpecific).to.equal('global');
+        const processList = process.platform === 'linux' ? ['main', 'renderer', 'sandboxed-renderer']
+          : ['main', 'renderer', 'sandboxed-renderer', 'node'];
+        for (const crashingProcess of processList) {
+          describe(`when ${crashingProcess} crashes`, () => {
+            it('stores crashes in the crash dump directory when uploadToServer: false', async () => {
+              const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+              const crashesDir = await remotely(() => {
+                const { crashReporter, app } = require('electron');
+                crashReporter.start({ submitURL: 'http://127.0.0.1', uploadToServer: false, ignoreSystemCrashHandler: true });
+                return app.getPath('crashDumps');
+              });
+              let reportsDir = crashesDir;
+              if (process.platform === 'darwin' || (process.platform === 'linux' && withLinuxCrashpad)) {
+                reportsDir = path.join(crashesDir, 'completed');
+              } else if (process.platform === 'win32') {
+                reportsDir = path.join(crashesDir, 'reports');
+              }
+              const newFileAppeared = waitForNewFileInDir(reportsDir);
+              crash(crashingProcess, remotely);
+              const newFiles = await newFileAppeared;
+              expect(newFiles.length).to.be.greaterThan(0);
+              if (process.platform === 'linux' && !withLinuxCrashpad) {
+                if (crashingProcess === 'main') {
+                  expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8}-[0-9a-f]{8}\.dmp$/);
+                } else {
+                  const process = crashingProcess === 'sandboxed-renderer' ? 'renderer' : crashingProcess;
+                  const regex = RegExp(`chromium-${process}-minidump-[0-9a-f]{16}.dmp`);
+                  expect(newFiles[0]).to.match(regex);
+                }
+              } else {
+                expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.dmp$/);
+              }
+            });
+
+            it('respects an overridden crash dump directory', async () => {
+              const { remotely } = await startRemoteControlApp(crashpadExtraArgs);
+              const crashesDir = path.join(app.getPath('temp'), uuid.v4());
+              const remoteCrashesDir = await remotely((crashesDir: string) => {
+                const { crashReporter, app } = require('electron');
+                app.setPath('crashDumps', crashesDir);
+                crashReporter.start({ submitURL: 'http://127.0.0.1', uploadToServer: false, ignoreSystemCrashHandler: true });
+                return app.getPath('crashDumps');
+              }, crashesDir);
+              expect(remoteCrashesDir).to.equal(crashesDir);
+
+              let reportsDir = crashesDir;
+              if (process.platform === 'darwin' || (process.platform === 'linux' && withLinuxCrashpad)) {
+                reportsDir = path.join(crashesDir, 'completed');
+              } else if (process.platform === 'win32') {
+                reportsDir = path.join(crashesDir, 'reports');
+              }
+              const newFileAppeared = waitForNewFileInDir(reportsDir);
+              crash(crashingProcess, remotely);
+              const newFiles = await newFileAppeared;
+              expect(newFiles.length).to.be.greaterThan(0);
+              if (process.platform === 'linux' && !withLinuxCrashpad) {
+                if (crashingProcess === 'main') {
+                  expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8}-[0-9a-f]{8}\.dmp$/);
+                } else {
+                  const process = crashingProcess !== 'sandboxed-renderer' ? crashingProcess : 'renderer';
+                  const regex = RegExp(`chromium-${process}-minidump-[0-9a-f]{16}.dmp`);
+                  expect(newFiles[0]).to.match(regex);
+                }
+              } else {
+                expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.dmp$/);
+              }
+            });
+          });
+        }
+      });
     });
-  });
-
-  // TODO(nornagon): also test crashing main / sandboxed renderers.
-  ifit(!isWindowsOnArm)('should not send a minidump when uploadToServer is false', async () => {
-    const { port, waitForCrash, getCrashes } = await startServer();
-    waitForCrash().then(() => expect.fail('expected not to receive a dump'));
-    await runCrashApp('renderer', port, ['--no-upload']);
-    // wait a sec in case the crash reporter is about to upload a crash
-    await delay(1000);
-    expect(getCrashes()).to.have.length(0);
-  });
+  }
 
   describe('start() option validation', () => {
     it('requires that the submitURL option be specified', () => {
@@ -380,54 +611,6 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
     });
   });
 
-  describe('getUploadedReports', () => {
-    it('returns an array of reports', async () => {
-      const { remotely } = await startRemoteControlApp();
-      await remotely(() => {
-        require('electron').crashReporter.start({ submitURL: 'http://127.0.0.1' });
-      });
-      const reports = await remotely(() => require('electron').crashReporter.getUploadedReports());
-      expect(reports).to.be.an('array');
-    });
-  });
-
-  // TODO(nornagon): re-enable on woa
-  ifdescribe(!isWindowsOnArm)('getLastCrashReport', () => {
-    it('returns the last uploaded report', async () => {
-      const { remotely } = await startRemoteControlApp();
-      const { port, waitForCrash } = await startServer();
-
-      // 0. clear the crash reports directory.
-      const dir = await remotely(() => require('electron').app.getPath('crashDumps'));
-      try {
-        fs.rmdirSync(dir, { recursive: true });
-        fs.mkdirSync(dir);
-      } catch (e) { /* ignore */ }
-
-      // 1. start the crash reporter.
-      await remotely((port: number) => {
-        require('electron').crashReporter.start({
-          submitURL: `http://127.0.0.1:${port}`,
-          compress: false,
-          ignoreSystemCrashHandler: true
-        });
-      }, [port]);
-      // 2. generate a crash in the renderer.
-      remotely(() => {
-        const { BrowserWindow } = require('electron');
-        const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
-        bw.loadURL('about:blank');
-        bw.webContents.executeJavaScript('process.crash()');
-      });
-      await waitForCrash();
-      // 3. get the crash from getLastCrashReport.
-      const firstReport = await remotely(() => require('electron').crashReporter.getLastCrashReport());
-      expect(firstReport).to.not.be.null();
-      expect(firstReport.date).to.be.an.instanceOf(Date);
-      expect((+new Date()) - (+firstReport.date)).to.be.lessThan(30000);
-    });
-  });
-
   describe('getUploadToServer()', () => {
     it('returns true when uploadToServer is set to true (by default)', async () => {
       const { remotely } = await startRemoteControlApp();
@@ -454,183 +637,6 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_
     });
   });
 
-  describe('getParameters', () => {
-    it('returns all of the current parameters', async () => {
-      const { remotely } = await startRemoteControlApp();
-      await remotely(() => {
-        require('electron').crashReporter.start({
-          submitURL: 'http://127.0.0.1',
-          extra: { extra1: 'hi' }
-        });
-      });
-      const parameters = await remotely(() => require('electron').crashReporter.getParameters());
-      expect(parameters).to.have.property('extra1', 'hi');
-    });
-
-    it('reflects added and removed parameters', async () => {
-      const { remotely } = await startRemoteControlApp();
-      await remotely(() => {
-        require('electron').crashReporter.start({ submitURL: 'http://127.0.0.1' });
-        require('electron').crashReporter.addExtraParameter('hello', 'world');
-      });
-      {
-        const parameters = await remotely(() => require('electron').crashReporter.getParameters());
-        expect(parameters).to.have.property('hello', 'world');
-      }
-
-      await remotely(() => { require('electron').crashReporter.removeExtraParameter('hello'); });
-
-      {
-        const parameters = await remotely(() => require('electron').crashReporter.getParameters());
-        expect(parameters).not.to.have.property('hello');
-      }
-    });
-
-    it('can be called in the renderer', async () => {
-      const { remotely } = await startRemoteControlApp();
-      const rendererParameters = await remotely(async () => {
-        const { crashReporter, BrowserWindow } = require('electron');
-        crashReporter.start({ submitURL: 'http://' });
-        const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
-        bw.loadURL('about:blank');
-        await bw.webContents.executeJavaScript('require(\'electron\').crashReporter.addExtraParameter(\'hello\', \'world\')');
-        return bw.webContents.executeJavaScript('require(\'electron\').crashReporter.getParameters()');
-      });
-      if (process.platform === 'linux') {
-        // On Linux, 'getParameters' will also include the global parameters,
-        // because breakpad doesn't support global parameters.
-        expect(rendererParameters).to.have.property('hello', 'world');
-      } else {
-        expect(rendererParameters).to.deep.equal({ hello: 'world' });
-      }
-    });
-
-    it('can be called in a node child process', async () => {
-      function slurp (stream: NodeJS.ReadableStream): Promise<string> {
-        return new Promise((resolve, reject) => {
-          const chunks: Buffer[] = [];
-          stream.on('data', chunk => { chunks.push(chunk); });
-          stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
-          stream.on('error', e => reject(e));
-        });
-      }
-      const child = childProcess.fork(path.join(__dirname, 'fixtures', 'module', 'print-crash-parameters.js'), [], { silent: true });
-      const output = await slurp(child.stdout!);
-      expect(JSON.parse(output)).to.deep.equal({ hello: 'world' });
-    });
-  });
-
-  describe('crash dumps directory', () => {
-    it('is set by default', () => {
-      expect(app.getPath('crashDumps')).to.be.a('string');
-    });
-
-    it('is inside the user data dir', () => {
-      expect(app.getPath('crashDumps')).to.include(app.getPath('userData'));
-    });
-
-    function crash (processType: string, remotely: Function) {
-      if (processType === 'main') {
-        return remotely(() => {
-          setTimeout(() => { process.crash(); });
-        });
-      } else if (processType === 'renderer') {
-        return remotely(() => {
-          const { BrowserWindow } = require('electron');
-          const bw = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
-          bw.loadURL('about:blank');
-          bw.webContents.executeJavaScript('process.crash()');
-        });
-      } else if (processType === 'sandboxed-renderer') {
-        const preloadPath = path.join(__dirname, 'fixtures', 'apps', 'crash', 'sandbox-preload.js');
-        return remotely((preload: string) => {
-          const { BrowserWindow } = require('electron');
-          const bw = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload, contextIsolation: false } });
-          bw.loadURL('about:blank');
-        }, preloadPath);
-      } else if (processType === 'node') {
-        const crashScriptPath = path.join(__dirname, 'fixtures', 'apps', 'crash', 'node-crash.js');
-        return remotely((crashScriptPath: string) => {
-          const { app } = require('electron');
-          const childProcess = require('child_process');
-          const version = app.getVersion();
-          const url = 'http://127.0.0.1';
-          childProcess.fork(crashScriptPath, [url, version], { silent: true });
-        }, crashScriptPath);
-      }
-    }
-
-    const processList = process.platform === 'linux' ? ['main', 'renderer', 'sandboxed-renderer']
-      : ['main', 'renderer', 'sandboxed-renderer', 'node'];
-    for (const crashingProcess of processList) {
-      describe(`when ${crashingProcess} crashes`, () => {
-        it('stores crashes in the crash dump directory when uploadToServer: false', async () => {
-          const { remotely } = await startRemoteControlApp();
-          const crashesDir = await remotely(() => {
-            const { crashReporter, app } = require('electron');
-            crashReporter.start({ submitURL: 'http://127.0.0.1', uploadToServer: false, ignoreSystemCrashHandler: true });
-            return app.getPath('crashDumps');
-          });
-          let reportsDir = crashesDir;
-          if (process.platform === 'darwin') {
-            reportsDir = path.join(crashesDir, 'completed');
-          } else if (process.platform === 'win32') {
-            reportsDir = path.join(crashesDir, 'reports');
-          }
-          const newFileAppeared = waitForNewFileInDir(reportsDir);
-          crash(crashingProcess, remotely);
-          const newFiles = await newFileAppeared;
-          expect(newFiles.length).to.be.greaterThan(0);
-          if (process.platform === 'linux') {
-            if (crashingProcess === 'main') {
-              expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8}-[0-9a-f]{8}\.dmp$/);
-            } else {
-              const process = crashingProcess === 'sandboxed-renderer' ? 'renderer' : crashingProcess;
-              const regex = RegExp(`chromium-${process}-minidump-[0-9a-f]{16}.dmp`);
-              expect(newFiles[0]).to.match(regex);
-            }
-          } else {
-            expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.dmp$/);
-          }
-        });
-
-        it('respects an overridden crash dump directory', async () => {
-          const { remotely } = await startRemoteControlApp();
-          const crashesDir = path.join(app.getPath('temp'), uuid.v4());
-          const remoteCrashesDir = await remotely((crashesDir: string) => {
-            const { crashReporter, app } = require('electron');
-            app.setPath('crashDumps', crashesDir);
-            crashReporter.start({ submitURL: 'http://127.0.0.1', uploadToServer: false, ignoreSystemCrashHandler: true });
-            return app.getPath('crashDumps');
-          }, crashesDir);
-          expect(remoteCrashesDir).to.equal(crashesDir);
-
-          let reportsDir = crashesDir;
-          if (process.platform === 'darwin') {
-            reportsDir = path.join(crashesDir, 'completed');
-          } else if (process.platform === 'win32') {
-            reportsDir = path.join(crashesDir, 'reports');
-          }
-          const newFileAppeared = waitForNewFileInDir(reportsDir);
-          crash(crashingProcess, remotely);
-          const newFiles = await newFileAppeared;
-          expect(newFiles.length).to.be.greaterThan(0);
-          if (process.platform === 'linux') {
-            if (crashingProcess === 'main') {
-              expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8}-[0-9a-f]{8}\.dmp$/);
-            } else {
-              const process = crashingProcess !== 'sandboxed-renderer' ? crashingProcess : 'renderer';
-              const regex = RegExp(`chromium-${process}-minidump-[0-9a-f]{16}.dmp`);
-              expect(newFiles[0]).to.match(regex);
-            }
-          } else {
-            expect(newFiles[0]).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.dmp$/);
-          }
-        });
-      });
-    }
-  });
-
   describe('when not started', () => {
     it('does not prevent process from crashing', async () => {
       const appPath = path.join(__dirname, '..', 'spec', 'fixtures', 'api', 'cookie-app');