├── .babelrc
├── .compilerc
├── .eslintrc
├── .gitignore
├── .npmignore
├── CODE_OF_CONDUCT.md
├── COPYING
├── README.md
├── build.cmd
├── build.sh
├── esdoc.json
├── package-lock.json
├── package.json
├── remote-ajax.js
├── src
├── custom-operators.js
├── execute-js-func.js
├── index.js
├── remote-ajax.js
├── remote-event-browser.js
├── remote-event.js
├── renderer-require-preload.html
├── renderer-require-preload.js
├── renderer-require.js
└── rx-dom.js
└── test
├── .eslintrc
├── asserttest.js
├── dummy-module.js
├── recursive-proxy-handler.js
├── remote-event.js
├── renderer-require.js
└── support.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2016-node5"],
3 | "plugins": ["transform-async-to-generator"]
4 | }
5 |
--------------------------------------------------------------------------------
/.compilerc:
--------------------------------------------------------------------------------
1 | {
2 | "application/javascript": {
3 | "presets": ["es2016-node5"],
4 | "plugins": ["transform-async-to-generator", "transform-runtime"],
5 | "sourceMaps": "inline"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "strict": 0,
5 | "indent": [
6 | 2,
7 | 2
8 | ],
9 | "semi": [
10 | 2,
11 | "always"
12 | ],
13 | "no-console": 0
14 | },
15 | "env": {
16 | "es6": true,
17 | "node": true,
18 | "browser": true
19 | },
20 | "extends": "eslint:recommended"
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
27 | node_modules
28 |
29 | lib
30 | test-dist
31 | docs
32 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
27 | node_modules
28 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, and in the interest of
4 | fostering an open and welcoming community, we pledge to respect all people who
5 | contribute through reporting issues, posting feature requests, updating
6 | documentation, submitting pull requests or patches, and other activities.
7 |
8 | We are committed to making participation in this project a harassment-free
9 | experience for everyone, regardless of level of experience, gender, gender
10 | identity and expression, sexual orientation, disability, personal appearance,
11 | body size, race, ethnicity, age, religion, or nationality.
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | * The use of sexualized language or imagery
16 | * Personal attacks
17 | * Trolling or insulting/derogatory comments
18 | * Public or private harassment
19 | * Publishing other's private information, such as physical or electronic
20 | addresses, without explicit permission
21 | * Other unethical or unprofessional conduct
22 |
23 | Project maintainers have the right and responsibility to remove, edit, or
24 | reject comments, commits, code, wiki edits, issues, and other contributions
25 | that are not aligned to this Code of Conduct, or to ban temporarily or
26 | permanently any contributor for other behaviors that they deem inappropriate,
27 | threatening, offensive, or harmful.
28 |
29 | By adopting this Code of Conduct, project maintainers commit themselves to
30 | fairly and consistently applying these principles to every aspect of managing
31 | this project. Project maintainers who do not follow or enforce the Code of
32 | Conduct may be permanently removed from the project team.
33 |
34 | This Code of Conduct applies both within project spaces and in public spaces
35 | when an individual is representing the project or its community.
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
38 | reported by contacting a project maintainer at paul@paulbetts.org. All
39 | complaints will be reviewed and investigated and will result in a response that
40 | is deemed necessary and appropriate to the circumstances. Maintainers are
41 | obligated to maintain confidentiality with regard to the reporter of an
42 | incident.
43 |
44 |
45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
46 | version 1.3.0, available at
47 | [http://contributor-covenant.org/version/1/3/0/][version]
48 |
49 | [homepage]: http://contributor-covenant.org
50 | [version]: http://contributor-covenant.org/version/1/3/0/
51 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Paul Betts
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED: electron-remote: an asynchronous 'remote', and more
2 |
3 | [](http://unmaintained.tech/)
4 |
5 | This project is no longer maintained, pull requests are no longer being reviewed or merged and issues are no longer being responded to.
6 |
7 | ---
8 |
9 |  
10 |
11 |
12 | electron-remote provides an alternative to Electron's `remote` module based around Promises instead of synchronous execution. It also provides an automatic way to use BrowserWindows as "background processes" that auto-scales based on usage, similar to Grand Central Dispatch or the .NET TPL Taskpool.
13 |
14 | ## The Quickest of Quick Starts
15 |
16 | ###### Calling main process modules from a renderer
17 |
18 | ```js
19 | import { createProxyForMainProcessModule } from 'electron-remote';
20 |
21 | // app is now a proxy for the app module in the main process
22 | const app = createProxyForMainProcessModule('app');
23 |
24 | // The difference is all methods return a Promise instead of blocking
25 | const memoryInfo = await app.getAppMemoryInfo();
26 | ```
27 |
28 | ###### Calling code in other windows
29 |
30 | ```js
31 | import { createProxyForRemote } from 'electron-remote';
32 |
33 | // myWindowJs is now a proxy object for myWindow's `window` global object
34 | const myWindowJs = createProxyForRemote(myWindow);
35 |
36 | // Functions suffixed with _get will read a value
37 | userAgent = await myWindowJs.navigator.userAgent_get()
38 | ```
39 |
40 | ###### Renderer Taskpool
41 |
42 | ```js
43 | import { requireTaskPool } from 'electron-remote';
44 |
45 | const myCoolModule = requireTaskPool(require.resolve('./my-cool-module'));
46 |
47 | // This method will run synchronously, but in a background BrowserWindow process
48 | // so that your app will not block
49 | let result = await myCoolModule.calculateDigitsOfPi(100000);
50 | ```
51 |
52 | ## But I like Remote!
53 |
54 | Remote is super convenient! But it also has some downsides - its main downside is that its action is **synchronous**. This means that both the main and window processes will _wait_ for a method to finish running. Even for quick methods, calling it too often can introduce scroll jank and generally cause performance problems.
55 |
56 | electron-remote is a version of remote that, while less ergonomic, guarantees that it won't block the calling thread.
57 |
58 | ## Using createProxyForRemote
59 |
60 | `createProxyForRemote` is a replacement for places where you would use Electron's `executeJavaScript` method on BrowserWindow or WebView instances - however, it works a little differently. Using a new feature in ES2015 called [proxy objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), we create an object which represents the `window` object on a remote context, and all method calls get sent as messages to that remote instead of being run immediately, which feels very similar to the `remote` Electron module.
61 |
62 | This provides a number of very important advantages:
63 |
64 | * `createProxyForRemote` uses asynchronous IPC instead of blocking
65 | * Parameters are serialized directly, so you don't have to try to build strings that can be `eval`d, which is a dangerous endeavor at best.
66 | * Calling methods on objects is far more convenient than trying to poke at things via a remote eval.
67 |
68 | #### How do I get properties if everything is a Promise tho???
69 |
70 | Astute observers will note, that getting the value of a property is always a synchronous operation - to facilitate that, any method with `_get()` appended to it will let you fetch the value for the property.
71 |
72 | ```js
73 | import { createProxyForRemote } from 'electron-remote';
74 |
75 | // myWindowJs is now a proxy object for myWindow's `window` global object
76 | const myWindowJs = createProxyForRemote(myWindow);
77 |
78 | // Functions suffixed with _get will read a value
79 | myWindowJs.navigator.userAgent_get()
80 | .then((agent) => console.log(`The user agent is ${agent}`));
81 | ```
82 |
83 | #### But do this first!
84 |
85 | Before you use `createProxyForRemote`, you **must** call `initializeEvalHandler()` in the target window on startup. This sets up the listeners that electron-remote will use.
86 |
87 | #### Bringing it all together
88 |
89 | ```js
90 | // In my window's main.js
91 | initializeEvalHandler();
92 | window.addNumbers = (a,b) => a + b;
93 |
94 |
95 | // In my main process
96 | let myWindowProxy = createProxyForRemote(myWindow);
97 | myWindowProxy.addNumbers(5, 5)
98 | .then((x) => console.log(x));
99 |
100 | >>> 10
101 | ```
102 |
103 | #### Using createProxyForMainProcessModule
104 | This is meant to be a drop-in replacement for places you would have used `remote` in a renderer process. It's almost identical to `createProxyForRemote`, but instead of `eval`ing JavaScript it can only call methods on main process modules. It still has all the same benefits: asynchronous IPC instead of an `ipc.sendSync`.
105 |
106 | ## Here Be Dragons
107 |
108 | electron-remote has a number of significant caveats versus the remote module that you should definitely be aware of:
109 |
110 | * Remote values must be Serializable
111 |
112 | Objects that you return to the calling process must be serializable (i.e. you can call `JSON.stringify` on it and get a valid thing)- this means that creating Classes won't work, nor will return objects like BrowserWindows or other Electron objects. For example:
113 |
114 | ```js
115 | let myWindowProxy = createProxyForRemote(myWindow);
116 |
117 | // XXX: BAD - HTML elements aren't serializable
118 | let obj = myWindowProxy.document.createElement('h1');
119 | ```
120 |
121 | * Remote event listeners aren't supported
122 |
123 | Anything that involves an event handler isn't going to work:
124 |
125 | ```js
126 | // XXX: BAD - You can't add event handlers
127 | myWindowProxy.document.addEventListener('onBlur', (e) => console.log("Blur!"));
128 | ```
129 |
130 | ## The Renderer Taskpool
131 |
132 | Renderer Taskpools provide an automatic way to use BrowserWindows as "background processes" that auto-scales based on usage, similar to Grand Central Dispatch or the .NET TPL Taskpool. This works by allowing you to provide a Module that you'd like to load in the remote processes, which will be loaded and unloaded on the fly according to demand.
133 |
134 | Let's look at the example again:
135 |
136 | ```js
137 | import { requireTaskPool } from 'electron-remote';
138 |
139 | const myCoolModule = requireTaskPool(require.resolve('./my-cool-module'));
140 |
141 | // This method will run synchronously, but in a background BrowserWindow process
142 | // so that your app will not block
143 | let result = await myCoolModule.calculateDigitsOfPi(100000);
144 | ```
145 |
146 | By default, `requireTaskPool` will create up to four background processes to concurrently run JS code on. As these processes become busy, requests will be queued to different processes and wait in line implicitly.
147 |
148 | ##### More Dragons
149 |
150 | Since `requireTaskPool` will create and destroy processes as needed, this means that global variables or other state will be destroyed as well. You can't rely on setting a global variable and having it persist for a period of time longer than one method call.
151 |
152 | ## The remote-ajax module
153 |
154 | One module that is super useful to have from the main process is a way to make network requests using Chromium's networking stack, which correctly does things such as respecting the system proxy settings. To this end, electron-remote comes with a convenient wrapper around Rx-DOM's AJAX methods called `remote-ajax`.
155 |
156 | ```js
157 | import { requireTaskPool } from 'electron-remote';
158 |
159 | const remoteAjax = requireTaskPool(require.resolve('electron-remote/remote-ajax'));
160 |
161 | // Result is the object that XmlHttpRequest gives you
162 | let result = await remoteAjax.get('https://httpbin.org/get');
163 | console.log(result.url)
164 |
165 | >>> 'https://httpbin.org/get'
166 | ```
167 |
168 | See the documentation for [Rx-DOM](https://github.com/Reactive-Extensions/RxJS-DOM/blob/master/modules/main-ajax/readme.md) for how these methods work.
169 |
170 | Another method that is included is `downloadFileOrUrl`, which lets you download a file to a target:
171 |
172 | ```js
173 | /**
174 | * Downloads a path as either a file path or a HTTP URL to a specific place
175 | *
176 | * @param {string} pathOrUrl Either an HTTP URL or a file path.
177 | * @return {string} The contents as a UTF-8 decoded string.
178 | */
179 | function downloadFileOrUrl(pathOrUrl, target)
180 | ```
181 |
--------------------------------------------------------------------------------
/build.cmd:
--------------------------------------------------------------------------------
1 | npm i && npm t
2 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | npm i && npm t
3 |
--------------------------------------------------------------------------------
/esdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./src",
3 | "destination": "./docs",
4 | "includes": ["\\.(js|es6)$"],
5 | "excludes": ["\\.config\\.(js|es6)$"],
6 | "access": ["public", "protected"],
7 | "autoPrivate": true,
8 | "unexportIdentifier": false,
9 | "undocumentIdentifier": true,
10 | "builtinExternal": true,
11 | "index": "./README.md",
12 | "package": "./package.json",
13 | "coverage": true,
14 | "includeSource": true,
15 | "title": "electron-compilers",
16 | "plugins": [
17 | {"name": "esdoc-es7-plugin"},
18 | {"name": "esdoc-plugin-async-to-sync"}
19 | ],
20 | "test": {
21 | "type": "mocha",
22 | "source": "./test",
23 | "includes": ["\\.(js|es6)$"]
24 | },
25 | "lint": true
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electron-remote",
3 | "version": "1.3.0",
4 | "description": "Execute JavaScript in remote Electron processes, but more betterer",
5 | "scripts": {
6 | "doc": "esdoc -c ./esdoc.json",
7 | "compile": "git clean -xdf ./lib && babel -d lib/ src/ && cp ./src/*.html ./lib/",
8 | "prepublish": "npm run compile",
9 | "test-renderer": "electron-mocha --renderer --require ./test/support.js ./test",
10 | "test-browser": "electron-mocha --require ./test/support.js ./test/renderer-require",
11 | "test": "npm run test-renderer && npm run test-browser",
12 | "node": "cross-env ELECTRON_RUN_AS_NODE=1 ./node_modules/electron-prebuilt-compile/node_modules/.bin/electron"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/paulcbetts/electron-remote"
17 | },
18 | "keywords": [
19 | "remote",
20 | "electron",
21 | "rx"
22 | ],
23 | "author": "Paul Betts ",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/paulcbetts/electron-remote/issues"
27 | },
28 | "main": "lib/index.js",
29 | "homepage": "https://github.com/paulcbetts/electron-remote",
30 | "dependencies": {
31 | "debug": "^2.5.1",
32 | "hashids": "^1.1.1",
33 | "lodash.get": "^4.4.2",
34 | "pify": "^2.3.0",
35 | "rxjs": "^5.0.0-beta.12",
36 | "xmlhttprequest": "^1.8.0"
37 | },
38 | "devDependencies": {
39 | "babel-cli": "^6.16.0",
40 | "babel-eslint": "^7.0.0",
41 | "babel-plugin-transform-async-to-generator": "^6.16.0",
42 | "babel-plugin-transform-runtime": "^6.15.0",
43 | "babel-preset-es2016-node5": "^1.1.2",
44 | "babel-register": "^6.16.3",
45 | "chai": "^3.5.0",
46 | "chai-as-promised": "^6.0.0",
47 | "cross-env": "^3.0.0",
48 | "electron-mocha": "^6.0.3",
49 | "electron-prebuilt-compile": "4.0.0",
50 | "esdoc": "^1.1.0",
51 | "esdoc-es7-plugin": "0.0.3",
52 | "esdoc-plugin-async-to-sync": "^0.5.0",
53 | "eslint": "^3.7.1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/remote-ajax.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/remote-ajax');
2 |
--------------------------------------------------------------------------------
/src/custom-operators.js:
--------------------------------------------------------------------------------
1 | import {Observable} from 'rxjs/Observable';
2 | import {Scheduler} from 'rxjs/Scheduler';
3 |
4 | import 'rxjs/add/operator/map';
5 | import 'rxjs/add/operator/switch';
6 | import 'rxjs/add/observable/timer';
7 |
8 | const newCoolOperators = {
9 | guaranteedThrottle: function (time, scheduler=Scheduler.timeout) {
10 | return this
11 | .map((x) => Observable.timer(time, scheduler).map(() => x))
12 | .switch();
13 | }
14 | };
15 |
16 | for (let key of Object.keys(newCoolOperators)) {
17 | Observable.prototype[key] = newCoolOperators[key];
18 | }
19 |
--------------------------------------------------------------------------------
/src/execute-js-func.js:
--------------------------------------------------------------------------------
1 | import {Observable} from 'rxjs/Observable';
2 | import {Subscription} from 'rxjs/Subscription';
3 | import Hashids from 'hashids';
4 | import get from 'lodash.get';
5 |
6 | import 'rxjs/add/observable/of';
7 | import 'rxjs/add/observable/throw';
8 |
9 | import 'rxjs/add/operator/catch';
10 | import 'rxjs/add/operator/do';
11 | import 'rxjs/add/operator/filter';
12 | import 'rxjs/add/operator/take';
13 | import 'rxjs/add/operator/mergeMap';
14 | import 'rxjs/add/operator/timeout';
15 | import 'rxjs/add/operator/toPromise';
16 |
17 | const requestChannel = 'execute-javascript-request';
18 | const responseChannel = 'execute-javascript-response';
19 | const rootEvalProxyName = 'electron-remote-eval-proxy';
20 | const requireElectronModule = '__requireElectronModule__';
21 |
22 | const electron = require('electron');
23 | const isBrowser = (process.type === 'browser');
24 | const ipc = electron[isBrowser ? 'ipcMain' : 'ipcRenderer'];
25 |
26 | const d = require('debug')('electron-remote:execute-js-func');
27 | const webContents = isBrowser ?
28 | electron.webContents :
29 | electron.remote.webContents;
30 |
31 | let nextId = 1;
32 | const hashIds = new Hashids();
33 |
34 | function getNextId() {
35 | return hashIds.encode(process.pid, nextId++);
36 | }
37 |
38 | /**
39 | * Determines the identifier for the current process (i.e. the thing we can use
40 | * to route messages to it)
41 | *
42 | * @return {object} An object with either a `guestInstanceId` or a `webContentsId`
43 | */
44 | export function getSenderIdentifier() {
45 | if (isBrowser) return {};
46 |
47 | if (process.guestInstanceId) {
48 | return { guestInstanceId: process.guestInstanceId };
49 | }
50 |
51 | return {
52 | webContentsId: require('electron').remote.getCurrentWebContents().id
53 | };
54 | }
55 |
56 | /**
57 | * Determines a way to send a reply back from an incoming eval request.
58 | *
59 | * @param {Object} request An object returned from {getSenderIdentifier}
60 | *
61 | * @return {Function} A function that act like ipc.send, but to a
62 | * particular process.
63 | *
64 | * @private
65 | */
66 | function getReplyMethod(request) {
67 | let target = findTargetFromParentInfo(request);
68 |
69 | if (target) {
70 | return (...a) => {
71 | if ('isDestroyed' in target && target.isDestroyed()) return;
72 | target.send(...a);
73 | };
74 | } else {
75 | d("Using reply to main process");
76 | return (...a) => ipc.send(...a);
77 | }
78 | }
79 |
80 | /**
81 | * Turns an IPC channel into an Observable
82 | *
83 | * @param {String} channel The IPC channel to listen to via `ipc.on`
84 | *
85 | * @return {Observable} An Observable which sends IPC args via `onNext`
86 | *
87 | * @private
88 | */
89 | function listenToIpc(channel) {
90 | return Observable.create((subj) => {
91 | let listener = (event, ...args) => {
92 | d(`Got an event for ${channel}: ${JSON.stringify(args)}`);
93 | subj.next(args);
94 | };
95 |
96 | d(`Setting up listener! ${channel}`);
97 | ipc.on(channel, listener);
98 |
99 | return new Subscription(() =>
100 | ipc.removeListener(channel, listener));
101 | });
102 | }
103 |
104 | /**
105 | * Returns a method that will act like `ipc.send` depending on the parameter
106 | * passed to it, so you don't have to check for `webContents`.
107 | *
108 | * @param {BrowserWindow|WebView} windowOrWebView The renderer to send to.
109 | *
110 | * @return {Function} A function that behaves like
111 | * `ipc.send`.
112 | *
113 | * @private
114 | */
115 | function getSendMethod(windowOrWebView) {
116 | if (!windowOrWebView) return (...a) => ipc.send(...a);
117 |
118 | if ('webContents' in windowOrWebView) {
119 | return (...a) => {
120 | d(`webContents send: ${JSON.stringify(a)}`);
121 | if (!windowOrWebView.webContents.isDestroyed()) {
122 | windowOrWebView.webContents.send(...a);
123 | } else {
124 | throw new Error(`WebContents has been destroyed`);
125 | }
126 | };
127 | } else {
128 | return (...a) => {
129 | d(`webView send: ${JSON.stringify(a)}`);
130 | windowOrWebView.send(...a);
131 | };
132 | }
133 | }
134 |
135 | /**
136 | * This method creates an Observable Promise that represents a future response
137 | * to a remoted call. It filters on ID, then cancels itself once either a
138 | * response is returned, or it times out.
139 | *
140 | * @param {Guid} id The ID of the sent request
141 | * @param {Number} timeout The timeout in milliseconds
142 | *
143 | * @return {Observable} An Observable Promise
144 | * representing the result, or
145 | * an OnError with the error.
146 | *
147 | * @private
148 | */
149 | function listenerForId(id, timeout) {
150 | return listenToIpc(responseChannel)
151 | .do(([x]) => d(`Got IPC! ${x.id} === ${id}; ${JSON.stringify(x)}`))
152 | .filter(([receive]) => receive.id === id && id)
153 | .take(1)
154 | .mergeMap(([receive]) => {
155 | if (receive.error) {
156 | let e = new Error(receive.error.message);
157 | e.stack = receive.error.stack;
158 | return Observable.throw(e);
159 | }
160 |
161 | return Observable.of(receive.result);
162 | })
163 | .timeout(timeout);
164 | }
165 |
166 |
167 | /**
168 | * Given the parentInfo returned from {getSenderIdentifier}, returns the actual
169 | * WebContents that it represents.
170 | *
171 | * @param {object} parentInfo The renderer process identifying info.
172 | *
173 | * @return {WebContents} An actual Renderer Process object.
174 | *
175 | * @private
176 | */
177 | function findTargetFromParentInfo(parentInfo=window.parentInfo) {
178 | if (!parentInfo) return null;
179 | if ('guestInstanceId' in parentInfo) {
180 | return require('electron').remote.getGuestWebContents(parentInfo.guestInstanceId);
181 | }
182 |
183 | if ('webContentsId' in parentInfo) {
184 | return webContents.fromId(parentInfo.webContentsId);
185 | }
186 |
187 | return null;
188 | }
189 |
190 | /**
191 | * Configures a child renderer process who to send replies to. Call this method
192 | * when you want child windows to be able to use their parent as an implicit
193 | * target.
194 | *
195 | * @param {BrowserWindow|WebView} windowOrWebView The child to configure
196 | */
197 | export function setParentInformation(windowOrWebView) {
198 | let info = getSenderIdentifier();
199 | let ret;
200 |
201 | if (info.guestInstanceId) {
202 | ret = remoteEval(windowOrWebView, `window.parentInfo = { guestInstanceId: ${info.guestInstanceId} }`);
203 | } else if (info.webContentsId) {
204 | ret = remoteEval(windowOrWebView, `window.parentInfo = { webContentsId: ${info.webContentsId} }`);
205 | } else {
206 | ret = remoteEval(windowOrWebView, `window.parentInfo = {}`);
207 | }
208 |
209 | return ret.catch((err) => d(`Unable to set parentInfo: ${err.stack || err.message}`));
210 | }
211 |
212 | /**
213 | * Evaluates a string `eval`-style in a remote renderer process.
214 | *
215 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code in.
216 | * @param {string} str The code to execute.
217 | * @param {Number} timeout The timeout in milliseconds
218 | *
219 | * @return {Observable} The result of the evaluation.
220 | * Must be JSON-serializable.
221 | */
222 | export function remoteEvalObservable(windowOrWebView, str, timeout=5*1000) {
223 | let send = getSendMethod(windowOrWebView || findTargetFromParentInfo());
224 | if (!send) {
225 | return Observable.throw(new Error(`Unable to find a target for: ${JSON.stringify(window.parentInfo)}`));
226 | }
227 |
228 | if (!str || str.length < 1) {
229 | return Observable.throw(new Error("RemoteEval called with empty or null code"));
230 | }
231 |
232 | let toSend = Object.assign({ id: getNextId(), eval: str }, getSenderIdentifier());
233 | let ret = listenerForId(toSend.id, timeout);
234 |
235 | d(`Sending: ${JSON.stringify(toSend)}`);
236 | send(requestChannel, toSend);
237 | return ret;
238 | }
239 |
240 | /**
241 | * Evaluates a string `eval`-style in a remote renderer process.
242 | *
243 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code in.
244 | * @param {string} str The code to execute.
245 | * @param {Number} timeout The timeout in milliseconds
246 | *
247 | * @return {Promise} The result of the evaluation.
248 | * Must be JSON-serializable.
249 | */
250 | export function remoteEval(windowOrWebView, str, timeout=5*1000) {
251 | return remoteEvalObservable(windowOrWebView, str, timeout).toPromise();
252 | }
253 |
254 | /**
255 | * Evaluates a JavaScript method on a remote object and returns the result. this
256 | * method can be used to either execute Functions in remote renderers, or return
257 | * values from objects. For example:
258 | *
259 | * let userAgent = await executeJavaScriptMethod(wnd, 'navigator.userAgent');
260 | *
261 | * executeJavaScriptMethod will also be smart enough to recognize when methods
262 | * themselves return Promises and await them:
263 | *
264 | * let fetchResult = await executeJavaScriptMethod('window.fetchHtml', 'https://google.com');
265 | *
266 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code
267 | * in. If this parameter is
268 | * null, this will reference
269 | * the browser process.
270 | * @param {Number} timeout Timeout in milliseconds
271 | * @param {string} pathToObject A path to the object to execute, in dotted
272 | * form i.e. 'document.querySelector'.
273 | * @param {Array} args The arguments to pass to the method
274 | *
275 | * @return {Observable} The result of evaluating the method or
276 | * property. Must be JSON serializable.
277 | */
278 | export function executeJavaScriptMethodObservable(windowOrWebView, timeout, pathToObject, ...args) {
279 | let send = getSendMethod(windowOrWebView || findTargetFromParentInfo());
280 | if (!send) {
281 | return Observable.throw(new Error(`Unable to find a target for: ${JSON.stringify(window.parentInfo)}`));
282 | }
283 |
284 | if (Array.isArray(pathToObject)) {
285 | pathToObject = pathToObject.join('.');
286 | }
287 |
288 | if (!pathToObject.match(/^[a-zA-Z0-9\._]+$/)) {
289 | return Observable.throw(new Error(`pathToObject must be of the form foo.bar.baz (got ${pathToObject})`));
290 | }
291 |
292 | let toSend = Object.assign({ args, id: getNextId(), path: pathToObject }, getSenderIdentifier());
293 | let ret = listenerForId(toSend.id, timeout);
294 |
295 | d(`Sending: ${JSON.stringify(toSend)}`);
296 | send(requestChannel, toSend);
297 | return ret;
298 | }
299 |
300 |
301 | /**
302 | * Evaluates a JavaScript method on a remote object and returns the result. this
303 | * method can be used to either execute Functions in remote renderers, or return
304 | * values from objects. For example:
305 | *
306 | * let userAgent = await executeJavaScriptMethod(wnd, 'navigator.userAgent');
307 | *
308 | * executeJavaScriptMethod will also be smart enough to recognize when methods
309 | * themselves return Promises and await them:
310 | *
311 | * let fetchResult = await executeJavaScriptMethod('window.fetchHtml', 'https://google.com');
312 | *
313 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code
314 | * in. If this parameter is
315 | * null, this will reference
316 | * the browser process.
317 | * @param {string} pathToObject A path to the object to execute, in dotted
318 | * form i.e. 'document.querySelector'.
319 | * @param {Array} args The arguments to pass to the method
320 | *
321 | * @return {Promise} The result of evaluating the method or
322 | * property. Must be JSON serializable.
323 | */
324 | export function executeJavaScriptMethod(windowOrWebView, pathToObject, ...args) {
325 | return executeJavaScriptMethodObservable(windowOrWebView, 5*1000, pathToObject, ...args).toPromise();
326 | }
327 |
328 | /**
329 | * Creates an object that is a representation of the remote process's 'window'
330 | * object that allows you to remotely invoke methods.
331 | *
332 | * @param {BrowserWindow|WebView} windowOrWebView The child to execute code
333 | * in. If this parameter is
334 | * null, this will reference
335 | * the browser process.
336 | * @param {number} timeout The timeout to use, defaults to 240sec
337 | *
338 | * @return {Object} A Proxy object that will invoke methods remotely.
339 | * Similar to {executeJavaScriptMethod}, methods will return
340 | * a Promise even if the target method returns a normal
341 | * value.
342 | */
343 | export function createProxyForRemote(windowOrWebView, timeout=240*1000) {
344 | return RecursiveProxyHandler.create(rootEvalProxyName, (methodChain, args) => {
345 | let chain = methodChain.splice(1);
346 |
347 | d(`Invoking ${chain.join('.')}(${JSON.stringify(args)})`);
348 | return executeJavaScriptMethodObservable(windowOrWebView, timeout, chain, ...args).toPromise();
349 | });
350 | }
351 |
352 | /**
353 | * Creates an object that is a representation of a module in the main process,
354 | * but with all of its methods Promisified.
355 | *
356 | * @param {String} moduleName The name of the main process module to proxy
357 | * @returns {Object} A Proxy object that will invoke methods remotely.
358 | * All methods will return a Promise.
359 | */
360 | export function createProxyForMainProcessModule(moduleName) {
361 | return createProxyForRemote(null)[requireElectronModule][moduleName];
362 | }
363 |
364 | /**
365 | * Walks the global object hierarchy to resolve the actual object that a dotted
366 | * object path refers to.
367 | *
368 | * @param {string} path A path to the object to execute, in dotted
369 | * form i.e. 'document.querySelector'.
370 | *
371 | * @return {Array} Returns the actual method object and its parent,
372 | * usually a Function and its `this` parameter, as
373 | * `[parent, obj]`
374 | *
375 | * @private
376 | */
377 | function objectAndParentGivenPath(path) {
378 | let obj = global || window;
379 | let parent = obj;
380 |
381 | for (let part of path.split('.')) {
382 | parent = obj;
383 | obj = obj[part];
384 | }
385 |
386 | d(`parent: ${parent}, obj: ${obj}`);
387 | if (typeof(parent) !== 'object') {
388 | throw new Error(`Couldn't access part of the object window.${path}`);
389 | }
390 |
391 | return [parent, obj];
392 | }
393 |
394 | /**
395 | * Given an object path and arguments, actually invokes the method and returns
396 | * the result. This method is run on the target side (i.e. not the one doing
397 | * the invoking). This method tries to figure out the return value of an object
398 | * and do the right thing, including awaiting Promises to get their values.
399 | *
400 | * @param {string} path A path to the object to execute, in dotted
401 | * form i.e. 'document.querySelector'.
402 | * @param {Array} args The arguments to pass to the method
403 | *
404 | * @return {Promise