@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.
Files changed (45) hide show
  1. package/dist/fetch/http-fetch.js +6 -27
  2. package/dist/fetch/http-fetch.js.map +1 -1
  3. package/dist/fetch/index.d.ts +5 -2
  4. package/dist/fetch/index.js +6 -26
  5. package/dist/fetch/index.js.map +1 -1
  6. package/dist/listen-zero.d.ts +2 -2
  7. package/dist/listen-zero.js +1 -1
  8. package/dist/listen-zero.js.map +1 -1
  9. package/dist/plugin/plugin-console.js +2 -2
  10. package/dist/plugin/plugin-console.js.map +1 -1
  11. package/dist/plugin/plugin-npm-dependencies.js +5 -1
  12. package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
  13. package/dist/plugin/plugin-remote-worker.js +22 -17
  14. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  15. package/dist/plugin/plugin-remote.js +0 -1
  16. package/dist/plugin/plugin-remote.js.map +1 -1
  17. package/dist/plugin/plugin-repl.js +1 -1
  18. package/dist/plugin/plugin-repl.js.map +1 -1
  19. package/dist/rpc-peer-eval.d.ts +4 -0
  20. package/dist/rpc-peer-eval.js +25 -0
  21. package/dist/rpc-peer-eval.js.map +1 -0
  22. package/dist/rpc-serializer.js.map +1 -1
  23. package/dist/rpc.d.ts +4 -4
  24. package/dist/rpc.js +6 -23
  25. package/dist/rpc.js.map +1 -1
  26. package/dist/runtime.js +7 -5
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/scrypted-server-main.js +14 -0
  29. package/dist/scrypted-server-main.js.map +1 -1
  30. package/package.json +12 -12
  31. package/python/plugin_remote.py +39 -11
  32. package/python/rpc.py +10 -10
  33. package/src/fetch/http-fetch.ts +7 -32
  34. package/src/fetch/index.ts +10 -33
  35. package/src/listen-zero.ts +3 -3
  36. package/src/plugin/plugin-console.ts +2 -2
  37. package/src/plugin/plugin-npm-dependencies.ts +5 -1
  38. package/src/plugin/plugin-remote-worker.ts +25 -17
  39. package/src/plugin/plugin-remote.ts +1 -2
  40. package/src/plugin/plugin-repl.ts +1 -1
  41. package/src/rpc-peer-eval.ts +27 -0
  42. package/src/rpc-serializer.ts +2 -2
  43. package/src/rpc.ts +16 -32
  44. package/src/runtime.ts +9 -5
  45. 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.114.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.14",
9
- "@scrypted/types": "^0.3.30",
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.5.4",
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.1.0",
25
- "py": "npm:@bjia56/portable-python@^0.1.52",
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.2",
27
+ "semver": "^7.6.3",
28
28
  "sharp": "^0.33.4",
29
29
  "source-map-support": "^0.5.21",
30
- "tar": "^7.4.0",
30
+ "tar": "^7.4.3",
31
31
  "tslib": "^2.6.3",
32
- "typescript": "^5.5.2",
32
+ "typescript": "^5.5.4",
33
33
  "whatwg-mimetype": "^4.0.0",
34
- "ws": "^8.17.1"
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.5",
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.10"
49
+ "@types/ws": "^8.5.11"
50
50
  },
51
51
  "bin": {
52
52
  "scrypted-serve": "bin/scrypted-serve"
@@ -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', '')}{o['proxyId']}{clusterSecret}", 'utf8'))
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, proxyId: str, sourcePeerPort: int = None):
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, proxyId: onProxySerialization(
427
- value, proxyId, clusterPeerPort)
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.tags['localPort'] = clusterPeerPort
462
- clusterPeer.onProxySerialization = lambda value, proxyId: onProxySerialization(
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
- if clusterPeer.tags.get('localPort') == sourcePort:
498
- return value
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
- pass
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'] = self.generateId()
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
- proxyId = self.generateId()
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(self):
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 = self.generateId()
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
@@ -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('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
- if (!options?.ignoreStatusCode) {
125
+
126
+ if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
126
127
  try {
127
- checkStatus(response.statusCode);
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
- }
@@ -7,7 +7,10 @@ export interface HttpFetchOptionsBase<B> {
7
7
  signal?: AbortSignal,
8
8
  timeout?: number;
9
9
  rejectUnauthorized?: boolean;
10
- ignoreStatusCode?: boolean;
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('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 (!options?.ignoreStatusCode) {
197
+ if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
194
198
  try {
195
- checkStatus(response.status);
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
- }
@@ -7,13 +7,13 @@ export class ListenZeroSingleClientTimeoutError extends Error {
7
7
  }
8
8
  }
9
9
 
10
- export async function listenZero(server: net.Server, hostname?: string) {
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?: string, options?: net.ServerOpts, listenTimeout = 30000) {
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, proxyId: string, sourcePeerPort?: number) => {
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
- // always reassign the id and source.
115
- // if this is already a p2p object, and is passed to a different peer,
116
- // a future p2p object must be routed to the correct p2p peer to find the object.
117
- // clusterEntry.proxyId = proxyId;
118
- // clusterEntry.source = source;
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, proxyId) => onProxySerialization(value, proxyId, clusterPeerPort);
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.tags.localPort = sourcePort;
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
- // if the localPort is the sourcePort, that means the rpc object already exists as it originated from this node.
196
- // so return the existing proxy.
197
- if (clusterPeer.tags.localPort === sourcePort)
198
- return value;
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
- peer.evalLocal(script, filename, params);
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
 
@@ -75,5 +75,5 @@ export async function createREPLServer(scrypted: ScryptedStatic, params: any, pl
75
75
  socket.on('error', cleanup);
76
76
  socket.on('end', cleanup);
77
77
  });
78
- return listenZero(server);
78
+ return listenZero(server, '127.0.0.1');
79
79
  }
@@ -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
+ }
@@ -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 {