Browse Source

feat: add `preload-error` event to `webContents` (#16411)

Milan Burda 6 years ago
parent
commit
7d4a1223fd

+ 10 - 0
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:

+ 6 - 1
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))
+})

+ 6 - 2
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))
   }
 }
 

+ 18 - 4
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

+ 69 - 0
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()

+ 1 - 0
spec/fixtures/module/preload-error-exception.js

@@ -0,0 +1 @@
+throw new Error('Hello World!')

+ 2 - 0
spec/fixtures/module/preload-error-syntax.js

@@ -0,0 +1,2 @@
+// eslint-disable-next-line
+foobar