From 7d4a1223fd894eca8a54ca20f086b47ea0b4390a Mon Sep 17 00:00:00 2001 From: Milan Burda Date: Fri, 18 Jan 2019 12:03:43 +0100 Subject: [PATCH] feat: add `preload-error` event to `webContents` (#16411) --- docs/api/web-contents.md | 10 +++ lib/browser/rpc-server.js | 7 +- lib/renderer/init.js | 8 ++- lib/sandboxed_renderer/init.js | 22 ++++-- spec/api-web-contents-spec.js | 69 +++++++++++++++++++ .../module/preload-error-exception.js | 1 + spec/fixtures/module/preload-error-syntax.js | 2 + 7 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 spec/fixtures/module/preload-error-exception.js create mode 100644 spec/fixtures/module/preload-error-syntax.js diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index aa73b28a0a4e3..846cc39ff27f5 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -663,6 +663,16 @@ Returns: Emitted when the associated window logs a console message. Will not be emitted for windows with *offscreen rendering* enabled. +#### Event: 'preload-error' + +Returns: + +* `event` Event +* `preloadPath` String +* `error` Error + +Emitted when the preload script `preloadPath` throws an unhandled exception `error`. + #### Event: 'desktop-capturer-get-sources' Returns: diff --git a/lib/browser/rpc-server.js b/lib/browser/rpc-server.js index 1320f470df3d5..bae2d8a153f2a 100644 --- a/lib/browser/rpc-server.js +++ b/lib/browser/rpc-server.js @@ -542,10 +542,11 @@ ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event) { try { preloadSrc = fs.readFileSync(preloadPath).toString() } catch (err) { - preloadError = { stack: err ? err.stack : (new Error(`Failed to load "${preloadPath}"`)).stack } + preloadError = errorUtils.serialize(err) } } event.returnValue = { + preloadPath, preloadSrc, preloadError, isRemoteModuleEnabled: event.sender._isRemoteModuleEnabled(), @@ -560,3 +561,7 @@ ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event) { } } }) + +ipcMain.on('ELECTRON_BROWSER_PRELOAD_ERROR', function (event, preloadPath, error) { + event.sender.emit('preload-error', event, preloadPath, errorUtils.deserialize(error)) +}) diff --git a/lib/renderer/init.js b/lib/renderer/init.js index 9cbe1c3d999c3..a514fddf57be5 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -147,13 +147,17 @@ if (nodeIntegration) { }) } +const errorUtils = require('@electron/internal/common/error-utils') + // Load the preload scripts. for (const preloadScript of preloadScripts) { try { require(preloadScript) } catch (error) { - console.error('Unable to load preload script: ' + preloadScript) - console.error(error.stack || error.message) + console.error(`Unable to load preload script: ${preloadScript}`) + console.error(`${error}`) + + ipcRenderer.send('ELECTRON_BROWSER_PRELOAD_ERROR', preloadScript, errorUtils.serialize(error)) } } diff --git a/lib/sandboxed_renderer/init.js b/lib/sandboxed_renderer/init.js index e5a99b516cbbc..0427816830069 100644 --- a/lib/sandboxed_renderer/init.js +++ b/lib/sandboxed_renderer/init.js @@ -29,7 +29,7 @@ Object.setPrototypeOf(process, EventEmitter.prototype) const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') const { - preloadSrc, preloadError, isRemoteModuleEnabled, isWebViewTagEnabled, process: processProps + preloadPath, preloadSrc, preloadError, isRemoteModuleEnabled, isWebViewTagEnabled, process: processProps } = ipcRenderer.sendSync('ELECTRON_BROWSER_SANDBOX_LOAD') process.isRemoteModuleEnabled = isRemoteModuleEnabled @@ -132,6 +132,8 @@ if (!process.guestInstanceId && isWebViewTagEnabled) { setupWebView(v8Util, webViewImpl) } +const errorUtils = require('@electron/internal/common/error-utils') + // Wrap the script into a function executed in global scope. It won't have // access to the current scope, so we'll expose a few objects as arguments: // @@ -151,7 +153,7 @@ if (!process.guestInstanceId && isWebViewTagEnabled) { // and any `require('electron')` calls in `preload.js` will work as expected // since browserify won't try to include `electron` in the bundle, falling back // to the `preloadRequire` function above. -if (preloadSrc) { +function runPreloadScript (preloadSrc) { const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate) { ${preloadSrc} })` @@ -159,9 +161,21 @@ if (preloadSrc) { // eval in window scope const preloadFn = binding.createPreloadScript(preloadWrapperSrc) const { setImmediate, clearImmediate } = require('timers') + preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate) -} else if (preloadError) { - console.error(preloadError.stack) +} + +try { + if (preloadSrc) { + runPreloadScript(preloadSrc) + } else if (preloadError) { + throw errorUtils.deserialize(preloadError) + } +} catch (error) { + console.error(`Unable to load preload script: ${preloadPath}`) + console.error(`${error}`) + + ipcRenderer.send('ELECTRON_BROWSER_PRELOAD_ERROR', preloadPath, errorUtils.serialize(error)) } // Warn about security issues diff --git a/spec/api-web-contents-spec.js b/spec/api-web-contents-spec.js index 79866f251b052..0a3bc85dd9954 100644 --- a/spec/api-web-contents-spec.js +++ b/spec/api-web-contents-spec.js @@ -1050,6 +1050,75 @@ describe('webContents module', () => { }) }) + describe('preload-error event', () => { + const generateSpecs = (description, sandbox) => { + describe(description, () => { + it('is triggered when unhandled exception is thrown', async () => { + const preload = path.join(fixtures, 'module', 'preload-error-exception.js') + + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox, + preload + } + }) + + const promise = emittedOnce(w.webContents, 'preload-error') + w.loadURL('about:blank') + + const [, preloadPath, error] = await promise + expect(preloadPath).to.equal(preload) + expect(error.message).to.equal('Hello World!') + }) + + it('is triggered on syntax errors', async () => { + const preload = path.join(fixtures, 'module', 'preload-error-syntax.js') + + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox, + preload + } + }) + + const promise = emittedOnce(w.webContents, 'preload-error') + w.loadURL('about:blank') + + const [, preloadPath, error] = await promise + expect(preloadPath).to.equal(preload) + expect(error.message).to.equal('foobar is not defined') + }) + + it('is triggered when preload script loading fails', async () => { + const preload = path.join(fixtures, 'module', 'preload-invalid.js') + + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox, + preload + } + }) + + const promise = emittedOnce(w.webContents, 'preload-error') + w.loadURL('about:blank') + + const [, preloadPath, error] = await promise + expect(preloadPath).to.equal(preload) + expect(error.message).to.contain('preload-invalid.js') + }) + }) + } + + generateSpecs('without sandbox', false) + generateSpecs('with sandbox', true) + }) + describe('takeHeapSnapshot()', () => { it('works with sandboxed renderers', async () => { w.destroy() diff --git a/spec/fixtures/module/preload-error-exception.js b/spec/fixtures/module/preload-error-exception.js new file mode 100644 index 0000000000000..710907d35a33a --- /dev/null +++ b/spec/fixtures/module/preload-error-exception.js @@ -0,0 +1 @@ +throw new Error('Hello World!') diff --git a/spec/fixtures/module/preload-error-syntax.js b/spec/fixtures/module/preload-error-syntax.js new file mode 100644 index 0000000000000..bf9c543fb191a --- /dev/null +++ b/spec/fixtures/module/preload-error-syntax.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line +foobar