@scrypted/server 0.114.0 → 0.115.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fetch/http-fetch.js +6 -27
- package/dist/fetch/http-fetch.js.map +1 -1
- package/dist/fetch/index.d.ts +5 -2
- package/dist/fetch/index.js +6 -26
- package/dist/fetch/index.js.map +1 -1
- package/dist/listen-zero.d.ts +2 -2
- package/dist/listen-zero.js +1 -1
- package/dist/listen-zero.js.map +1 -1
- package/dist/plugin/plugin-console.js +2 -2
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-npm-dependencies.js +5 -1
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +22 -17
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +0 -1
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +1 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/rpc-peer-eval.d.ts +4 -0
- package/dist/rpc-peer-eval.js +25 -0
- package/dist/rpc-peer-eval.js.map +1 -0
- package/dist/rpc-serializer.js.map +1 -1
- package/dist/rpc.d.ts +4 -4
- package/dist/rpc.js +6 -23
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +7 -5
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-server-main.js +14 -0
- package/dist/scrypted-server-main.js.map +1 -1
- package/package.json +12 -12
- package/python/plugin_remote.py +39 -11
- package/python/rpc.py +10 -10
- package/src/fetch/http-fetch.ts +7 -32
- package/src/fetch/index.ts +10 -33
- package/src/listen-zero.ts +3 -3
- package/src/plugin/plugin-console.ts +2 -2
- package/src/plugin/plugin-npm-dependencies.ts +5 -1
- package/src/plugin/plugin-remote-worker.ts +25 -17
- package/src/plugin/plugin-remote.ts +1 -2
- package/src/plugin/plugin-repl.ts +1 -1
- package/src/rpc-peer-eval.ts +27 -0
- package/src/rpc-serializer.ts +2 -2
- package/src/rpc.ts +16 -32
- package/src/runtime.ts +9 -5
- package/src/scrypted-server-main.ts +16 -0
package/package.json
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"name": "@scrypted/server",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.115.1",
|
4
4
|
"description": "",
|
5
5
|
"dependencies": {
|
6
6
|
"@mapbox/node-pre-gyp": "^1.0.11",
|
7
7
|
"@scrypted/ffmpeg-static": "^6.1.0-build1",
|
8
|
-
"@scrypted/node-pty": "^1.0.
|
9
|
-
"@scrypted/types": "^0.3.
|
8
|
+
"@scrypted/node-pty": "^1.0.18",
|
9
|
+
"@scrypted/types": "^0.3.43",
|
10
10
|
"adm-zip": "^0.5.14",
|
11
11
|
"body-parser": "^1.20.2",
|
12
12
|
"cookie-parser": "^1.4.6",
|
13
13
|
"dotenv": "^16.4.5",
|
14
|
-
"engine.io": "^6.
|
14
|
+
"engine.io": "^6.6.0",
|
15
15
|
"express": "^4.19.2",
|
16
16
|
"follow-redirects": "^1.15.6",
|
17
17
|
"http-auth": "^4.2.0",
|
@@ -21,17 +21,17 @@
|
|
21
21
|
"nan": "^2.20.0",
|
22
22
|
"node-dijkstra": "^2.5.0",
|
23
23
|
"node-forge": "^1.3.1",
|
24
|
-
"node-gyp": "^10.
|
25
|
-
"py": "npm:@bjia56/portable-python@^0.1.
|
24
|
+
"node-gyp": "^10.2.0",
|
25
|
+
"py": "npm:@bjia56/portable-python@^0.1.54",
|
26
26
|
"router": "^1.3.8",
|
27
|
-
"semver": "^7.6.
|
27
|
+
"semver": "^7.6.3",
|
28
28
|
"sharp": "^0.33.4",
|
29
29
|
"source-map-support": "^0.5.21",
|
30
|
-
"tar": "^7.4.
|
30
|
+
"tar": "^7.4.3",
|
31
31
|
"tslib": "^2.6.3",
|
32
|
-
"typescript": "^5.5.
|
32
|
+
"typescript": "^5.5.4",
|
33
33
|
"whatwg-mimetype": "^4.0.0",
|
34
|
-
"ws": "^8.
|
34
|
+
"ws": "^8.18.0"
|
35
35
|
},
|
36
36
|
"devDependencies": {
|
37
37
|
"@types/adm-zip": "^0.5.5",
|
@@ -40,13 +40,13 @@
|
|
40
40
|
"@types/follow-redirects": "^1.14.4",
|
41
41
|
"@types/http-auth": "^4.1.4",
|
42
42
|
"@types/ip": "^1.1.3",
|
43
|
-
"@types/lodash": "^4.17.
|
43
|
+
"@types/lodash": "^4.17.7",
|
44
44
|
"@types/node-dijkstra": "^2.5.6",
|
45
45
|
"@types/node-forge": "^1.3.11",
|
46
46
|
"@types/semver": "^7.5.8",
|
47
47
|
"@types/source-map-support": "^0.5.10",
|
48
48
|
"@types/whatwg-mimetype": "^3.0.2",
|
49
|
-
"@types/ws": "^8.5.
|
49
|
+
"@types/ws": "^8.5.11"
|
50
50
|
},
|
51
51
|
"bin": {
|
52
52
|
"scrypted-serve": "bin/scrypted-serve"
|
package/python/plugin_remote.py
CHANGED
@@ -29,6 +29,7 @@ from plugin_pip import install_with_pip, need_requirements, remove_pip_dirs
|
|
29
29
|
from scrypted_python.scrypted_sdk import PluginFork, ScryptedStatic
|
30
30
|
from scrypted_python.scrypted_sdk.types import (Device, DeviceManifest,
|
31
31
|
EventDetails,
|
32
|
+
ScryptedInterface,
|
32
33
|
ScryptedInterfaceMethods,
|
33
34
|
ScryptedInterfaceProperty,
|
34
35
|
Storage)
|
@@ -139,6 +140,19 @@ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
|
|
139
140
|
state = self.systemState.get(check, None)
|
140
141
|
if not state:
|
141
142
|
continue
|
143
|
+
checkInterfaces = state.get('interfaces', None)
|
144
|
+
if not checkInterfaces:
|
145
|
+
continue
|
146
|
+
interfaces = checkInterfaces.get('value', [])
|
147
|
+
if ScryptedInterface.ScryptedPlugin.value in interfaces:
|
148
|
+
checkPluginId = state.get('pluginId', None)
|
149
|
+
if not checkPluginId:
|
150
|
+
continue
|
151
|
+
pluginId = checkPluginId.get('value', None)
|
152
|
+
if not pluginId:
|
153
|
+
continue
|
154
|
+
if pluginId == name:
|
155
|
+
return self.getDeviceById(check)
|
142
156
|
checkName = state.get('name', None)
|
143
157
|
if not checkName:
|
144
158
|
continue
|
@@ -260,6 +274,13 @@ class DeviceState(scrypted_python.scrypted_sdk.types.DeviceState):
|
|
260
274
|
self.systemManager.api.setState(self.nativeId, property, value)
|
261
275
|
|
262
276
|
|
277
|
+
class WritableDeviceState(scrypted_python.scrypted_sdk.types.WritableDeviceState):
|
278
|
+
|
279
|
+
def __init__(self, id, setState) -> None:
|
280
|
+
self.id = id
|
281
|
+
self.setState = setState
|
282
|
+
|
283
|
+
|
263
284
|
class DeviceStorage(Storage):
|
264
285
|
id: str
|
265
286
|
nativeId: str
|
@@ -390,12 +411,17 @@ class PluginRemote:
|
|
390
411
|
|
391
412
|
def computeClusterObjectHash(o: ClusterObject) -> str:
|
392
413
|
m = hashlib.sha256()
|
393
|
-
m.update(bytes(f"{o['id']}{o['port']}{o.get('sourcePort'
|
414
|
+
m.update(bytes(f"{o['id']}{o['port']}{o.get('sourcePort') or ''}{o['proxyId']}{clusterSecret}", 'utf8'))
|
394
415
|
return base64.b64encode(m.digest()).decode('utf-8')
|
395
416
|
|
396
|
-
def onProxySerialization(value: Any,
|
417
|
+
def onProxySerialization(value: Any, sourcePeerPort: int = None):
|
397
418
|
properties: dict = rpc.RpcPeer.prepareProxyProperties(value) or {}
|
398
419
|
clusterEntry = properties.get('__cluster', None)
|
420
|
+
proxyId: str = (clusterEntry and clusterEntry.get('proxyId', None)) or rpc.RpcPeer.generateId()
|
421
|
+
|
422
|
+
if clusterEntry and clusterPort == clusterEntry['port'] and sourcePeerPort != clusterEntry.get('sourcePort', None):
|
423
|
+
clusterEntry = None
|
424
|
+
|
399
425
|
if not properties.get('__cluster', None):
|
400
426
|
clusterEntry: ClusterObject = {
|
401
427
|
'id': clusterId,
|
@@ -406,7 +432,7 @@ class PluginRemote:
|
|
406
432
|
clusterEntry['sha256'] = computeClusterObjectHash(clusterEntry)
|
407
433
|
properties['__cluster'] = clusterEntry
|
408
434
|
|
409
|
-
return properties
|
435
|
+
return proxyId, properties
|
410
436
|
|
411
437
|
self.peer.onProxySerialization = onProxySerialization
|
412
438
|
|
@@ -423,8 +449,8 @@ class PluginRemote:
|
|
423
449
|
rpcTransport = rpc_reader.RpcStreamTransport(reader, writer)
|
424
450
|
peer: rpc.RpcPeer
|
425
451
|
peer, peerReadLoop = await rpc_reader.prepare_peer_readloop(self.loop, rpcTransport)
|
426
|
-
peer.onProxySerialization = lambda value
|
427
|
-
value,
|
452
|
+
peer.onProxySerialization = lambda value: onProxySerialization(
|
453
|
+
value, clusterPeerPort)
|
428
454
|
future: asyncio.Future[rpc.RpcPeer] = asyncio.Future()
|
429
455
|
future.set_result(peer)
|
430
456
|
clusterPeers[clusterPeerPort] = future
|
@@ -458,9 +484,8 @@ class PluginRemote:
|
|
458
484
|
rpcTransport = rpc_reader.RpcStreamTransport(
|
459
485
|
reader, writer)
|
460
486
|
clusterPeer, peerReadLoop = await rpc_reader.prepare_peer_readloop(self.loop, rpcTransport)
|
461
|
-
clusterPeer.
|
462
|
-
|
463
|
-
value, proxyId, clusterPeerPort)
|
487
|
+
clusterPeer.onProxySerialization = lambda value: onProxySerialization(
|
488
|
+
value, clusterPeerPort)
|
464
489
|
|
465
490
|
async def run_loop():
|
466
491
|
try:
|
@@ -494,8 +519,11 @@ class PluginRemote:
|
|
494
519
|
|
495
520
|
try:
|
496
521
|
clusterPeer = await clusterPeerPromise
|
497
|
-
|
498
|
-
|
522
|
+
weakref = clusterPeer.remoteWeakProxies.get(proxyId, None)
|
523
|
+
existing = weakref() if weakref else None
|
524
|
+
if existing:
|
525
|
+
return existing
|
526
|
+
|
499
527
|
peerConnectRPCObject = clusterPeer.tags.get('connectRPCObject')
|
500
528
|
if not peerConnectRPCObject:
|
501
529
|
peerConnectRPCObject = await clusterPeer.getParam('connectRPCObject')
|
@@ -743,7 +771,7 @@ class PluginRemote:
|
|
743
771
|
pass
|
744
772
|
|
745
773
|
async def createDeviceState(self, id, setState):
|
746
|
-
|
774
|
+
return WritableDeviceState(id, setState)
|
747
775
|
|
748
776
|
async def getServicePort(self, name):
|
749
777
|
if name == "repl":
|
package/python/rpc.py
CHANGED
@@ -126,7 +126,7 @@ class RpcPeer:
|
|
126
126
|
self.pendingResults: Mapping[str, Future] = {}
|
127
127
|
self.remoteWeakProxies: Mapping[str, any] = {}
|
128
128
|
self.nameDeserializerMap: Mapping[str, RpcSerializer] = {}
|
129
|
-
self.onProxySerialization: Callable[[Any, str], Any] = None
|
129
|
+
self.onProxySerialization: Callable[[Any, str], tuple[str, Any]] = None
|
130
130
|
self.killed = False
|
131
131
|
self.tags = {}
|
132
132
|
|
@@ -274,7 +274,7 @@ class RpcPeer:
|
|
274
274
|
|
275
275
|
proxiedEntry = self.localProxied.get(value, None)
|
276
276
|
if proxiedEntry:
|
277
|
-
proxiedEntry['finalizerId'] =
|
277
|
+
proxiedEntry['finalizerId'] = RpcPeer.generateId()
|
278
278
|
ret = {
|
279
279
|
'__remote_proxy_id': proxiedEntry['id'],
|
280
280
|
'__remote_proxy_finalizer_id': proxiedEntry['finalizerId'],
|
@@ -292,7 +292,12 @@ class RpcPeer:
|
|
292
292
|
}
|
293
293
|
return ret
|
294
294
|
|
295
|
-
|
295
|
+
if self.onProxySerialization:
|
296
|
+
proxyId, __remote_proxy_props = self.onProxySerialization(value)
|
297
|
+
else:
|
298
|
+
__remote_proxy_props = RpcPeer.prepareProxyProperties(value)
|
299
|
+
proxyId = RpcPeer.generateId()
|
300
|
+
|
296
301
|
proxiedEntry = {
|
297
302
|
'id': proxyId,
|
298
303
|
'finalizerId': proxyId,
|
@@ -300,11 +305,6 @@ class RpcPeer:
|
|
300
305
|
self.localProxied[value] = proxiedEntry
|
301
306
|
self.localProxyMap[proxyId] = value
|
302
307
|
|
303
|
-
if self.onProxySerialization:
|
304
|
-
__remote_proxy_props = self.onProxySerialization(value, proxyId)
|
305
|
-
else:
|
306
|
-
__remote_proxy_props = RpcPeer.prepareProxyProperties(value)
|
307
|
-
|
308
308
|
ret = {
|
309
309
|
'__remote_proxy_id': proxyId,
|
310
310
|
'__remote_proxy_finalizer_id': proxyId,
|
@@ -491,7 +491,7 @@ class RpcPeer:
|
|
491
491
|
|
492
492
|
randomDigits = string.ascii_uppercase + string.ascii_lowercase + string.digits
|
493
493
|
|
494
|
-
def generateId(
|
494
|
+
def generateId():
|
495
495
|
return ''.join(random.choices(RpcPeer.randomDigits, k=8))
|
496
496
|
|
497
497
|
async def createPendingResult(self, cb: Callable[[str, Callable[[Exception], None]], None]):
|
@@ -500,7 +500,7 @@ class RpcPeer:
|
|
500
500
|
future.set_exception(RPCResultError(None, 'RpcPeer has been killed (createPendingResult)'))
|
501
501
|
return future
|
502
502
|
|
503
|
-
id =
|
503
|
+
id = RpcPeer.generateId()
|
504
504
|
self.pendingResults[id] = future
|
505
505
|
await cb(id, lambda e: future.set_exception(RPCResultError(e, None)))
|
506
506
|
return await future
|
package/src/fetch/http-fetch.ts
CHANGED
@@ -89,11 +89,11 @@ export async function httpFetch<T extends HttpFetchOptions<Readable>>(options: T
|
|
89
89
|
controller = new AbortController();
|
90
90
|
timeout = setTimeout(() => controller.abort(), options.timeout);
|
91
91
|
|
92
|
-
options.signal?.addEventListener('abort', () => controller.abort(
|
92
|
+
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
|
93
93
|
}
|
94
94
|
|
95
95
|
const signal = controller?.signal || options.signal;
|
96
|
-
signal?.addEventListener('abort', () => request.destroy(new Error('abort')));
|
96
|
+
signal?.addEventListener('abort', () => request.destroy(new Error(options.signal?.reason || 'abort')));
|
97
97
|
|
98
98
|
const nodeHeaders: Record<string, string[]> = {};
|
99
99
|
for (const [k, v] of headers) {
|
@@ -122,9 +122,12 @@ export async function httpFetch<T extends HttpFetchOptions<Readable>>(options: T
|
|
122
122
|
try {
|
123
123
|
const [response] = await once(request, 'response') as [IncomingMessage];
|
124
124
|
|
125
|
-
|
125
|
+
|
126
|
+
if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
|
126
127
|
try {
|
127
|
-
checkStatus
|
128
|
+
const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
|
129
|
+
if (!checker(response.statusCode))
|
130
|
+
throw new Error(`http response statusCode ${response.statusCode}`);
|
128
131
|
}
|
129
132
|
catch (e) {
|
130
133
|
readMessageBuffer(response).catch(() => { });
|
@@ -150,31 +153,3 @@ export async function httpFetch<T extends HttpFetchOptions<Readable>>(options: T
|
|
150
153
|
}
|
151
154
|
}
|
152
155
|
|
153
|
-
function ensureType<T>(v: T) {
|
154
|
-
}
|
155
|
-
|
156
|
-
async function test() {
|
157
|
-
const a = await httpFetch({
|
158
|
-
url: 'http://example.com',
|
159
|
-
});
|
160
|
-
|
161
|
-
ensureType<Buffer>(a.body);
|
162
|
-
|
163
|
-
const b = await httpFetch({
|
164
|
-
url: 'http://example.com',
|
165
|
-
responseType: 'json',
|
166
|
-
});
|
167
|
-
ensureType<any>(b.body);
|
168
|
-
|
169
|
-
const c = await httpFetch({
|
170
|
-
url: 'http://example.com',
|
171
|
-
responseType: 'readable',
|
172
|
-
});
|
173
|
-
ensureType<IncomingMessage>(c.body);
|
174
|
-
|
175
|
-
const d = await httpFetch({
|
176
|
-
url: 'http://example.com',
|
177
|
-
responseType: 'buffer',
|
178
|
-
});
|
179
|
-
ensureType<Buffer>(d.body);
|
180
|
-
}
|
package/src/fetch/index.ts
CHANGED
@@ -7,7 +7,10 @@ export interface HttpFetchOptionsBase<B> {
|
|
7
7
|
signal?: AbortSignal,
|
8
8
|
timeout?: number;
|
9
9
|
rejectUnauthorized?: boolean;
|
10
|
-
|
10
|
+
/**
|
11
|
+
* Checks the status code. Defaults to true.
|
12
|
+
*/
|
13
|
+
checkStatusCode?: boolean | ((statusCode: number) => boolean);
|
11
14
|
body?: B | string | ArrayBufferView | any;
|
12
15
|
withCredentials?: boolean;
|
13
16
|
}
|
@@ -40,6 +43,7 @@ export function fetchStatusCodeOk(statusCode: number) {
|
|
40
43
|
export function checkStatus(statusCode: number) {
|
41
44
|
if (!fetchStatusCodeOk(statusCode))
|
42
45
|
throw new Error(`http response statusCode ${statusCode}`);
|
46
|
+
return true;
|
43
47
|
}
|
44
48
|
|
45
49
|
export function getFetchMethod(options: HttpFetchOptions<any>) {
|
@@ -177,7 +181,7 @@ export async function domFetch<T extends HttpFetchOptions<BodyInit>>(options: T)
|
|
177
181
|
controller = new AbortController();
|
178
182
|
timeout = setTimeout(() => controller.abort(), options.timeout);
|
179
183
|
|
180
|
-
options.signal?.addEventListener('abort', () => controller.abort(
|
184
|
+
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
|
181
185
|
}
|
182
186
|
|
183
187
|
try {
|
@@ -190,9 +194,11 @@ export async function domFetch<T extends HttpFetchOptions<BodyInit>>(options: T)
|
|
190
194
|
body,
|
191
195
|
});
|
192
196
|
|
193
|
-
if (
|
197
|
+
if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
|
194
198
|
try {
|
195
|
-
checkStatus
|
199
|
+
const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
|
200
|
+
if (!checker(response.status))
|
201
|
+
throw new Error(`http response statusCode ${response.status}`);
|
196
202
|
}
|
197
203
|
catch (e) {
|
198
204
|
response.arrayBuffer().catch(() => { });
|
@@ -210,32 +216,3 @@ export async function domFetch<T extends HttpFetchOptions<BodyInit>>(options: T)
|
|
210
216
|
clearTimeout(timeout);
|
211
217
|
}
|
212
218
|
}
|
213
|
-
|
214
|
-
function ensureType<T>(v: T) {
|
215
|
-
}
|
216
|
-
|
217
|
-
async function test() {
|
218
|
-
const a = await domFetch({
|
219
|
-
url: 'http://example.com',
|
220
|
-
});
|
221
|
-
|
222
|
-
ensureType<Buffer>(a.body);
|
223
|
-
|
224
|
-
const b = await domFetch({
|
225
|
-
url: 'http://example.com',
|
226
|
-
responseType: 'json',
|
227
|
-
});
|
228
|
-
ensureType<any>(b.body);
|
229
|
-
|
230
|
-
const c = await domFetch({
|
231
|
-
url: 'http://example.com',
|
232
|
-
responseType: 'readable',
|
233
|
-
});
|
234
|
-
ensureType<Response>(c.body);
|
235
|
-
|
236
|
-
const d = await domFetch({
|
237
|
-
url: 'http://example.com',
|
238
|
-
responseType: 'buffer',
|
239
|
-
});
|
240
|
-
ensureType<Buffer>(d.body);
|
241
|
-
}
|
package/src/listen-zero.ts
CHANGED
@@ -7,13 +7,13 @@ export class ListenZeroSingleClientTimeoutError extends Error {
|
|
7
7
|
}
|
8
8
|
}
|
9
9
|
|
10
|
-
export async function listenZero(server: net.Server, hostname
|
11
|
-
server.listen(0, hostname);
|
10
|
+
export async function listenZero(server: net.Server, hostname: string) {
|
11
|
+
server.listen(0, hostname || '127.0.0.1');
|
12
12
|
await once(server, 'listening');
|
13
13
|
return (server.address() as net.AddressInfo).port;
|
14
14
|
}
|
15
15
|
|
16
|
-
export async function listenZeroSingleClient(hostname
|
16
|
+
export async function listenZeroSingleClient(hostname: string, options?: net.ServerOpts, listenTimeout = 30000) {
|
17
17
|
const server = new net.Server(options);
|
18
18
|
const port = await listenZero(server, hostname);
|
19
19
|
|
@@ -295,8 +295,8 @@ export async function createConsoleServer(remoteStdout: Readable, remoteStderr:
|
|
295
295
|
socket.once('error', cleanup);
|
296
296
|
socket.once('end', cleanup);
|
297
297
|
});
|
298
|
-
const readPort = await listenZero(readServer);
|
299
|
-
const writePort = await listenZero(writeServer);
|
298
|
+
const readPort = await listenZero(readServer, '127.0.0.1');
|
299
|
+
const writePort = await listenZero(writeServer, '127.0.0.1');
|
300
300
|
|
301
301
|
return {
|
302
302
|
clear(nativeId: ScryptedNativeId) {
|
@@ -9,8 +9,12 @@ import { ensurePluginVolume } from "./plugin-volume";
|
|
9
9
|
|
10
10
|
export function defaultNpmExec(args: string[], options: child_process.SpawnOptions) {
|
11
11
|
let npm = 'npm';
|
12
|
-
if (os.platform() === 'win32')
|
12
|
+
if (os.platform() === 'win32') {
|
13
13
|
npm += '.cmd';
|
14
|
+
// wrap each argument in a quote to handle spaces in paths
|
15
|
+
// https://github.com/nodejs/node/issues/38490#issuecomment-927330248
|
16
|
+
args = args.map(arg => '"' + arg + '"');
|
17
|
+
}
|
14
18
|
const cp = child_process.spawn(npm, args, options);
|
15
19
|
return cp;
|
16
20
|
}
|
@@ -9,6 +9,7 @@ import { computeClusterObjectHash } from '../cluster/cluster-hash';
|
|
9
9
|
import { ClusterObject, ConnectRPCObject } from '../cluster/connect-rpc-object';
|
10
10
|
import { listenZero } from '../listen-zero';
|
11
11
|
import { RpcMessage, RpcPeer } from '../rpc';
|
12
|
+
import { evalLocal } from '../rpc-peer-eval';
|
12
13
|
import { createDuplexRpcPeer } from '../rpc-serializer';
|
13
14
|
import { MediaManagerImpl } from './media';
|
14
15
|
import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
@@ -95,10 +96,19 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
95
96
|
const { clusterId, clusterSecret, zipHash } = zipOptions;
|
96
97
|
const { zipFile, unzippedPath } = await prepareZip(getPluginVolume(pluginId), zipHash, getZip);
|
97
98
|
|
98
|
-
const onProxySerialization = (value: any,
|
99
|
+
const onProxySerialization = (value: any, sourcePeerPort?: number) => {
|
99
100
|
const properties = RpcPeer.prepareProxyProperties(value) || {};
|
100
101
|
let clusterEntry: ClusterObject = properties.__cluster;
|
101
102
|
|
103
|
+
// ensure globally stable proxyIds.
|
104
|
+
const proxyId = clusterEntry?.proxyId || RpcPeer.generateId();
|
105
|
+
|
106
|
+
// if the cluster entry already exists, check if it belongs to this node.
|
107
|
+
// if it belongs to this node, the entry must also be for this peer.
|
108
|
+
// relying on the liveness/gc of a different peer may cause race conditions.
|
109
|
+
if (clusterEntry && clusterPort === clusterEntry.port && sourcePeerPort !== clusterEntry.sourcePort)
|
110
|
+
clusterEntry = undefined;
|
111
|
+
|
102
112
|
// set the cluster identity if it does not exist.
|
103
113
|
if (!clusterEntry) {
|
104
114
|
clusterEntry = {
|
@@ -111,12 +121,11 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
111
121
|
clusterEntry.sha256 = computeClusterObjectHash(clusterEntry, clusterSecret);
|
112
122
|
properties.__cluster = clusterEntry;
|
113
123
|
}
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
return properties;
|
124
|
+
|
125
|
+
return {
|
126
|
+
proxyId,
|
127
|
+
properties,
|
128
|
+
};
|
120
129
|
}
|
121
130
|
peer.onProxySerialization = onProxySerialization;
|
122
131
|
|
@@ -133,7 +142,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
133
142
|
const clusterRpcServer = net.createServer(client => {
|
134
143
|
const clusterPeer = createDuplexRpcPeer(peer.selfName, 'cluster-client', client, client);
|
135
144
|
const clusterPeerPort = client.remotePort;
|
136
|
-
clusterPeer.onProxySerialization = (value
|
145
|
+
clusterPeer.onProxySerialization = (value) => onProxySerialization(value, clusterPeerPort);
|
137
146
|
clusterPeers.set(clusterPeerPort, Promise.resolve(clusterPeer));
|
138
147
|
startPluginRemoteOptions?.onClusterPeer?.(clusterPeer);
|
139
148
|
const connectRPCObject: ConnectRPCObject = async (o) => {
|
@@ -164,8 +173,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
164
173
|
const sourcePort = (socket.address() as net.AddressInfo).port;
|
165
174
|
|
166
175
|
const clusterPeer = createDuplexRpcPeer(peer.selfName, 'cluster-server', socket, socket);
|
167
|
-
clusterPeer.
|
168
|
-
clusterPeer.onProxySerialization = (value, proxyId) => onProxySerialization(value, proxyId, sourcePort);
|
176
|
+
clusterPeer.onProxySerialization = (value) => onProxySerialization(value, sourcePort);
|
169
177
|
return clusterPeer;
|
170
178
|
}
|
171
179
|
catch (e) {
|
@@ -192,10 +200,10 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
192
200
|
try {
|
193
201
|
const clusterPeerPromise = ensureClusterPeer(port);
|
194
202
|
const clusterPeer = await clusterPeerPromise;
|
195
|
-
//
|
196
|
-
|
197
|
-
if (
|
198
|
-
return
|
203
|
+
// the proxy id is guaranteed to be unique in all peers in a cluster
|
204
|
+
const existing = clusterPeer.remoteWeakProxies[proxyId]?.deref();
|
205
|
+
if (existing)
|
206
|
+
return existing;
|
199
207
|
let peerConnectRPCObject: ConnectRPCObject = clusterPeer.tags['connectRPCObject'];
|
200
208
|
if (!peerConnectRPCObject) {
|
201
209
|
peerConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
|
@@ -269,11 +277,11 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
269
277
|
|
270
278
|
process.on('uncaughtException', e => {
|
271
279
|
getPluginConsole().error('uncaughtException', e);
|
272
|
-
scrypted.log.e('uncaughtException ' + e?.toString());
|
280
|
+
scrypted.log.e('uncaughtException ' + (e.stack || e?.toString()));
|
273
281
|
});
|
274
282
|
process.on('unhandledRejection', e => {
|
275
283
|
getPluginConsole().error('unhandledRejection', e);
|
276
|
-
scrypted.log.e('unhandledRejection ' + e?.toString());
|
284
|
+
scrypted.log.e('unhandledRejection ' + ((e as Error).stack || e?.toString()));
|
277
285
|
});
|
278
286
|
|
279
287
|
installSourceMapSupport({
|
@@ -376,7 +384,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
|
376
384
|
|
377
385
|
try {
|
378
386
|
const filename = zipOptions?.debug ? '/plugin/main.nodejs.js' : `/${pluginId}/main.nodejs.js`;
|
379
|
-
|
387
|
+
evalLocal(peer, script, filename, params);
|
380
388
|
|
381
389
|
if (zipOptions?.fork) {
|
382
390
|
// pluginConsole?.log('plugin forked');
|
@@ -179,7 +179,6 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
|
|
179
179
|
|
180
180
|
set?(target: any, p: PropertyKey, value: any, receiver: any) {
|
181
181
|
checkProperty(p.toString(), value);
|
182
|
-
const now = Date.now();
|
183
182
|
this.deviceManager.systemManager.state[this.id][p as string] = {
|
184
183
|
value,
|
185
184
|
};
|
@@ -446,7 +445,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
|
|
446
445
|
return remote;
|
447
446
|
}
|
448
447
|
catch (e) {
|
449
|
-
throw new RPCResultError(peer, 'error while retrieving PluginRemote', e);
|
448
|
+
throw new RPCResultError(peer, 'error while retrieving PluginRemote', e as Error);
|
450
449
|
}
|
451
450
|
}
|
452
451
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import type { CompileFunctionOptions } from 'vm';
|
2
|
+
import { RpcPeer } from "./rpc";
|
3
|
+
|
4
|
+
type CompileFunction = (code: string, params?: ReadonlyArray<string>, options?: CompileFunctionOptions) => Function;
|
5
|
+
|
6
|
+
function compileFunction(code: string, params?: ReadonlyArray<string>, options?: CompileFunctionOptions): any {
|
7
|
+
params = params || [];
|
8
|
+
const f = `(function(${params.join(',')}) {;${code};})`;
|
9
|
+
return eval(f);
|
10
|
+
}
|
11
|
+
|
12
|
+
export function evalLocal<T>(peer: RpcPeer, script: string, filename?: string, coercedParams?: { [name: string]: any }): T {
|
13
|
+
const params = Object.assign({}, peer.params, coercedParams);
|
14
|
+
let compile: CompileFunction;
|
15
|
+
try {
|
16
|
+
// prevent bundlers from trying to include non-existent vm module.
|
17
|
+
compile = module[`require`]('vm').compileFunction;
|
18
|
+
}
|
19
|
+
catch (e) {
|
20
|
+
compile = compileFunction;
|
21
|
+
}
|
22
|
+
const f = compile(script, Object.keys(params), {
|
23
|
+
filename,
|
24
|
+
});
|
25
|
+
const value = f(...Object.values(params));
|
26
|
+
return value;
|
27
|
+
}
|
package/src/rpc-serializer.ts
CHANGED
@@ -10,7 +10,7 @@ export function createDuplexRpcPeer(selfName: string, peerName: string, readable
|
|
10
10
|
serializer.sendMessage(message, reject, serializationContext);
|
11
11
|
}
|
12
12
|
catch (e) {
|
13
|
-
reject?.(e);
|
13
|
+
reject?.(e as Error);
|
14
14
|
readable.destroy();
|
15
15
|
}
|
16
16
|
});
|
@@ -165,7 +165,7 @@ export function createRpcDuplexSerializer(writable: {
|
|
165
165
|
serializer.onMessageFinish(message);
|
166
166
|
}
|
167
167
|
catch (e) {
|
168
|
-
serializer.kill('message parse failure ' + e.message);
|
168
|
+
serializer.kill('message parse failure ' + (e as Error).message);
|
169
169
|
}
|
170
170
|
}
|
171
171
|
else {
|