fix: remove catch-all HandleScope (#22531)

This commit is contained in:
Jeremy Apthorp 2020-03-10 18:16:58 -07:00 committed by GitHub
parent 4bca5205bb
commit 19314d3caf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 131 additions and 75 deletions

View file

@ -139,23 +139,20 @@ int NodeMain(int argc, char* argv[]) {
JavascriptEnvironment gin_env(loop); JavascriptEnvironment gin_env(loop);
v8::Isolate* isolate = gin_env.isolate(); v8::Isolate* isolate = gin_env.isolate();
v8::Isolate::Scope isolate_scope(isolate);
v8::Locker locker(isolate);
node::Environment* env = nullptr;
node::IsolateData* isolate_data = nullptr;
{
v8::HandleScope scope(isolate);
node::IsolateData* isolate_data = isolate_data = node::CreateIsolateData(isolate, loop, gin_env.platform());
node::CreateIsolateData(isolate, loop, gin_env.platform());
CHECK_NE(nullptr, isolate_data); CHECK_NE(nullptr, isolate_data);
v8::Locker locker(isolate); env = node::CreateEnvironment(isolate_data, gin_env.context(), argc, argv,
v8::Isolate::Scope isolate_scope(isolate); exec_argc, exec_argv);
v8::HandleScope handle_scope(isolate);
node::Environment* env = node::CreateEnvironment(
isolate_data, gin_env.context(), argc, argv, exec_argc, exec_argv);
CHECK_NE(nullptr, env); CHECK_NE(nullptr, env);
// Enable support for v8 inspector.
NodeDebugger node_debugger(env);
node_debugger.Start();
// TODO(codebytere): we shouldn't have to call this - upstream? // TODO(codebytere): we shouldn't have to call this - upstream?
env->InitializeDiagnostics(); env->InitializeDiagnostics();
@ -172,7 +169,8 @@ int NodeMain(int argc, char* argv[]) {
// Setup process.crashReporter.start in child node processes // Setup process.crashReporter.start in child node processes
gin_helper::Dictionary reporter = gin::Dictionary::CreateEmpty(isolate); gin_helper::Dictionary reporter = gin::Dictionary::CreateEmpty(isolate);
reporter.SetMethod("start", &crash_reporter::CrashReporter::StartInstance); reporter.SetMethod("start",
&crash_reporter::CrashReporter::StartInstance);
#if !defined(OS_LINUX) #if !defined(OS_LINUX)
reporter.SetMethod("addExtraParameter", &AddExtraParameter); reporter.SetMethod("addExtraParameter", &AddExtraParameter);
@ -185,9 +183,15 @@ int NodeMain(int argc, char* argv[]) {
if (process.Get("versions", &versions)) { if (process.Get("versions", &versions)) {
versions.SetReadOnly(ELECTRON_PROJECT_NAME, ELECTRON_VERSION_STRING); versions.SetReadOnly(ELECTRON_PROJECT_NAME, ELECTRON_VERSION_STRING);
} }
}
// Enable support for v8 inspector.
NodeDebugger node_debugger(env);
node_debugger.Start();
// TODO(codebytere): we should try to handle this upstream. // TODO(codebytere): we should try to handle this upstream.
{ {
v8::HandleScope scope(isolate);
node::InternalCallbackScope callback_scope( node::InternalCallbackScope callback_scope(
env, v8::Local<v8::Object>(), {1, 0}, env, v8::Local<v8::Object>(), {1, 0},
node::InternalCallbackScope::kAllowEmptyResource | node::InternalCallbackScope::kAllowEmptyResource |

View file

@ -788,8 +788,11 @@ void App::RenderProcessReady(content::RenderProcessHost* host) {
// `RenderProcessPreferences`, so this is at least more explicit... // `RenderProcessPreferences`, so this is at least more explicit...
content::WebContents* web_contents = content::WebContents* web_contents =
ElectronBrowserClient::Get()->GetWebContentsFromProcessID(host->GetID()); ElectronBrowserClient::Get()->GetWebContentsFromProcessID(host->GetID());
if (web_contents) if (web_contents) {
WebContents::FromOrCreate(v8::Isolate::GetCurrent(), web_contents); v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
WebContents::FromOrCreate(isolate, web_contents);
}
} }
void App::RenderProcessDisconnected(base::ProcessId host_pid) { void App::RenderProcessDisconnected(base::ProcessId host_pid) {

View file

@ -314,6 +314,7 @@ v8::Local<v8::Promise> Cookies::FlushStore() {
} }
void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) { void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) {
v8::HandleScope scope(isolate());
Emit("changed", gin::ConvertToV8(isolate(), change.cookie), Emit("changed", gin::ConvertToV8(isolate(), change.cookie),
gin::ConvertToV8(isolate(), change.cause), gin::ConvertToV8(isolate(), change.cause),
gin::ConvertToV8(isolate(), gin::ConvertToV8(isolate(),

View file

@ -214,6 +214,7 @@ void Menu::OnMenuWillClose() {
} }
void Menu::OnMenuWillShow() { void Menu::OnMenuWillShow() {
v8::HandleScope scope(isolate());
g_menus[weak_map_id()] = v8::Global<v8::Object>(isolate(), GetWrapper()); g_menus[weak_map_id()] = v8::Global<v8::Object>(isolate(), GetWrapper());
Emit("menu-will-show"); Emit("menu-will-show");
} }

View file

@ -85,8 +85,10 @@ ServiceWorkerContext::~ServiceWorkerContext() {
void ServiceWorkerContext::OnReportConsoleMessage( void ServiceWorkerContext::OnReportConsoleMessage(
int64_t version_id, int64_t version_id,
const content::ConsoleMessage& message) { const content::ConsoleMessage& message) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
Emit("console-message", Emit("console-message",
gin::DataObjectBuilder(v8::Isolate::GetCurrent()) gin::DataObjectBuilder(isolate)
.Set("versionId", version_id) .Set("versionId", version_id)
.Set("source", MessageSourceToString(message.source)) .Set("source", MessageSourceToString(message.source))
.Set("level", static_cast<int32_t>(message.message_level)) .Set("level", static_cast<int32_t>(message.message_level))

View file

@ -433,6 +433,7 @@ void SimpleURLLoaderWrapper::OnRetry(base::OnceClosure start_retry) {}
void SimpleURLLoaderWrapper::OnResponseStarted( void SimpleURLLoaderWrapper::OnResponseStarted(
const GURL& final_url, const GURL& final_url,
const network::mojom::URLResponseHead& response_head) { const network::mojom::URLResponseHead& response_head) {
v8::HandleScope scope(isolate());
gin::Dictionary dict = gin::Dictionary::CreateEmpty(isolate()); gin::Dictionary dict = gin::Dictionary::CreateEmpty(isolate());
dict.Set("statusCode", response_head.headers->response_code()); dict.Set("statusCode", response_head.headers->response_code());
dict.Set("statusMessage", response_head.headers->GetStatusText()); dict.Set("statusMessage", response_head.headers->GetStatusText());

View file

@ -6,8 +6,10 @@
#include <utility> #include <utility>
#include "gin/data_object_builder.h"
#include "gin/object_template_builder.h" #include "gin/object_template_builder.h"
#include "shell/common/gin_converters/blink_converter.h" #include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/std_converter.h"
namespace gin_helper { namespace gin_helper {
@ -15,7 +17,16 @@ gin::WrapperInfo Event::kWrapperInfo = {gin::kEmbedderNativeGin};
Event::Event() {} Event::Event() {}
Event::~Event() = default; Event::~Event() {
if (callback_) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
auto message = gin::DataObjectBuilder(isolate)
.Set("error", "reply was never sent")
.Build();
SendReply(isolate, message);
}
}
void Event::SetCallback(InvokeCallback callback) { void Event::SetCallback(InvokeCallback callback) {
DCHECK(!callback_); DCHECK(!callback_);

View file

@ -28,10 +28,11 @@ void AutofillDriver::ShowAutofillPopup(
const gfx::RectF& bounds, const gfx::RectF& bounds,
const std::vector<base::string16>& values, const std::vector<base::string16>& values,
const std::vector<base::string16>& labels) { const std::vector<base::string16>& labels) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
auto* web_contents = auto* web_contents =
api::WebContents::From( api::WebContents::From(isolate, content::WebContents::FromRenderFrameHost(
v8::Isolate::GetCurrent(), render_frame_host_))
content::WebContents::FromRenderFrameHost(render_frame_host_))
.get(); .get();
if (!web_contents || !web_contents->owner_window()) if (!web_contents || !web_contents->owner_window())
return; return;

View file

@ -1300,6 +1300,7 @@ bool ElectronBrowserClient::WillInterceptWebSocket(
return false; return false;
v8::Isolate* isolate = v8::Isolate::GetCurrent(); v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
auto* browser_context = frame->GetProcess()->GetBrowserContext(); auto* browser_context = frame->GetProcess()->GetBrowserContext();
auto web_request = api::WebRequest::FromOrCreate(isolate, browser_context); auto web_request = api::WebRequest::FromOrCreate(isolate, browser_context);
@ -1320,6 +1321,7 @@ void ElectronBrowserClient::CreateWebSocket(
mojo::PendingRemote<network::mojom::WebSocketHandshakeClient> mojo::PendingRemote<network::mojom::WebSocketHandshakeClient>
handshake_client) { handshake_client) {
v8::Isolate* isolate = v8::Isolate::GetCurrent(); v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
auto* browser_context = frame->GetProcess()->GetBrowserContext(); auto* browser_context = frame->GetProcess()->GetBrowserContext();
auto web_request = api::WebRequest::FromOrCreate(isolate, browser_context); auto web_request = api::WebRequest::FromOrCreate(isolate, browser_context);
DCHECK(web_request.get()); DCHECK(web_request.get());
@ -1345,6 +1347,7 @@ bool ElectronBrowserClient::WillCreateURLLoaderFactory(
bool* disable_secure_dns, bool* disable_secure_dns,
network::mojom::URLLoaderFactoryOverridePtr* factory_override) { network::mojom::URLLoaderFactoryOverridePtr* factory_override) {
v8::Isolate* isolate = v8::Isolate::GetCurrent(); v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
api::Protocol* protocol = api::Protocol* protocol =
api::Protocol::FromWrappedClass(isolate, browser_context); api::Protocol::FromWrappedClass(isolate, browser_context);
DCHECK(protocol); DCHECK(protocol);

View file

@ -293,6 +293,8 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
// avoid conflicts we only initialize our V8 environment after that. // avoid conflicts we only initialize our V8 environment after that.
js_env_ = std::make_unique<JavascriptEnvironment>(node_bindings_->uv_loop()); js_env_ = std::make_unique<JavascriptEnvironment>(node_bindings_->uv_loop());
v8::HandleScope scope(js_env_->isolate());
node_bindings_->Initialize(); node_bindings_->Initialize();
// Create the global environment. // Create the global environment.
node::Environment* env = node_bindings_->CreateEnvironment( node::Environment* env = node_bindings_->CreateEnvironment(

View file

@ -28,8 +28,9 @@ ElectronNavigationThrottle::WillRedirectRequest() {
return PROCEED; return PROCEED;
} }
auto api_contents = v8::Isolate* isolate = v8::Isolate::GetCurrent();
electron::api::WebContents::From(v8::Isolate::GetCurrent(), contents); v8::HandleScope scope(isolate);
auto api_contents = electron::api::WebContents::From(isolate, contents);
if (api_contents.IsEmpty()) { if (api_contents.IsEmpty()) {
// No need to emit any event if the WebContents is not available in JS. // No need to emit any event if the WebContents is not available in JS.
return PROCEED; return PROCEED;

View file

@ -29,13 +29,21 @@ JavascriptEnvironment::JavascriptEnvironment(uv_loop_t* event_loop)
gin::IsolateHolder::IsolateType::kUtility, gin::IsolateHolder::IsolateType::kUtility,
gin::IsolateHolder::IsolateCreationMode::kNormal, gin::IsolateHolder::IsolateCreationMode::kNormal,
isolate_), isolate_),
isolate_scope_(isolate_), locker_(isolate_) {
locker_(isolate_), isolate_->Enter();
handle_scope_(isolate_), v8::HandleScope scope(isolate_);
context_(isolate_, node::NewContext(isolate_)), auto context = node::NewContext(isolate_);
context_scope_(v8::Local<v8::Context>::New(isolate_, context_)) {} context_ = v8::Global<v8::Context>(isolate_, context);
context->Enter();
}
JavascriptEnvironment::~JavascriptEnvironment() = default; JavascriptEnvironment::~JavascriptEnvironment() {
{
v8::HandleScope scope(isolate_);
context_.Get(isolate_)->Exit();
}
isolate_->Exit();
}
v8::Isolate* JavascriptEnvironment::Initialize(uv_loop_t* event_loop) { v8::Isolate* JavascriptEnvironment::Initialize(uv_loop_t* event_loop) {
auto* cmd = base::CommandLine::ForCurrentProcess(); auto* cmd = base::CommandLine::ForCurrentProcess();

View file

@ -41,11 +41,8 @@ class JavascriptEnvironment {
v8::Isolate* isolate_; v8::Isolate* isolate_;
gin::IsolateHolder isolate_holder_; gin::IsolateHolder isolate_holder_;
v8::Isolate::Scope isolate_scope_;
v8::Locker locker_; v8::Locker locker_;
v8::HandleScope handle_scope_;
v8::Global<v8::Context> context_; v8::Global<v8::Context> context_;
v8::Context::Scope context_scope_;
std::unique_ptr<MicrotasksRunner> microtasks_runner_; std::unique_ptr<MicrotasksRunner> microtasks_runner_;

View file

@ -51,6 +51,7 @@ void LoginHandler::EmitEvent(
scoped_refptr<net::HttpResponseHeaders> response_headers, scoped_refptr<net::HttpResponseHeaders> response_headers,
bool first_auth_attempt) { bool first_auth_attempt) {
v8::Isolate* isolate = v8::Isolate::GetCurrent(); v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope scope(isolate);
auto api_web_contents = api::WebContents::From(isolate, web_contents()); auto api_web_contents = api::WebContents::From(isolate, web_contents());
if (api_web_contents.IsEmpty()) { if (api_web_contents.IsEmpty()) {
@ -58,8 +59,6 @@ void LoginHandler::EmitEvent(
return; return;
} }
v8::HandleScope scope(isolate);
auto details = gin::Dictionary::CreateEmpty(isolate); auto details = gin::Dictionary::CreateEmpty(isolate);
details.Set("url", url); details.Set("url", url);

View file

@ -92,6 +92,7 @@ void NodeStreamLoader::ReadMore() {
} }
is_reading_ = true; is_reading_ = true;
auto weak = weak_factory_.GetWeakPtr(); auto weak = weak_factory_.GetWeakPtr();
v8::HandleScope scope(isolate_);
// buffer = emitter.read() // buffer = emitter.read()
v8::MaybeLocal<v8::Value> ret = node::MakeCallback( v8::MaybeLocal<v8::Value> ret = node::MakeCallback(
isolate_, emitter_.Get(isolate_), "read", 0, nullptr, {0, 0}); isolate_, emitter_.Get(isolate_), "read", 0, nullptr, {0, 0});

View file

@ -302,6 +302,7 @@ void OpenDialogCompletion(int chosen,
NSOpenPanel* dialog, NSOpenPanel* dialog,
bool security_scoped_bookmarks, bool security_scoped_bookmarks,
gin_helper::Promise<gin_helper::Dictionary> promise) { gin_helper::Promise<gin_helper::Dictionary> promise) {
v8::HandleScope scope(promise.isolate());
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate()); gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
if (chosen == NSFileHandlingPanelCancelButton) { if (chosen == NSFileHandlingPanelCancelButton) {
dict.Set("canceled", true); dict.Set("canceled", true);
@ -379,6 +380,7 @@ void SaveDialogCompletion(int chosen,
NSSavePanel* dialog, NSSavePanel* dialog,
bool security_scoped_bookmarks, bool security_scoped_bookmarks,
gin_helper::Promise<gin_helper::Dictionary> promise) { gin_helper::Promise<gin_helper::Dictionary> promise) {
v8::HandleScope scope(promise.isolate());
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate()); gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
if (chosen == NSFileHandlingPanelCancelButton) { if (chosen == NSFileHandlingPanelCancelButton) {
dict.Set("canceled", true); dict.Set("canceled", true);

View file

@ -38,6 +38,7 @@ v8::Local<v8::Object> CreateEvent(v8::Isolate* isolate,
} }
v8::Local<v8::Context> context = isolate->GetCurrentContext(); v8::Local<v8::Context> context = isolate->GetCurrentContext();
CHECK(!context.IsEmpty());
v8::Local<v8::Object> event = v8::Local<v8::Object> event =
v8::Local<v8::ObjectTemplate>::New(isolate, event_template) v8::Local<v8::ObjectTemplate>::New(isolate, event_template)
->NewInstance(context) ->NewInstance(context)

View file

@ -55,13 +55,16 @@ class TrackableObject : public TrackableObjectBase, public EventEmitter<T> {
public: public:
// Mark the JS object as destroyed. // Mark the JS object as destroyed.
void MarkDestroyed() { void MarkDestroyed() {
v8::HandleScope scope(gin_helper::Wrappable<T>::isolate());
v8::Local<v8::Object> wrapper = gin_helper::Wrappable<T>::GetWrapper(); v8::Local<v8::Object> wrapper = gin_helper::Wrappable<T>::GetWrapper();
if (!wrapper.IsEmpty()) { if (!wrapper.IsEmpty()) {
wrapper->SetAlignedPointerInInternalField(0, nullptr); wrapper->SetAlignedPointerInInternalField(0, nullptr);
gin_helper::WrappableBase::wrapper_.ClearWeak();
} }
} }
bool IsDestroyed() { bool IsDestroyed() {
v8::HandleScope scope(gin_helper::Wrappable<T>::isolate());
v8::Local<v8::Object> wrapper = gin_helper::Wrappable<T>::GetWrapper(); v8::Local<v8::Object> wrapper = gin_helper::Wrappable<T>::GetWrapper();
return wrapper->InternalFieldCount() == 0 || return wrapper->InternalFieldCount() == 0 ||
wrapper->GetAlignedPointerFromInternalField(0) == nullptr; wrapper->GetAlignedPointerFromInternalField(0) == nullptr;
@ -72,6 +75,7 @@ class TrackableObject : public TrackableObjectBase, public EventEmitter<T> {
if (!weak_map_) if (!weak_map_)
return nullptr; return nullptr;
v8::HandleScope scope(isolate);
v8::MaybeLocal<v8::Object> object = weak_map_->Get(isolate, id); v8::MaybeLocal<v8::Object> object = weak_map_->Get(isolate, id);
if (object.IsEmpty()) if (object.IsEmpty())
return nullptr; return nullptr;

View file

@ -16,6 +16,7 @@ WrappableBase::~WrappableBase() {
if (wrapper_.IsEmpty()) if (wrapper_.IsEmpty())
return; return;
v8::HandleScope scope(isolate());
GetWrapper()->SetAlignedPointerInInternalField(0, nullptr); GetWrapper()->SetAlignedPointerInInternalField(0, nullptr);
wrapper_.ClearWeak(); wrapper_.ClearWeak();
wrapper_.Reset(); wrapper_.Reset();
@ -49,7 +50,8 @@ void WrappableBase::InitWith(v8::Isolate* isolate,
isolate_ = isolate; isolate_ = isolate;
wrapper->SetAlignedPointerInInternalField(0, this); wrapper->SetAlignedPointerInInternalField(0, this);
wrapper_.Reset(isolate, wrapper); wrapper_.Reset(isolate, wrapper);
wrapper_.SetWeak(this, FirstWeakCallback, v8::WeakCallbackType::kParameter); wrapper_.SetWeak(this, FirstWeakCallback,
v8::WeakCallbackType::kInternalFields);
// Call object._init if we have one. // Call object._init if we have one.
v8::Local<v8::Function> init; v8::Local<v8::Function> init;
@ -62,24 +64,21 @@ void WrappableBase::InitWith(v8::Isolate* isolate,
// static // static
void WrappableBase::FirstWeakCallback( void WrappableBase::FirstWeakCallback(
const v8::WeakCallbackInfo<WrappableBase>& data) { const v8::WeakCallbackInfo<WrappableBase>& data) {
WrappableBase* wrappable = data.GetParameter(); WrappableBase* wrappable =
static_cast<WrappableBase*>(data.GetInternalField(0));
if (wrappable) {
wrappable->wrapper_.Reset(); wrappable->wrapper_.Reset();
data.SetSecondPassCallback(SecondWeakCallback); data.SetSecondPassCallback(SecondWeakCallback);
} }
}
// static // static
void WrappableBase::SecondWeakCallback( void WrappableBase::SecondWeakCallback(
const v8::WeakCallbackInfo<WrappableBase>& data) { const v8::WeakCallbackInfo<WrappableBase>& data) {
// Certain classes (for example api::WebContents and api::WebContentsView) WrappableBase* wrappable =
// are running JS code in the destructor, while V8 may crash when JS code static_cast<WrappableBase*>(data.GetInternalField(0));
// runs inside weak callback. if (wrappable)
// delete wrappable;
// We work around this problem by delaying the deletion to next tick where
// garbage collection is done.
base::ThreadTaskRunnerHandle::Get()->PostNonNestableTask(
FROM_HERE,
base::BindOnce([](WrappableBase* wrappable) { delete wrappable; },
base::Unretained(data.GetParameter())));
} }
namespace internal { namespace internal {

View file

@ -52,6 +52,8 @@ class WrappableBase {
// Helper to init with arguments. // Helper to init with arguments.
void InitWithArgs(gin::Arguments* args); void InitWithArgs(gin::Arguments* args);
v8::Global<v8::Object> wrapper_; // Weak
private: private:
static void FirstWeakCallback( static void FirstWeakCallback(
const v8::WeakCallbackInfo<WrappableBase>& data); const v8::WeakCallbackInfo<WrappableBase>& data);
@ -59,7 +61,6 @@ class WrappableBase {
const v8::WeakCallbackInfo<WrappableBase>& data); const v8::WeakCallbackInfo<WrappableBase>& data);
v8::Isolate* isolate_ = nullptr; v8::Isolate* isolate_ = nullptr;
v8::Global<v8::Object> wrapper_; // Weak
DISALLOW_COPY_AND_ASSIGN(WrappableBase); DISALLOW_COPY_AND_ASSIGN(WrappableBase);
}; };

View file

@ -1,5 +1,8 @@
import { expect } from 'chai' import { expect } from 'chai'
import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron' import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron'
import { emittedOnce } from './events-helpers'
const v8Util = process.electronBinding('v8_util')
describe('ipc module', () => { describe('ipc module', () => {
describe('invoke', () => { describe('invoke', () => {
@ -103,6 +106,17 @@ describe('ipc module', () => {
ipcMain.removeHandler('test') ipcMain.removeHandler('test')
} }
}) })
it('throws an error in the renderer if the reply callback is dropped', async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ipcMain.handleOnce('test', () => new Promise(resolve => {
setTimeout(() => v8Util.requestGarbageCollectionForTesting())
/* never resolve */
}))
w.webContents.executeJavaScript(`(${rendererInvoke})()`)
const [, { error }] = await emittedOnce(ipcMain, 'result')
expect(error).to.match(/reply was never sent/)
})
}) })
describe('ordering', () => { describe('ordering', () => {