electron/lib/renderer/api/remote.js
Thiago de Arruda 6b5bd3b6ce Fix how rpc-server releases references after page reload
In addition to listening for "render-view-deleted", listen for
"ELECTRON_BROWSER_CONTEXT_RELEASE" synchronous message, which is sent by the
remote module when the page is about to be navigated.

This is required to allow child windows running in the same renderer to
correctly manage remote object references, since `render-view-deleted` is only
called when the renderer exits.

Close #9387
2017-05-16 09:05:52 -03:00

372 lines
11 KiB
JavaScript

'use strict'
// Note: Don't use destructuring assignment for `Buffer`, or we'll hit a
// browserify bug that makes the statement invalid, throwing an error in
// sandboxed renderer.
const Buffer = require('buffer').Buffer
const v8Util = process.atomBinding('v8_util')
const {ipcRenderer, isPromise, CallbacksRegistry} = require('electron')
const resolvePromise = Promise.resolve.bind(Promise)
const callbacksRegistry = new CallbacksRegistry()
const remoteObjectCache = v8Util.createIDWeakMap()
// Convert the arguments object into an array of meta data.
const wrapArgs = function (args, visited) {
if (visited == null) {
visited = new Set()
}
const valueToMeta = function (value) {
// Check for circular reference.
if (visited.has(value)) {
return {
type: 'value',
value: null
}
}
if (Array.isArray(value)) {
visited.add(value)
let meta = {
type: 'array',
value: wrapArgs(value, visited)
}
visited.delete(value)
return meta
} else if (ArrayBuffer.isView(value)) {
return {
type: 'buffer',
value: Buffer.from(value)
}
} else if (value instanceof Date) {
return {
type: 'date',
value: value.getTime()
}
} else if ((value != null) && typeof value === 'object') {
if (isPromise(value)) {
return {
type: 'promise',
then: valueToMeta(function (onFulfilled, onRejected) {
value.then(onFulfilled, onRejected)
})
}
} else if (v8Util.getHiddenValue(value, 'atomId')) {
return {
type: 'remote-object',
id: v8Util.getHiddenValue(value, 'atomId')
}
}
let meta = {
type: 'object',
name: value.constructor != null ? value.constructor.name : '',
members: []
}
visited.add(value)
for (let prop in value) {
meta.members.push({
name: prop,
value: valueToMeta(value[prop])
})
}
visited.delete(value)
return meta
} else if (typeof value === 'function' && v8Util.getHiddenValue(value, 'returnValue')) {
return {
type: 'function-with-return-value',
value: valueToMeta(value())
}
} else if (typeof value === 'function') {
return {
type: 'function',
id: callbacksRegistry.add(value),
location: v8Util.getHiddenValue(value, 'location'),
length: value.length
}
} else {
return {
type: 'value',
value: value
}
}
}
return args.map(valueToMeta)
}
// Populate object's members from descriptors.
// The |ref| will be kept referenced by |members|.
// This matches |getObjectMemebers| in rpc-server.
const setObjectMembers = function (ref, object, metaId, members) {
if (!Array.isArray(members)) return
for (let member of members) {
if (object.hasOwnProperty(member.name)) continue
let descriptor = { enumerable: member.enumerable }
if (member.type === 'method') {
const remoteMemberFunction = function (...args) {
if (this && this.constructor === remoteMemberFunction) {
// Constructor call.
let ret = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_CONSTRUCTOR', metaId, member.name, wrapArgs(args))
return metaToValue(ret)
} else {
// Call member function.
let ret = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_CALL', metaId, member.name, wrapArgs(args))
return metaToValue(ret)
}
}
let descriptorFunction = proxyFunctionProperties(remoteMemberFunction, metaId, member.name)
descriptor.get = function () {
descriptorFunction.ref = ref // The member should reference its object.
return descriptorFunction
}
// Enable monkey-patch the method
descriptor.set = function (value) {
descriptorFunction = value
return value
}
descriptor.configurable = true
} else if (member.type === 'get') {
descriptor.get = function () {
return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_GET', metaId, member.name))
}
// Only set setter when it is writable.
if (member.writable) {
descriptor.set = function (value) {
const args = wrapArgs([value])
const meta = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_SET', metaId, member.name, args)
// Meta will be non-null when a setter error occurred so parse it
// to a value so it gets re-thrown.
if (meta != null) {
metaToValue(meta)
}
return value
}
}
}
Object.defineProperty(object, member.name, descriptor)
}
}
// Populate object's prototype from descriptor.
// This matches |getObjectPrototype| in rpc-server.
const setObjectPrototype = function (ref, object, metaId, descriptor) {
if (descriptor === null) return
let proto = {}
setObjectMembers(ref, proto, metaId, descriptor.members)
setObjectPrototype(ref, proto, metaId, descriptor.proto)
Object.setPrototypeOf(object, proto)
}
// Wrap function in Proxy for accessing remote properties
const proxyFunctionProperties = function (remoteMemberFunction, metaId, name) {
let loaded = false
// Lazily load function properties
const loadRemoteProperties = () => {
if (loaded) return
loaded = true
const meta = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_GET', metaId, name)
setObjectMembers(remoteMemberFunction, remoteMemberFunction, meta.id, meta.members)
}
return new Proxy(remoteMemberFunction, {
set: (target, property, value, receiver) => {
if (property !== 'ref') loadRemoteProperties()
target[property] = value
return true
},
get: (target, property, receiver) => {
if (!target.hasOwnProperty(property)) loadRemoteProperties()
const value = target[property]
// Bind toString to target if it is a function to avoid
// Function.prototype.toString is not generic errors
if (property === 'toString' && typeof value === 'function') {
return value.bind(target)
}
return value
},
ownKeys: (target) => {
loadRemoteProperties()
return Object.getOwnPropertyNames(target)
},
getOwnPropertyDescriptor: (target, property) => {
let descriptor = Object.getOwnPropertyDescriptor(target, property)
if (descriptor != null) return descriptor
loadRemoteProperties()
return Object.getOwnPropertyDescriptor(target, property)
}
})
}
// Convert meta data from browser into real value.
const metaToValue = function (meta) {
var el, i, len, ref1, results, ret
switch (meta.type) {
case 'value':
return meta.value
case 'array':
ref1 = meta.members
results = []
for (i = 0, len = ref1.length; i < len; i++) {
el = ref1[i]
results.push(metaToValue(el))
}
return results
case 'buffer':
return Buffer.from(meta.value)
case 'promise':
return resolvePromise({then: metaToValue(meta.then)})
case 'error':
return metaToPlainObject(meta)
case 'date':
return new Date(meta.value)
case 'exception':
throw new Error(meta.message + '\n' + meta.stack)
default:
if (remoteObjectCache.has(meta.id)) return remoteObjectCache.get(meta.id)
if (meta.type === 'function') {
// A shadow class to represent the remote function object.
let remoteFunction = function (...args) {
if (this && this.constructor === remoteFunction) {
// Constructor call.
let obj = ipcRenderer.sendSync('ELECTRON_BROWSER_CONSTRUCTOR', meta.id, wrapArgs(args))
// Returning object in constructor will replace constructed object
// with the returned object.
// http://stackoverflow.com/questions/1978049/what-values-can-a-constructor-return-to-avoid-returning-this
return metaToValue(obj)
} else {
// Function call.
let obj = ipcRenderer.sendSync('ELECTRON_BROWSER_FUNCTION_CALL', meta.id, wrapArgs(args))
return metaToValue(obj)
}
}
ret = remoteFunction
} else {
ret = {}
}
// Populate delegate members.
setObjectMembers(ret, ret, meta.id, meta.members)
// Populate delegate prototype.
setObjectPrototype(ret, ret, meta.id, meta.proto)
// Set constructor.name to object's name.
Object.defineProperty(ret.constructor, 'name', { value: meta.name })
// Track delegate object's life time, and tell the browser to clean up
// when the object is GCed.
v8Util.setRemoteObjectFreer(ret, meta.id)
// Remember object's id.
v8Util.setHiddenValue(ret, 'atomId', meta.id)
remoteObjectCache.set(meta.id, ret)
return ret
}
}
// Construct a plain object from the meta.
const metaToPlainObject = function (meta) {
var i, len, obj, ref1
obj = (function () {
switch (meta.type) {
case 'error':
return new Error()
default:
return {}
}
})()
ref1 = meta.members
for (i = 0, len = ref1.length; i < len; i++) {
let {name, value} = ref1[i]
obj[name] = value
}
return obj
}
// Browser calls a callback in renderer.
ipcRenderer.on('ELECTRON_RENDERER_CALLBACK', function (event, id, args) {
callbacksRegistry.apply(id, metaToValue(args))
})
// A callback in browser is released.
ipcRenderer.on('ELECTRON_RENDERER_RELEASE_CALLBACK', function (event, id) {
callbacksRegistry.remove(id)
})
process.on('exit', () => {
ipcRenderer.sendSync('ELECTRON_BROWSER_CONTEXT_RELEASE')
})
// Get remote module.
exports.require = function (module) {
return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_REQUIRE', module))
}
// Alias to remote.require('electron').xxx.
exports.getBuiltin = function (module) {
return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_GET_BUILTIN', module))
}
// Get current BrowserWindow.
exports.getCurrentWindow = function () {
return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_CURRENT_WINDOW'))
}
// Get current WebContents object.
exports.getCurrentWebContents = function () {
return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_CURRENT_WEB_CONTENTS'))
}
// Get a global object in browser.
exports.getGlobal = function (name) {
return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_GLOBAL', name))
}
// Get the process object in browser.
exports.__defineGetter__('process', function () {
return exports.getGlobal('process')
})
// Create a funtion that will return the specifed value when called in browser.
exports.createFunctionWithReturnValue = function (returnValue) {
const func = function () {
return returnValue
}
v8Util.setHiddenValue(func, 'returnValue', true)
return func
}
// Get the guest WebContents from guestInstanceId.
exports.getGuestWebContents = function (guestInstanceId) {
const meta = ipcRenderer.sendSync('ELECTRON_BROWSER_GUEST_WEB_CONTENTS', guestInstanceId)
return metaToValue(meta)
}
const addBuiltinProperty = (name) => {
Object.defineProperty(exports, name, {
get: function () {
return exports.getBuiltin(name)
}
})
}
const browserModules =
require('../../common/api/module-list').concat(
require('../../browser/api/module-list'))
// And add a helper receiver for each one.
browserModules
.filter((m) => !m.private)
.map((m) => m.name)
.forEach(addBuiltinProperty)