This repository has been archived by the owner on Jun 17, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 57
/
renderer-require.js
165 lines (142 loc) · 6.61 KB
/
renderer-require.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import path from 'path';
import {fromRemoteWindow} from './remote-event';
import {AsyncSubject} from 'rxjs/AsyncSubject';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/defer';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeAll';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/multicast';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/toPromise';
import {createProxyForRemote, executeJavaScriptMethod, executeJavaScriptMethodObservable, RecursiveProxyHandler} from './execute-js-func';
import './custom-operators';
const d = require('debug')('electron-remote:renderer-require');
const BrowserWindow = process.type === 'renderer' ?
require('electron').remote.BrowserWindow :
require('electron').BrowserWindow;
/**
* Creates a BrowserWindow, requires a module in it, then returns a Proxy
* object that will call into it. You probably want to use {requireTaskPool}
* instead.
*
* @param {string} modulePath The path of the module to include.
* @param {number} timeout The timeout to use, defaults to 240sec
* @return {Object} Returns an Object with a `module` which is a Proxy
* object, and a `unsubscribe` method that will clean up
* the window.
*/
export async function rendererRequireDirect(modulePath, timeout=240*1000) {
let bw = new BrowserWindow({width: 500, height: 500, show: false});
let fullPath = require.resolve(modulePath);
let ready = Observable.merge(
fromRemoteWindow(bw, 'did-finish-load', true),
fromRemoteWindow(bw, 'did-fail-load', true)
.filter(([, errCode]) => errCode !== 0)
.mergeMap(([, , errMsg]) => Observable.throw(new Error(errMsg)))
).take(1).toPromise();
/* Uncomment for debugging!
bw.show();
bw.openDevTools();
*/
let preloadFile = path.join(__dirname, 'renderer-require-preload.html');
bw.loadURL(`file:///${preloadFile}?module=${encodeURIComponent(fullPath)}`);
await ready;
let fail = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure');
if (fail) {
let msg = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure.message');
throw new Error(msg);
}
return {
module: createProxyForRemote(bw).requiredModule,
executeJavaScriptMethod: (chain, ...args) => executeJavaScriptMethodObservable(bw, timeout, chain, ...args).toPromise(),
executeJavaScriptMethodObservable: (chain, ...args) => executeJavaScriptMethodObservable(bw, timeout, chain, ...args),
unsubscribe: () => bw.isDestroyed() ? bw.destroy() : bw.close()
};
}
/**
* requires a module in BrowserWindows that are created/destroyed as-needed, and
* returns a Proxy object that will secretly marshal invocations to other processes
* and marshal back the result. This is the cool method in this library.
*
* Note that since the global context is created / destroyed, you *cannot* rely
* on module state (i.e. global variables) to be consistent
*
* @param {string} modulePath The path to the module. You may have to
* `require.resolve` it.
* @param {Number} maxConcurrency The maximum number of concurrent processes
* to run. Defaults to 4.
* @param {Number} idleTimeout The amount of time to wait before closing
* a BrowserWindow as idle, in ms
* @param {Number} methodTimeout The amount of time to wait before a method
* fails, in ms
*
* @return {Proxy} An ES6 Proxy object representing the module.
*/
export function requireTaskPool(modulePath, maxConcurrency=4, idleTimeout=5*1000, methodTimeout=240*1000) {
return new RendererTaskpoolItem(modulePath, maxConcurrency, idleTimeout, methodTimeout).moduleProxy;
}
/**
* This class implements the scheduling logic for queuing and dispatching method
* invocations to various background windows. It is complicated. But in like,
* a cool way.
*/
class RendererTaskpoolItem {
constructor(modulePath, maxConcurrency, idleTimeout, methodTimeout) {
const freeWindowList = [];
const invocationQueue = new Subject();
const completionQueue = new Subject();
// This method will find a window that is currently idle or if it doesn't
// exist, create one.
const getOrCreateWindow = () => {
let item = freeWindowList.pop();
if (item) return Observable.of(item);
return Observable.fromPromise(rendererRequireDirect(modulePath, methodTimeout));
};
// Here, we set up a pipeline that maps a stream of invocations (i.e.
// something we can pass to executeJavaScriptMethod) => stream of Future
// Results from various windows => Stream of completed results, for which we
// throw the Window that completed the result back onto the free window stack.
invocationQueue
.map(({chain, args, retval}) => Observable.defer(() => {
return getOrCreateWindow()
.mergeMap((wnd) => {
d(`Actually invoking ${chain.join('.')}(${JSON.stringify(args)})`);
let ret = wnd.executeJavaScriptMethodObservable(chain, ...args);
ret.multicast(retval).connect();
return ret.map(() => wnd).catch(() => Observable.of(wnd));
});
}))
.mergeAll(maxConcurrency)
.subscribe((wnd) => {
if (!wnd || !wnd.unsubscribe) throw new Error("Bogus!");
freeWindowList.push(wnd);
completionQueue.next(true);
});
// Here, we create a version of RecursiveProxyHandler that will turn method
// invocations into something we can push onto our invocationQueue pipeline.
// This is the object that ends up being returned to the caller of
// requireTaskPool.
this.moduleProxy = RecursiveProxyHandler.create('__removeme__', (methodChain, args) => {
let chain = methodChain.splice(1);
d(`Queuing ${chain.join('.')}(${JSON.stringify(args)})`);
let retval = new AsyncSubject();
invocationQueue.next({ chain: ['requiredModule'].concat(chain), args, retval });
return retval.toPromise();
});
// If we haven't received any invocations within a certain idle timeout
// period, burn all of our BrowserWindow instances
completionQueue.guaranteedThrottle(idleTimeout).subscribe(() => {
d(`Freeing ${freeWindowList.length} taskpool processes`);
while (freeWindowList.length > 0) {
let wnd = freeWindowList.pop();
if (wnd) wnd.unsubscribe();
}
});
}
}