diff --git a/lib/browser/rpc-server.js b/lib/browser/rpc-server.js index b12b518888a2..1dac51645926 100644 --- a/lib/browser/rpc-server.js +++ b/lib/browser/rpc-server.js @@ -3,6 +3,8 @@ const {Buffer} = require('buffer') const electron = require('electron') const v8Util = process.atomBinding('v8_util') +const {WebContents} = process.atomBinding('web_contents') + const {ipcMain, isPromise, webContents} = electron const fs = require('fs') @@ -146,6 +148,27 @@ const throwRPCError = function (message) { throw error } +const removeRemoteListenersAndLogWarning = (meta, args, callIntoRenderer) => { + let message = `Attempting to call a function in a renderer window that has been closed or released.` + + `\nFunction provided here: ${meta.location}` + + if (args.length > 0 && (args[0].sender instanceof WebContents)) { + const {sender} = args[0] + const remoteEvents = sender.eventNames().filter((eventName) => { + return sender.listeners(eventName).includes(callIntoRenderer) + }) + + if (remoteEvents.length > 0) { + message += `\nRemote event names: ${remoteEvents.join(', ')}` + remoteEvents.forEach((eventName) => { + sender.removeListener(eventName, callIntoRenderer) + }) + } + } + + console.warn(message) +} + // Convert array of meta data from renderer into array of real values. const unwrapArgs = function (sender, args) { const metaToValue = function (meta) { @@ -196,7 +219,7 @@ const unwrapArgs = function (sender, args) { if (!sender.isDestroyed() && webContentsId === sender.getId()) { sender.send('ELECTRON_RENDERER_CALLBACK', meta.id, valueToMeta(sender, args)) } else { - throw new Error(`Attempting to call a function in a renderer window that has been closed or released. Function provided here: ${meta.location}.`) + removeRemoteListenersAndLogWarning(meta, args, callIntoRenderer) } } diff --git a/spec/api-ipc-spec.js b/spec/api-ipc-spec.js index 76310f47812a..b1ca29c6a158 100644 --- a/spec/api-ipc-spec.js +++ b/spec/api-ipc-spec.js @@ -494,6 +494,30 @@ describe('ipc module', function () { w.removeListener('test', listener) assert.equal(w.listenerCount('test'), 0) }) + + it('detaches listeners subscribed to destroyed renderers, and shows a warning', (done) => { + w = new BrowserWindow({ + show: false + }) + w.webContents.once('did-finish-load', () => { + w.webContents.once('did-finish-load', () => { + const expectedMessage = [ + 'Attempting to call a function in a renderer window that has been closed or released.', + 'Function provided here: remote-event-handler.html:11:33', + 'Remote event names: remote-handler, other-remote-handler' + ].join('\n') + const results = ipcRenderer.sendSync('try-emit-web-contents-event', w.webContents.id, 'remote-handler') + assert.deepEqual(results, { + warningMessage: expectedMessage, + listenerCountBefore: 2, + listenerCountAfter: 1 + }) + done() + }) + w.webContents.reload() + }) + w.loadURL('file://' + path.join(fixtures, 'api', 'remote-event-handler.html')) + }) }) it('throws an error when removing all the listeners', () => { diff --git a/spec/fixtures/api/remote-event-handler.html b/spec/fixtures/api/remote-event-handler.html new file mode 100644 index 000000000000..30c3cfb36ad7 --- /dev/null +++ b/spec/fixtures/api/remote-event-handler.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/spec/static/main.js b/spec/static/main.js index 9612ed25cf05..eaa5dd0f3059 100644 --- a/spec/static/main.js +++ b/spec/static/main.js @@ -249,3 +249,27 @@ ipcMain.on('create-window-with-options-cycle', (event) => { ipcMain.on('prevent-next-new-window', (event, id) => { webContents.fromId(id).once('new-window', event => event.preventDefault()) }) + +ipcMain.on('try-emit-web-contents-event', (event, id, eventName) => { + const consoleWarn = console.warn + let warningMessage = null + const contents = webContents.fromId(id) + const listenerCountBefore = contents.listenerCount(eventName) + + try { + console.warn = (message) => { + warningMessage = message + } + contents.emit(eventName, {sender: contents}) + } finally { + console.warn = consoleWarn + } + + const listenerCountAfter = contents.listenerCount(eventName) + + event.returnValue = { + warningMessage, + listenerCountBefore, + listenerCountAfter + } +})