electron/lib/renderer/api/remote.js
Samuel Attard 78411db4b5
fix: maintain a ref count for objects sent over remote (#17464)
* spec: clean up after a failed window count assertion

Previously when this assertion failed all tests that ran after the
failed assertion also failed.  This ensure that the assertion fails for
the test that actually caused the issue but cleans up the left-over
windows so that future tests do not fail.

* fix: maintain a ref count for objects sent over remote

Previously there was a race condition where a GC could occur in the
renderer process between the main process sending a meta.id and the
renderer pulling the proxy out its weakmap to stop it being GC'ed.

This fixes that race condition by maintaining a "sent" ref count in the
object registry and a "received" ref count in the object cache on the
renderer side.  The deref request now sends the number of refs the
renderer thinks it owns, if the number does not match the value in the
object registry it is assumed that there is an IPC message containing a
new reference in flight and this race condition was hit.

The browser side ref count is then reduced and we wait for the new deref
message.  This guaruntees that an object will only be removed from the
registry if every reference we sent has been guarunteed to be unreffed.
2019-04-16 16:08:11 -04:00

359 lines
11 KiB
JavaScript

'use strict'
const v8Util = process.electronBinding('v8_util')
const { isPromise } = require('electron')
const resolvePromise = Promise.resolve.bind(Promise)
const CallbacksRegistry = require('@electron/internal/renderer/callbacks-registry')
const bufferUtils = require('@electron/internal/common/buffer-utils')
const errorUtils = require('@electron/internal/common/error-utils')
const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal')
const callbacksRegistry = new CallbacksRegistry()
const remoteObjectCache = v8Util.createIDWeakMap()
// An unique ID that can represent current context.
const contextId = v8Util.getHiddenValue(global, 'contextId')
// Notify the main process when current context is going to be released.
// Note that when the renderer process is destroyed, the message may not be
// sent, we also listen to the "render-view-deleted" event in the main process
// to guard that situation.
process.on('exit', () => {
const command = 'ELECTRON_BROWSER_CONTEXT_RELEASE'
ipcRendererInternal.sendSync(command, contextId)
})
// Convert the arguments object into an array of meta data.
function wrapArgs (args, visited = new Set()) {
const valueToMeta = (value) => {
// Check for circular reference.
if (visited.has(value)) {
return {
type: 'value',
value: null
}
}
if (Array.isArray(value)) {
visited.add(value)
const meta = {
type: 'array',
value: wrapArgs(value, visited)
}
visited.delete(value)
return meta
} else if (bufferUtils.isBuffer(value)) {
return {
type: 'buffer',
value: bufferUtils.bufferToMeta(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')
}
}
const meta = {
type: 'object',
name: value.constructor ? value.constructor.name : '',
members: []
}
visited.add(value)
for (const 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.
function setObjectMembers (ref, object, metaId, members) {
if (!Array.isArray(members)) return
for (const member of members) {
if (object.hasOwnProperty(member.name)) continue
const descriptor = { enumerable: member.enumerable }
if (member.type === 'method') {
const remoteMemberFunction = function (...args) {
let command
if (this && this.constructor === remoteMemberFunction) {
command = 'ELECTRON_BROWSER_MEMBER_CONSTRUCTOR'
} else {
command = 'ELECTRON_BROWSER_MEMBER_CALL'
}
const ret = ipcRendererInternal.sendSync(command, contextId, metaId, member.name, wrapArgs(args))
return metaToValue(ret)
}
let descriptorFunction = proxyFunctionProperties(remoteMemberFunction, metaId, member.name)
descriptor.get = () => {
descriptorFunction.ref = ref // The member should reference its object.
return descriptorFunction
}
// Enable monkey-patch the method
descriptor.set = (value) => {
descriptorFunction = value
return value
}
descriptor.configurable = true
} else if (member.type === 'get') {
descriptor.get = () => {
const command = 'ELECTRON_BROWSER_MEMBER_GET'
const meta = ipcRendererInternal.sendSync(command, contextId, metaId, member.name)
return metaToValue(meta)
}
if (member.writable) {
descriptor.set = (value) => {
const args = wrapArgs([value])
const command = 'ELECTRON_BROWSER_MEMBER_SET'
const meta = ipcRendererInternal.sendSync(command, contextId, metaId, member.name, args)
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.
function setObjectPrototype (ref, object, metaId, descriptor) {
if (descriptor === null) return
const 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
function proxyFunctionProperties (remoteMemberFunction, metaId, name) {
let loaded = false
// Lazily load function properties
const loadRemoteProperties = () => {
if (loaded) return
loaded = true
const command = 'ELECTRON_BROWSER_MEMBER_GET'
const meta = ipcRendererInternal.sendSync(command, contextId, 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]
if (property === 'toString' && typeof value === 'function') {
return value.bind(target)
}
return value
},
ownKeys: (target) => {
loadRemoteProperties()
return Object.getOwnPropertyNames(target)
},
getOwnPropertyDescriptor: (target, property) => {
const descriptor = Object.getOwnPropertyDescriptor(target, property)
if (descriptor) return descriptor
loadRemoteProperties()
return Object.getOwnPropertyDescriptor(target, property)
}
})
}
// Convert meta data from browser into real value.
function metaToValue (meta) {
const types = {
value: () => meta.value,
array: () => meta.members.map((member) => metaToValue(member)),
buffer: () => bufferUtils.metaToBuffer(meta.value),
promise: () => resolvePromise({ then: metaToValue(meta.then) }),
error: () => metaToPlainObject(meta),
date: () => new Date(meta.value),
exception: () => { throw errorUtils.deserialize(meta.value) }
}
if (meta.type in types) {
return types[meta.type]()
} else {
let ret
if (remoteObjectCache.has(meta.id)) {
v8Util.addRemoteObjectRef(contextId, meta.id)
return remoteObjectCache.get(meta.id)
}
// A shadow class to represent the remote function object.
if (meta.type === 'function') {
const remoteFunction = function (...args) {
let command
if (this && this.constructor === remoteFunction) {
command = 'ELECTRON_BROWSER_CONSTRUCTOR'
} else {
command = 'ELECTRON_BROWSER_FUNCTION_CALL'
}
const obj = ipcRendererInternal.sendSync(command, contextId, meta.id, wrapArgs(args))
return metaToValue(obj)
}
ret = remoteFunction
} else {
ret = {}
}
setObjectMembers(ret, ret, meta.id, meta.members)
setObjectPrototype(ret, ret, meta.id, meta.proto)
Object.defineProperty(ret.constructor, 'name', { value: meta.name })
// Track delegate obj's lifetime & tell browser to clean up when object is GCed.
v8Util.setRemoteObjectFreer(ret, contextId, meta.id)
v8Util.setHiddenValue(ret, 'atomId', meta.id)
v8Util.addRemoteObjectRef(contextId, meta.id)
remoteObjectCache.set(meta.id, ret)
return ret
}
}
// Construct a plain object from the meta.
function metaToPlainObject (meta) {
const obj = (() => meta.type === 'error' ? new Error() : {})()
for (let i = 0; i < meta.members.length; i++) {
const { name, value } = meta.members[i]
obj[name] = value
}
return obj
}
function handleMessage (channel, handler) {
ipcRendererInternal.on(channel, (event, passedContextId, id, ...args) => {
if (passedContextId === contextId) {
handler(id, ...args)
} else {
// Message sent to an un-exist context, notify the error to main process.
ipcRendererInternal.send('ELECTRON_BROWSER_WRONG_CONTEXT_ERROR', contextId, passedContextId, id)
}
})
}
// Browser calls a callback in renderer.
handleMessage('ELECTRON_RENDERER_CALLBACK', (id, args) => {
callbacksRegistry.apply(id, metaToValue(args))
})
// A callback in browser is released.
handleMessage('ELECTRON_RENDERER_RELEASE_CALLBACK', (id) => {
callbacksRegistry.remove(id)
})
exports.require = (module) => {
const command = 'ELECTRON_BROWSER_REQUIRE'
const meta = ipcRendererInternal.sendSync(command, contextId, module)
return metaToValue(meta)
}
// Alias to remote.require('electron').xxx.
exports.getBuiltin = (module) => {
const command = 'ELECTRON_BROWSER_GET_BUILTIN'
const meta = ipcRendererInternal.sendSync(command, contextId, module)
return metaToValue(meta)
}
exports.getCurrentWindow = () => {
const command = 'ELECTRON_BROWSER_CURRENT_WINDOW'
const meta = ipcRendererInternal.sendSync(command, contextId)
return metaToValue(meta)
}
// Get current WebContents object.
exports.getCurrentWebContents = () => {
const command = 'ELECTRON_BROWSER_CURRENT_WEB_CONTENTS'
const meta = ipcRendererInternal.sendSync(command, contextId)
return metaToValue(meta)
}
// Get a global object in browser.
exports.getGlobal = (name) => {
const command = 'ELECTRON_BROWSER_GLOBAL'
const meta = ipcRendererInternal.sendSync(command, contextId, name)
return metaToValue(meta)
}
// Get the process object in browser.
exports.__defineGetter__('process', () => exports.getGlobal('process'))
// Create a function that will return the specified value when called in browser.
exports.createFunctionWithReturnValue = (returnValue) => {
const func = () => returnValue
v8Util.setHiddenValue(func, 'returnValue', true)
return func
}
// Get the guest WebContents from guestInstanceId.
exports.getGuestWebContents = (guestInstanceId) => {
const command = 'ELECTRON_BROWSER_GUEST_WEB_CONTENTS'
const meta = ipcRendererInternal.sendSync(command, contextId, guestInstanceId)
return metaToValue(meta)
}
const addBuiltinProperty = (name) => {
Object.defineProperty(exports, name, {
get: () => exports.getBuiltin(name)
})
}
const browserModules =
require('@electron/internal/common/api/module-list').concat(
require('@electron/internal/browser/api/module-list'))
// And add a helper receiver for each one.
browserModules
.filter((m) => !m.private)
.map((m) => m.name)
.forEach(addBuiltinProperty)