remote.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import { CallbacksRegistry } from '../remote/callbacks-registry';
  2. import { isPromise, isSerializableObject, serialize, deserialize } from '../../common/type-utils';
  3. import { MetaTypeFromRenderer, ObjectMember, ObjProtoDescriptor, MetaType } from '../../common/remote/types';
  4. import { ipcRendererInternal } from '../ipc-renderer-internal';
  5. import type { BrowserWindow, WebContents } from 'electron/main';
  6. import deprecate from '@electron/internal/common/api/deprecate';
  7. import { browserModuleNames } from '@electron/internal/browser/api/module-names';
  8. import { commonModuleList } from '@electron/internal/common/api/module-list';
  9. import { IPC_MESSAGES } from '@electron/internal/common/remote/ipc-messages';
  10. deprecate.log('The remote module is deprecated. Use https://github.com/electron/remote instead.');
  11. const v8Util = process._linkedBinding('electron_common_v8_util');
  12. const { hasSwitch } = process._linkedBinding('electron_common_command_line');
  13. const callbacksRegistry = new CallbacksRegistry();
  14. const remoteObjectCache = new Map();
  15. const finalizationRegistry = new (window as any).FinalizationRegistry((id: number) => {
  16. const ref = remoteObjectCache.get(id);
  17. if (ref !== undefined && ref.deref() === undefined) {
  18. remoteObjectCache.delete(id);
  19. ipcRendererInternal.send(IPC_MESSAGES.BROWSER_DEREFERENCE, contextId, id, 0);
  20. }
  21. });
  22. function getCachedRemoteObject (id: number) {
  23. const ref = remoteObjectCache.get(id);
  24. if (ref !== undefined) {
  25. const deref = ref.deref();
  26. if (deref !== undefined) return deref;
  27. }
  28. }
  29. function setCachedRemoteObject (id: number, value: any) {
  30. const wr = new (window as any).WeakRef(value);
  31. remoteObjectCache.set(id, wr);
  32. finalizationRegistry.register(value, id);
  33. return value;
  34. }
  35. // An unique ID that can represent current context.
  36. const contextId = v8Util.getHiddenValue<string>(global, 'contextId');
  37. // Notify the main process when current context is going to be released.
  38. // Note that when the renderer process is destroyed, the message may not be
  39. // sent, we also listen to the "render-view-deleted" event in the main process
  40. // to guard that situation.
  41. process.on('exit', () => {
  42. const command = IPC_MESSAGES.BROWSER_CONTEXT_RELEASE;
  43. ipcRendererInternal.send(command, contextId);
  44. });
  45. const IS_REMOTE_PROXY = Symbol('is-remote-proxy');
  46. // Convert the arguments object into an array of meta data.
  47. function wrapArgs (args: any[], visited = new Set()): any {
  48. const valueToMeta = (value: any): any => {
  49. // Check for circular reference.
  50. if (visited.has(value)) {
  51. return {
  52. type: 'value',
  53. value: null
  54. };
  55. }
  56. if (value && value.constructor && value.constructor.name === 'NativeImage') {
  57. return { type: 'nativeimage', value: serialize(value) };
  58. } else if (Array.isArray(value)) {
  59. visited.add(value);
  60. const meta = {
  61. type: 'array',
  62. value: wrapArgs(value, visited)
  63. };
  64. visited.delete(value);
  65. return meta;
  66. } else if (value instanceof Buffer) {
  67. return {
  68. type: 'buffer',
  69. value
  70. };
  71. } else if (isSerializableObject(value)) {
  72. return {
  73. type: 'value',
  74. value
  75. };
  76. } else if (typeof value === 'object') {
  77. if (isPromise(value)) {
  78. return {
  79. type: 'promise',
  80. then: valueToMeta(function (onFulfilled: Function, onRejected: Function) {
  81. value.then(onFulfilled, onRejected);
  82. })
  83. };
  84. } else if (v8Util.getHiddenValue(value, 'electronId')) {
  85. return {
  86. type: 'remote-object',
  87. id: v8Util.getHiddenValue(value, 'electronId')
  88. };
  89. }
  90. const meta: MetaTypeFromRenderer = {
  91. type: 'object',
  92. name: value.constructor ? value.constructor.name : '',
  93. members: []
  94. };
  95. visited.add(value);
  96. for (const prop in value) { // eslint-disable-line guard-for-in
  97. meta.members.push({
  98. name: prop,
  99. value: valueToMeta(value[prop])
  100. });
  101. }
  102. visited.delete(value);
  103. return meta;
  104. } else if (typeof value === 'function' && v8Util.getHiddenValue(value, 'returnValue')) {
  105. return {
  106. type: 'function-with-return-value',
  107. value: valueToMeta(value())
  108. };
  109. } else if (typeof value === 'function') {
  110. return {
  111. type: 'function',
  112. id: callbacksRegistry.add(value),
  113. location: v8Util.getHiddenValue(value, 'location'),
  114. length: value.length
  115. };
  116. } else {
  117. return {
  118. type: 'value',
  119. value
  120. };
  121. }
  122. };
  123. return args.map(valueToMeta);
  124. }
  125. // Populate object's members from descriptors.
  126. // The |ref| will be kept referenced by |members|.
  127. // This matches |getObjectMembers| in rpc-server.
  128. function setObjectMembers (ref: any, object: any, metaId: number, members: ObjectMember[]) {
  129. if (!Array.isArray(members)) return;
  130. for (const member of members) {
  131. if (Object.prototype.hasOwnProperty.call(object, member.name)) continue;
  132. const descriptor: PropertyDescriptor = { enumerable: member.enumerable };
  133. if (member.type === 'method') {
  134. const remoteMemberFunction = function (this: any, ...args: any[]) {
  135. let command;
  136. if (this && this.constructor === remoteMemberFunction) {
  137. command = IPC_MESSAGES.BROWSER_MEMBER_CONSTRUCTOR;
  138. } else {
  139. command = IPC_MESSAGES.BROWSER_MEMBER_CALL;
  140. }
  141. const ret = ipcRendererInternal.sendSync(command, contextId, metaId, member.name, wrapArgs(args));
  142. return metaToValue(ret);
  143. };
  144. let descriptorFunction = proxyFunctionProperties(remoteMemberFunction, metaId, member.name);
  145. descriptor.get = () => {
  146. descriptorFunction.ref = ref; // The member should reference its object.
  147. return descriptorFunction;
  148. };
  149. // Enable monkey-patch the method
  150. descriptor.set = (value) => {
  151. descriptorFunction = value;
  152. return value;
  153. };
  154. descriptor.configurable = true;
  155. } else if (member.type === 'get') {
  156. descriptor.get = () => {
  157. const command = IPC_MESSAGES.BROWSER_MEMBER_GET;
  158. const meta = ipcRendererInternal.sendSync(command, contextId, metaId, member.name);
  159. return metaToValue(meta);
  160. };
  161. if (member.writable) {
  162. descriptor.set = (value) => {
  163. const args = wrapArgs([value]);
  164. const command = IPC_MESSAGES.BROWSER_MEMBER_SET;
  165. const meta = ipcRendererInternal.sendSync(command, contextId, metaId, member.name, args);
  166. if (meta != null) metaToValue(meta);
  167. return value;
  168. };
  169. }
  170. }
  171. Object.defineProperty(object, member.name, descriptor);
  172. }
  173. }
  174. // Populate object's prototype from descriptor.
  175. // This matches |getObjectPrototype| in rpc-server.
  176. function setObjectPrototype (ref: any, object: any, metaId: number, descriptor: ObjProtoDescriptor) {
  177. if (descriptor === null) return;
  178. const proto = {};
  179. setObjectMembers(ref, proto, metaId, descriptor.members);
  180. setObjectPrototype(ref, proto, metaId, descriptor.proto);
  181. Object.setPrototypeOf(object, proto);
  182. }
  183. // Wrap function in Proxy for accessing remote properties
  184. function proxyFunctionProperties (remoteMemberFunction: Function, metaId: number, name: string) {
  185. let loaded = false;
  186. // Lazily load function properties
  187. const loadRemoteProperties = () => {
  188. if (loaded) return;
  189. loaded = true;
  190. const command = IPC_MESSAGES.BROWSER_MEMBER_GET;
  191. const meta = ipcRendererInternal.sendSync(command, contextId, metaId, name);
  192. setObjectMembers(remoteMemberFunction, remoteMemberFunction, meta.id, meta.members);
  193. };
  194. return new Proxy(remoteMemberFunction as any, {
  195. set: (target, property, value) => {
  196. if (property !== 'ref') loadRemoteProperties();
  197. target[property] = value;
  198. return true;
  199. },
  200. get: (target, property) => {
  201. if (property === IS_REMOTE_PROXY) return true;
  202. if (!Object.prototype.hasOwnProperty.call(target, property)) loadRemoteProperties();
  203. const value = target[property];
  204. if (property === 'toString' && typeof value === 'function') {
  205. return value.bind(target);
  206. }
  207. return value;
  208. },
  209. ownKeys: (target) => {
  210. loadRemoteProperties();
  211. return Object.getOwnPropertyNames(target);
  212. },
  213. getOwnPropertyDescriptor: (target, property) => {
  214. const descriptor = Object.getOwnPropertyDescriptor(target, property);
  215. if (descriptor) return descriptor;
  216. loadRemoteProperties();
  217. return Object.getOwnPropertyDescriptor(target, property);
  218. }
  219. });
  220. }
  221. // Convert meta data from browser into real value.
  222. function metaToValue (meta: MetaType): any {
  223. if (meta.type === 'value') {
  224. return meta.value;
  225. } else if (meta.type === 'array') {
  226. return meta.members.map((member) => metaToValue(member));
  227. } else if (meta.type === 'nativeimage') {
  228. return deserialize(meta.value);
  229. } else if (meta.type === 'buffer') {
  230. return Buffer.from(meta.value.buffer, meta.value.byteOffset, meta.value.byteLength);
  231. } else if (meta.type === 'promise') {
  232. return Promise.resolve({ then: metaToValue(meta.then) });
  233. } else if (meta.type === 'error') {
  234. return metaToError(meta);
  235. } else if (meta.type === 'exception') {
  236. if (meta.value.type === 'error') { throw metaToError(meta.value); } else { throw new Error(`Unexpected value type in exception: ${meta.value.type}`); }
  237. } else {
  238. let ret;
  239. if ('id' in meta) {
  240. const cached = getCachedRemoteObject(meta.id);
  241. if (cached !== undefined) { return cached; }
  242. }
  243. // A shadow class to represent the remote function object.
  244. if (meta.type === 'function') {
  245. const remoteFunction = function (this: any, ...args: any[]) {
  246. let command;
  247. if (this && this.constructor === remoteFunction) {
  248. command = IPC_MESSAGES.BROWSER_CONSTRUCTOR;
  249. } else {
  250. command = IPC_MESSAGES.BROWSER_FUNCTION_CALL;
  251. }
  252. const obj = ipcRendererInternal.sendSync(command, contextId, meta.id, wrapArgs(args));
  253. return metaToValue(obj);
  254. };
  255. ret = remoteFunction;
  256. } else {
  257. ret = {};
  258. }
  259. setObjectMembers(ret, ret, meta.id, meta.members);
  260. setObjectPrototype(ret, ret, meta.id, meta.proto);
  261. if (ret.constructor && (ret.constructor as any)[IS_REMOTE_PROXY]) {
  262. Object.defineProperty(ret.constructor, 'name', { value: meta.name });
  263. }
  264. // Track delegate obj's lifetime & tell browser to clean up when object is GCed.
  265. v8Util.setHiddenValue(ret, 'electronId', meta.id);
  266. setCachedRemoteObject(meta.id, ret);
  267. return ret;
  268. }
  269. }
  270. function metaToError (meta: { type: 'error', value: any, members: ObjectMember[] }) {
  271. const obj = meta.value;
  272. for (const { name, value } of meta.members) {
  273. obj[name] = metaToValue(value);
  274. }
  275. return obj;
  276. }
  277. function handleMessage (channel: string, handler: Function) {
  278. ipcRendererInternal.onMessageFromMain(channel, (event, passedContextId, id, ...args) => {
  279. if (passedContextId === contextId) {
  280. handler(id, ...args);
  281. } else {
  282. // Message sent to an un-exist context, notify the error to main process.
  283. ipcRendererInternal.send(IPC_MESSAGES.BROWSER_WRONG_CONTEXT_ERROR, contextId, passedContextId, id);
  284. }
  285. });
  286. }
  287. const enableStacks = hasSwitch('enable-api-filtering-logging');
  288. function getCurrentStack (): string | undefined {
  289. const target = { stack: undefined as string | undefined };
  290. if (enableStacks) {
  291. Error.captureStackTrace(target, getCurrentStack);
  292. }
  293. return target.stack;
  294. }
  295. // Browser calls a callback in renderer.
  296. handleMessage(IPC_MESSAGES.RENDERER_CALLBACK, (id: number, args: any) => {
  297. callbacksRegistry.apply(id, metaToValue(args));
  298. });
  299. // A callback in browser is released.
  300. handleMessage(IPC_MESSAGES.RENDERER_RELEASE_CALLBACK, (id: number) => {
  301. callbacksRegistry.remove(id);
  302. });
  303. exports.require = (module: string) => {
  304. const command = IPC_MESSAGES.BROWSER_REQUIRE;
  305. const meta = ipcRendererInternal.sendSync(command, contextId, module, getCurrentStack());
  306. return metaToValue(meta);
  307. };
  308. // Alias to remote.require('electron').xxx.
  309. export function getBuiltin (module: string) {
  310. const command = IPC_MESSAGES.BROWSER_GET_BUILTIN;
  311. const meta = ipcRendererInternal.sendSync(command, contextId, module, getCurrentStack());
  312. return metaToValue(meta);
  313. }
  314. export function getCurrentWindow (): BrowserWindow {
  315. const command = IPC_MESSAGES.BROWSER_GET_CURRENT_WINDOW;
  316. const meta = ipcRendererInternal.sendSync(command, contextId, getCurrentStack());
  317. return metaToValue(meta);
  318. }
  319. // Get current WebContents object.
  320. export function getCurrentWebContents (): WebContents {
  321. const command = IPC_MESSAGES.BROWSER_GET_CURRENT_WEB_CONTENTS;
  322. const meta = ipcRendererInternal.sendSync(command, contextId, getCurrentStack());
  323. return metaToValue(meta);
  324. }
  325. // Get a global object in browser.
  326. export function getGlobal<T = any> (name: string): T {
  327. const command = IPC_MESSAGES.BROWSER_GET_GLOBAL;
  328. const meta = ipcRendererInternal.sendSync(command, contextId, name, getCurrentStack());
  329. return metaToValue(meta);
  330. }
  331. // Get the process object in browser.
  332. Object.defineProperty(exports, 'process', {
  333. get: () => exports.getGlobal('process')
  334. });
  335. // Create a function that will return the specified value when called in browser.
  336. export function createFunctionWithReturnValue<T> (returnValue: T): () => T {
  337. const func = () => returnValue;
  338. v8Util.setHiddenValue(func, 'returnValue', true);
  339. return func;
  340. }
  341. const addBuiltinProperty = (name: string) => {
  342. Object.defineProperty(exports, name, {
  343. get: () => exports.getBuiltin(name)
  344. });
  345. };
  346. const browserModules = commonModuleList.concat(browserModuleNames.map(name => ({ name, loader: () => {} })));
  347. // And add a helper receiver for each one.
  348. browserModules
  349. .filter((m) => !m.private)
  350. .map((m) => m.name)
  351. .forEach(addBuiltinProperty);