6f076f7433
Use a single synchronous IPC call to retrieve data required by early sandbox scripts. This has two purposes: - Optimize preload script initialization by: - Using one synchronous IPC call to retrieve preload script, webContentsId (more on that later), process.{platform,execPath,env} - Lazy loading as many modules as possible. - Fix #12316 for sandbox. @MarshallOfSound addressed the issue in #12342, but it was still present in sandbox mode. By loading webContentsId very early and skipping remote module at early startup, we fix it for sandbox.
471 lines
15 KiB
JavaScript
471 lines
15 KiB
JavaScript
'use strict'
|
|
|
|
const {Buffer} = require('buffer')
|
|
const electron = require('electron')
|
|
const {EventEmitter} = require('events')
|
|
const fs = require('fs')
|
|
const v8Util = process.atomBinding('v8_util')
|
|
|
|
const {ipcMain, isPromise, webContents} = electron
|
|
|
|
const objectsRegistry = require('./objects-registry')
|
|
|
|
const hasProp = {}.hasOwnProperty
|
|
|
|
// The internal properties of Function.
|
|
const FUNCTION_PROPERTIES = [
|
|
'length', 'name', 'arguments', 'caller', 'prototype'
|
|
]
|
|
|
|
// The remote functions in renderer processes.
|
|
// id => Function
|
|
let rendererFunctions = v8Util.createDoubleIDWeakMap()
|
|
|
|
// Return the description of object's members:
|
|
let getObjectMembers = function (object) {
|
|
let names = Object.getOwnPropertyNames(object)
|
|
// For Function, we should not override following properties even though they
|
|
// are "own" properties.
|
|
if (typeof object === 'function') {
|
|
names = names.filter((name) => {
|
|
return !FUNCTION_PROPERTIES.includes(name)
|
|
})
|
|
}
|
|
// Map properties to descriptors.
|
|
return names.map((name) => {
|
|
let descriptor = Object.getOwnPropertyDescriptor(object, name)
|
|
let member = {name, enumerable: descriptor.enumerable, writable: false}
|
|
if (descriptor.get === undefined && typeof object[name] === 'function') {
|
|
member.type = 'method'
|
|
} else {
|
|
if (descriptor.set || descriptor.writable) member.writable = true
|
|
member.type = 'get'
|
|
}
|
|
return member
|
|
})
|
|
}
|
|
|
|
// Return the description of object's prototype.
|
|
let getObjectPrototype = function (object) {
|
|
let proto = Object.getPrototypeOf(object)
|
|
if (proto === null || proto === Object.prototype) return null
|
|
return {
|
|
members: getObjectMembers(proto),
|
|
proto: getObjectPrototype(proto)
|
|
}
|
|
}
|
|
|
|
// Convert a real value into meta data.
|
|
let valueToMeta = function (sender, value, optimizeSimpleObject = false) {
|
|
// Determine the type of value.
|
|
const meta = { type: typeof value }
|
|
if (meta.type === 'object') {
|
|
// Recognize certain types of objects.
|
|
if (value === null) {
|
|
meta.type = 'value'
|
|
} else if (ArrayBuffer.isView(value)) {
|
|
meta.type = 'buffer'
|
|
} else if (Array.isArray(value)) {
|
|
meta.type = 'array'
|
|
} else if (value instanceof Error) {
|
|
meta.type = 'error'
|
|
} else if (value instanceof Date) {
|
|
meta.type = 'date'
|
|
} else if (isPromise(value)) {
|
|
meta.type = 'promise'
|
|
} else if (hasProp.call(value, 'callee') && value.length != null) {
|
|
// Treat the arguments object as array.
|
|
meta.type = 'array'
|
|
} else if (optimizeSimpleObject && v8Util.getHiddenValue(value, 'simple')) {
|
|
// Treat simple objects as value.
|
|
meta.type = 'value'
|
|
}
|
|
}
|
|
|
|
// Fill the meta object according to value's type.
|
|
if (meta.type === 'array') {
|
|
meta.members = value.map((el) => valueToMeta(sender, el, optimizeSimpleObject))
|
|
} else if (meta.type === 'object' || meta.type === 'function') {
|
|
meta.name = value.constructor ? value.constructor.name : ''
|
|
|
|
// Reference the original value if it's an object, because when it's
|
|
// passed to renderer we would assume the renderer keeps a reference of
|
|
// it.
|
|
meta.id = objectsRegistry.add(sender, value)
|
|
meta.members = getObjectMembers(value)
|
|
meta.proto = getObjectPrototype(value)
|
|
} else if (meta.type === 'buffer') {
|
|
meta.value = Buffer.from(value)
|
|
} else if (meta.type === 'promise') {
|
|
// Add default handler to prevent unhandled rejections in main process
|
|
// Instead they should appear in the renderer process
|
|
value.then(function () {}, function () {})
|
|
|
|
meta.then = valueToMeta(sender, function (onFulfilled, onRejected) {
|
|
value.then(onFulfilled, onRejected)
|
|
})
|
|
} else if (meta.type === 'error') {
|
|
meta.members = plainObjectToMeta(value)
|
|
|
|
// Error.name is not part of own properties.
|
|
meta.members.push({
|
|
name: 'name',
|
|
value: value.name
|
|
})
|
|
} else if (meta.type === 'date') {
|
|
meta.value = value.getTime()
|
|
} else {
|
|
meta.type = 'value'
|
|
meta.value = value
|
|
}
|
|
return meta
|
|
}
|
|
|
|
// Convert object to meta by value.
|
|
const plainObjectToMeta = function (obj) {
|
|
return Object.getOwnPropertyNames(obj).map(function (name) {
|
|
return {
|
|
name: name,
|
|
value: obj[name]
|
|
}
|
|
})
|
|
}
|
|
|
|
// Convert Error into meta data.
|
|
const exceptionToMeta = function (sender, error) {
|
|
return {
|
|
type: 'exception',
|
|
message: error.message,
|
|
stack: error.stack || error,
|
|
cause: valueToMeta(sender, error.cause)
|
|
}
|
|
}
|
|
|
|
const throwRPCError = function (message) {
|
|
const error = new Error(message)
|
|
error.code = 'EBADRPC'
|
|
error.errno = -72
|
|
throw error
|
|
}
|
|
|
|
const removeRemoteListenersAndLogWarning = (sender, meta, callIntoRenderer) => {
|
|
let message = `Attempting to call a function in a renderer window that has been closed or released.` +
|
|
`\nFunction provided here: ${meta.location}`
|
|
|
|
if (sender instanceof EventEmitter) {
|
|
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) {
|
|
let i, len, member, ref, returnValue
|
|
switch (meta.type) {
|
|
case 'value':
|
|
return meta.value
|
|
case 'remote-object':
|
|
return objectsRegistry.get(meta.id)
|
|
case 'array':
|
|
return unwrapArgs(sender, meta.value)
|
|
case 'buffer':
|
|
return Buffer.from(meta.value)
|
|
case 'date':
|
|
return new Date(meta.value)
|
|
case 'promise':
|
|
return Promise.resolve({
|
|
then: metaToValue(meta.then)
|
|
})
|
|
case 'object': {
|
|
let ret = {}
|
|
Object.defineProperty(ret.constructor, 'name', { value: meta.name })
|
|
|
|
ref = meta.members
|
|
for (i = 0, len = ref.length; i < len; i++) {
|
|
member = ref[i]
|
|
ret[member.name] = metaToValue(member.value)
|
|
}
|
|
return ret
|
|
}
|
|
case 'function-with-return-value':
|
|
returnValue = metaToValue(meta.value)
|
|
return function () {
|
|
return returnValue
|
|
}
|
|
case 'function': {
|
|
// Merge webContentsId and meta.id, since meta.id can be the same in
|
|
// different webContents.
|
|
const webContentsId = sender.getId()
|
|
const objectId = [webContentsId, meta.id]
|
|
|
|
// Cache the callbacks in renderer.
|
|
if (rendererFunctions.has(objectId)) {
|
|
return rendererFunctions.get(objectId)
|
|
}
|
|
|
|
let callIntoRenderer = function (...args) {
|
|
if (!sender.isDestroyed() && webContentsId === sender.getId()) {
|
|
sender.send('ELECTRON_RENDERER_CALLBACK', meta.id, valueToMeta(sender, args))
|
|
} else {
|
|
removeRemoteListenersAndLogWarning(this, meta, callIntoRenderer)
|
|
}
|
|
}
|
|
Object.defineProperty(callIntoRenderer, 'length', { value: meta.length })
|
|
|
|
v8Util.setRemoteCallbackFreer(callIntoRenderer, meta.id, sender)
|
|
rendererFunctions.set(objectId, callIntoRenderer)
|
|
return callIntoRenderer
|
|
}
|
|
default:
|
|
throw new TypeError(`Unknown type: ${meta.type}`)
|
|
}
|
|
}
|
|
return args.map(metaToValue)
|
|
}
|
|
|
|
// Call a function and send reply asynchronously if it's a an asynchronous
|
|
// style function and the caller didn't pass a callback.
|
|
const callFunction = function (event, func, caller, args) {
|
|
let err, funcMarkedAsync, funcName, funcPassedCallback, ref, ret
|
|
funcMarkedAsync = v8Util.getHiddenValue(func, 'asynchronous')
|
|
funcPassedCallback = typeof args[args.length - 1] === 'function'
|
|
try {
|
|
if (funcMarkedAsync && !funcPassedCallback) {
|
|
args.push(function (ret) {
|
|
event.returnValue = valueToMeta(event.sender, ret, true)
|
|
})
|
|
func.apply(caller, args)
|
|
} else {
|
|
ret = func.apply(caller, args)
|
|
event.returnValue = valueToMeta(event.sender, ret, true)
|
|
}
|
|
} catch (error) {
|
|
// Catch functions thrown further down in function invocation and wrap
|
|
// them with the function name so it's easier to trace things like
|
|
// `Error processing argument -1.`
|
|
funcName = ((ref = func.name) != null) ? ref : 'anonymous'
|
|
err = new Error(`Could not call remote function '${funcName}'. Check that the function signature is correct. Underlying error: ${error.message}`)
|
|
err.cause = error
|
|
throw err
|
|
}
|
|
}
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_REQUIRE', function (event, module) {
|
|
try {
|
|
event.returnValue = valueToMeta(event.sender, process.mainModule.require(module))
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_GET_BUILTIN', function (event, module) {
|
|
try {
|
|
event.returnValue = valueToMeta(event.sender, electron[module])
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_GLOBAL', function (event, name) {
|
|
try {
|
|
event.returnValue = valueToMeta(event.sender, global[name])
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_CURRENT_WINDOW', function (event) {
|
|
try {
|
|
event.returnValue = valueToMeta(event.sender, event.sender.getOwnerBrowserWindow())
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_CURRENT_WEB_CONTENTS', function (event) {
|
|
event.returnValue = valueToMeta(event.sender, event.sender)
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_CONSTRUCTOR', function (event, id, args) {
|
|
try {
|
|
args = unwrapArgs(event.sender, args)
|
|
let constructor = objectsRegistry.get(id)
|
|
|
|
if (constructor == null) {
|
|
throwRPCError(`Cannot call constructor on missing remote object ${id}`)
|
|
}
|
|
|
|
// Call new with array of arguments.
|
|
// http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
|
|
let obj = new (Function.prototype.bind.apply(constructor, [null].concat(args)))()
|
|
event.returnValue = valueToMeta(event.sender, obj)
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_FUNCTION_CALL', function (event, id, args) {
|
|
try {
|
|
args = unwrapArgs(event.sender, args)
|
|
let func = objectsRegistry.get(id)
|
|
|
|
if (func == null) {
|
|
throwRPCError(`Cannot call function on missing remote object ${id}`)
|
|
}
|
|
|
|
callFunction(event, func, global, args)
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_MEMBER_CONSTRUCTOR', function (event, id, method, args) {
|
|
try {
|
|
args = unwrapArgs(event.sender, args)
|
|
let object = objectsRegistry.get(id)
|
|
|
|
if (object == null) {
|
|
throwRPCError(`Cannot call constructor '${method}' on missing remote object ${id}`)
|
|
}
|
|
|
|
// Call new with array of arguments.
|
|
let constructor = object[method]
|
|
let obj = new (Function.prototype.bind.apply(constructor, [null].concat(args)))()
|
|
event.returnValue = valueToMeta(event.sender, obj)
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_MEMBER_CALL', function (event, id, method, args) {
|
|
try {
|
|
args = unwrapArgs(event.sender, args)
|
|
let obj = objectsRegistry.get(id)
|
|
|
|
if (obj == null) {
|
|
throwRPCError(`Cannot call function '${method}' on missing remote object ${id}`)
|
|
}
|
|
|
|
callFunction(event, obj[method], obj, args)
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_MEMBER_SET', function (event, id, name, args) {
|
|
try {
|
|
args = unwrapArgs(event.sender, args)
|
|
let obj = objectsRegistry.get(id)
|
|
|
|
if (obj == null) {
|
|
throwRPCError(`Cannot set property '${name}' on missing remote object ${id}`)
|
|
}
|
|
|
|
obj[name] = args[0]
|
|
event.returnValue = null
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_MEMBER_GET', function (event, id, name) {
|
|
try {
|
|
let obj = objectsRegistry.get(id)
|
|
|
|
if (obj == null) {
|
|
throwRPCError(`Cannot get property '${name}' on missing remote object ${id}`)
|
|
}
|
|
|
|
event.returnValue = valueToMeta(event.sender, obj[name])
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_DEREFERENCE', function (event, id) {
|
|
objectsRegistry.remove(event.sender.getId(), id)
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_CONTEXT_RELEASE', (e, contextId) => {
|
|
objectsRegistry.clear(contextId)
|
|
e.returnValue = null
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_GUEST_WEB_CONTENTS', function (event, guestInstanceId) {
|
|
try {
|
|
let guestViewManager = require('./guest-view-manager')
|
|
event.returnValue = valueToMeta(event.sender, guestViewManager.getGuest(guestInstanceId))
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function (event, requestId, guestInstanceId, method, ...args) {
|
|
try {
|
|
let guestViewManager = require('./guest-view-manager')
|
|
let guest = guestViewManager.getGuest(guestInstanceId)
|
|
if (requestId) {
|
|
const responseCallback = function (result) {
|
|
event.sender.send(`ELECTRON_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, result)
|
|
}
|
|
args.push(responseCallback)
|
|
}
|
|
guest[method].apply(guest, args)
|
|
} catch (error) {
|
|
event.returnValue = exceptionToMeta(event.sender, error)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_SEND_TO', function (event, sendToAll, webContentsId, channel, ...args) {
|
|
let contents = webContents.fromId(webContentsId)
|
|
if (!contents) {
|
|
console.error(`Sending message to WebContents with unknown ID ${webContentsId}`)
|
|
return
|
|
}
|
|
|
|
if (sendToAll) {
|
|
contents.sendToAll(channel, ...args)
|
|
} else {
|
|
contents.send(channel, ...args)
|
|
}
|
|
})
|
|
|
|
// Implements window.close()
|
|
ipcMain.on('ELECTRON_BROWSER_WINDOW_CLOSE', function (event) {
|
|
const window = event.sender.getOwnerBrowserWindow()
|
|
if (window) {
|
|
window.close()
|
|
}
|
|
event.returnValue = null
|
|
})
|
|
|
|
ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event, preloadPath) {
|
|
let preloadSrc = null
|
|
let preloadError = null
|
|
if (preloadPath) {
|
|
try {
|
|
preloadSrc = fs.readFileSync(preloadPath).toString()
|
|
} catch (err) {
|
|
preloadError = {stack: err ? err.stack : (new Error(`Failed to load "${preloadPath}"`)).stack}
|
|
}
|
|
}
|
|
event.returnValue = {
|
|
preloadSrc: preloadSrc,
|
|
preloadError: preloadError,
|
|
webContentsId: event.sender.getId(),
|
|
platform: process.platform,
|
|
execPath: process.execPath,
|
|
env: process.env
|
|
}
|
|
})
|