fix: ensure correct ordering of sendSync w.r.t. send (#18630)

This commit is contained in:
Jeremy Apthorp 2019-06-04 17:10:31 -07:00 коммит произвёл GitHub
Родитель ed5fb4a720
Коммит 9e8bd433df
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 131 добавлений и 65 удалений

Просмотреть файл

@ -36,15 +36,21 @@ RenderFrame* GetCurrentRenderFrame() {
class IPCRenderer : public mate::Wrappable<IPCRenderer> { class IPCRenderer : public mate::Wrappable<IPCRenderer> {
public: public:
explicit IPCRenderer(v8::Isolate* isolate) { explicit IPCRenderer(v8::Isolate* isolate)
: task_runner_(base::CreateSingleThreadTaskRunnerWithTraits({})) {
Init(isolate); Init(isolate);
RenderFrame* render_frame = GetCurrentRenderFrame(); RenderFrame* render_frame = GetCurrentRenderFrame();
DCHECK(render_frame); DCHECK(render_frame);
render_frame->GetRemoteInterfaces()->GetInterface(
mojo::MakeRequest(&electron_browser_ptr_)); // Bind the interface on the background runner. All accesses will be via
render_frame->GetRemoteInterfaces()->GetInterface( // the thread-safe pointer. This is to support our "fake-sync"
mojo::MakeRequest(&electron_browser_sync_ptr_)); // MessageSync() hack; see the comment in IPCRenderer::SendSync.
atom::mojom::ElectronBrowserPtrInfo info;
render_frame->GetRemoteInterfaces()->GetInterface(mojo::MakeRequest(&info));
electron_browser_ptr_ = atom::mojom::ThreadSafeElectronBrowserPtr::Create(
std::move(info), task_runner_);
} }
static void BuildPrototype(v8::Isolate* isolate, static void BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype) { v8::Local<v8::FunctionTemplate> prototype) {
prototype->SetClassName(mate::StringToV8(isolate, "IPCRenderer")); prototype->SetClassName(mate::StringToV8(isolate, "IPCRenderer"));
@ -55,15 +61,15 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
.SetMethod("sendToHost", &IPCRenderer::SendToHost) .SetMethod("sendToHost", &IPCRenderer::SendToHost)
.SetMethod("invoke", &IPCRenderer::Invoke); .SetMethod("invoke", &IPCRenderer::Invoke);
} }
static mate::Handle<IPCRenderer> Create(v8::Isolate* isolate) { static mate::Handle<IPCRenderer> Create(v8::Isolate* isolate) {
return mate::CreateHandle(isolate, new IPCRenderer(isolate)); return mate::CreateHandle(isolate, new IPCRenderer(isolate));
} }
void Send(mate::Arguments* args, void Send(bool internal,
bool internal,
const std::string& channel, const std::string& channel,
const base::ListValue& arguments) { const base::ListValue& arguments) {
electron_browser_ptr_->Message(internal, channel, arguments.Clone()); electron_browser_ptr_->get()->Message(internal, channel, arguments.Clone());
} }
v8::Local<v8::Promise> Invoke(mate::Arguments* args, v8::Local<v8::Promise> Invoke(mate::Arguments* args,
@ -71,33 +77,31 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
const base::Value& arguments) { const base::Value& arguments) {
atom::util::Promise p(args->isolate()); atom::util::Promise p(args->isolate());
auto handle = p.GetHandle(); auto handle = p.GetHandle();
electron_browser_ptr_->Invoke(
electron_browser_ptr_->get()->Invoke(
channel, arguments.Clone(), channel, arguments.Clone(),
base::BindOnce( base::BindOnce([](atom::util::Promise p,
[](atom::util::Promise p, base::Value value) { p.Resolve(value); }, base::Value result) { p.Resolve(result); },
std::move(p))); std::move(p)));
return handle; return handle;
} }
void SendTo(mate::Arguments* args, void SendTo(bool internal,
bool internal,
bool send_to_all, bool send_to_all,
int32_t web_contents_id, int32_t web_contents_id,
const std::string& channel, const std::string& channel,
const base::ListValue& arguments) { const base::ListValue& arguments) {
electron_browser_ptr_->MessageTo(internal, send_to_all, web_contents_id, electron_browser_ptr_->get()->MessageTo(
channel, arguments.Clone()); internal, send_to_all, web_contents_id, channel, arguments.Clone());
} }
void SendToHost(mate::Arguments* args, void SendToHost(const std::string& channel,
const std::string& channel,
const base::ListValue& arguments) { const base::ListValue& arguments) {
electron_browser_ptr_->MessageHost(channel, arguments.Clone()); electron_browser_ptr_->get()->MessageHost(channel, arguments.Clone());
} }
base::Value SendSync(mate::Arguments* args, base::Value SendSync(bool internal,
bool internal,
const std::string& channel, const std::string& channel,
const base::ListValue& arguments) { const base::ListValue& arguments) {
// We aren't using a true synchronous mojo call here. We're calling an // We aren't using a true synchronous mojo call here. We're calling an
@ -134,12 +138,12 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
// //
// However, in the calling process, we still need to block on the result, // However, in the calling process, we still need to block on the result,
// because the caller is expecting a result synchronously. So we do a bit // because the caller is expecting a result synchronously. So we do a bit
// of a trick: we pass the Mojo handle over to a new thread, send the // of a trick: we pass the Mojo handle over to a worker thread, send the
// asynchronous message from that thread, and then block on the result. // asynchronous message from that thread, and then block on the result.
// It's important that we pass the handle over to the new thread, because // It's important that we pass the handle over to the worker thread,
// that allows Mojo to process incoming messages (most importantly, the // because that allows Mojo to process incoming messages (most importantly,
// response to our request) on the new thread. If we didn't pass it to a // the response to our request) on that thread. If we didn't pass it to a
// new thread, and instead sent the call from the main thread, we would // worker thread, and instead sent the call from the main thread, we would
// never receive a response because Mojo wouldn't be able to run its // never receive a response because Mojo wouldn't be able to run its
// message handling code, because the main thread would be tied up blocking // message handling code, because the main thread would be tied up blocking
// on the WaitableEvent. // on the WaitableEvent.
@ -148,61 +152,43 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
base::Value result; base::Value result;
// A task is posted to a separate thread to execute the request so that // A task is posted to a worker thread to execute the request so that
// this thread may block on a waitable event. It is safe to pass raw // this thread may block on a waitable event. It is safe to pass raw
// pointers to |result| and |event| as this stack frame will survive until // pointers to |result| and |response_received_event| as this stack frame
// the request is complete. // will survive until the request is complete.
scoped_refptr<base::SingleThreadTaskRunner> task_runner =
base::CreateSingleThreadTaskRunnerWithTraits({});
base::WaitableEvent response_received_event; base::WaitableEvent response_received_event;
task_runner_->PostTask(
// We unbind the interface from this thread to pass it over to the worker
// thread temporarily. This requires that no callbacks be pending for this
// interface.
auto interface_info = electron_browser_sync_ptr_.PassInterface();
task_runner->PostTask(
FROM_HERE, base::BindOnce(&IPCRenderer::SendMessageSyncOnWorkerThread, FROM_HERE, base::BindOnce(&IPCRenderer::SendMessageSyncOnWorkerThread,
base::Unretained(&interface_info), base::Unretained(this),
base::Unretained(&response_received_event), base::Unretained(&response_received_event),
base::Unretained(&result), internal, channel, base::Unretained(&result), internal, channel,
base::Unretained(&arguments))); arguments.Clone()));
response_received_event.Wait(); response_received_event.Wait();
electron_browser_sync_ptr_.Bind(std::move(interface_info));
return result; return result;
} }
private: private:
static void SendMessageSyncOnWorkerThread( void SendMessageSyncOnWorkerThread(base::WaitableEvent* event,
atom::mojom::ElectronBrowserPtrInfo* interface_info, base::Value* result,
base::WaitableEvent* event, bool internal,
base::Value* result, const std::string& channel,
bool internal, base::Value arguments) {
const std::string& channel, electron_browser_ptr_->get()->MessageSync(
const base::ListValue* arguments) { internal, channel, std::move(arguments),
atom::mojom::ElectronBrowserPtr browser_ptr(std::move(*interface_info));
browser_ptr->MessageSync(
internal, channel, arguments->Clone(),
base::BindOnce(&IPCRenderer::ReturnSyncResponseToMainThread, base::BindOnce(&IPCRenderer::ReturnSyncResponseToMainThread,
std::move(browser_ptr), base::Unretained(interface_info),
base::Unretained(event), base::Unretained(result))); base::Unretained(event), base::Unretained(result)));
} }
static void ReturnSyncResponseToMainThread( static void ReturnSyncResponseToMainThread(base::WaitableEvent* event,
atom::mojom::ElectronBrowserPtr ptr, base::Value* result,
atom::mojom::ElectronBrowserPtrInfo* interface_info, base::Value response) {
base::WaitableEvent* event,
base::Value* result,
base::Value response) {
*result = std::move(response); *result = std::move(response);
*interface_info = ptr.PassInterface();
event->Signal(); event->Signal();
} }
atom::mojom::ElectronBrowserPtr electron_browser_ptr_; scoped_refptr<base::SequencedTaskRunner> task_runner_;
scoped_refptr<atom::mojom::ThreadSafeElectronBrowserPtr>
// We execute all synchronous calls on a separate mojo pipe, because electron_browser_ptr_;
// of the way that we block on the result of synchronous calls.
atom::mojom::ElectronBrowserPtr electron_browser_sync_ptr_;
}; };
void Initialize(v8::Local<v8::Object> exports, void Initialize(v8::Local<v8::Object> exports,

Просмотреть файл

@ -102,8 +102,88 @@ describe('ipc module', () => {
it('forbids multiple handlers', async () => { it('forbids multiple handlers', async () => {
ipcMain.handle('test', () => {}) ipcMain.handle('test', () => {})
expect(() => { ipcMain.handle('test', () => {}) }).to.throw(/second handler/) try {
ipcMain.removeHandler('test') expect(() => { ipcMain.handle('test', () => {}) }).to.throw(/second handler/)
} finally {
ipcMain.removeHandler('test')
}
})
})
describe('ordering', () => {
let w = (null as unknown as BrowserWindow);
before(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
await w.loadURL('about:blank')
})
after(async () => {
w.destroy()
})
it('between send and sendSync is consistent', async () => {
const received: number[] = []
ipcMain.on('test-async', (e, i) => { received.push(i) })
ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null })
const done = new Promise(resolve => ipcMain.once('done', () => { resolve() }))
try {
function rendererStressTest() {
const {ipcRenderer} = require('electron')
for (let i = 0; i < 1000; i++) {
switch ((Math.random() * 2) | 0) {
case 0:
ipcRenderer.send('test-async', i)
break;
case 1:
ipcRenderer.sendSync('test-sync', i)
break;
}
}
ipcRenderer.send('done')
}
w.webContents.executeJavaScript(`(${rendererStressTest})()`)
await done
} finally {
ipcMain.removeAllListeners('test-async')
ipcMain.removeAllListeners('test-sync')
}
expect(received).to.have.lengthOf(1000)
expect(received).to.deep.equal([...received].sort((a, b) => a - b))
})
it('between send, sendSync, and invoke is consistent', async () => {
const received: number[] = []
ipcMain.handle('test-invoke', (e, i) => { received.push(i) })
ipcMain.on('test-async', (e, i) => { received.push(i) })
ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null })
const done = new Promise(resolve => ipcMain.once('done', () => { resolve() }))
try {
function rendererStressTest() {
const {ipcRenderer} = require('electron')
for (let i = 0; i < 1000; i++) {
switch ((Math.random() * 3) | 0) {
case 0:
ipcRenderer.send('test-async', i)
break;
case 1:
ipcRenderer.sendSync('test-sync', i)
break;
case 2:
ipcRenderer.invoke('test-invoke', i)
break;
}
}
ipcRenderer.send('done')
}
w.webContents.executeJavaScript(`(${rendererStressTest})()`)
await done
} finally {
ipcMain.removeHandler('test-invoke')
ipcMain.removeAllListeners('test-async')
ipcMain.removeAllListeners('test-sync')
}
expect(received).to.have.lengthOf(1000)
expect(received).to.deep.equal([...received].sort((a, b) => a - b))
}) })
}) })
}) })