@scrypted/server 0.2.8 → 0.2.10

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.

Potentially problematic release.


This version of @scrypted/server might be problematic. Click here for more details.

Files changed (42) hide show
  1. package/dist/mixin/mixin-cycle.js +30 -0
  2. package/dist/mixin/mixin-cycle.js.map +1 -0
  3. package/dist/plugin/media.js +11 -1
  4. package/dist/plugin/media.js.map +1 -1
  5. package/dist/plugin/plugin-host.js +2 -2
  6. package/dist/plugin/plugin-host.js.map +1 -1
  7. package/dist/plugin/plugin-remote-worker.js +1 -1
  8. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  9. package/dist/plugin/plugin-state-check.js +3 -0
  10. package/dist/plugin/plugin-state-check.js.map +1 -1
  11. package/dist/plugin/runtime/python-worker.js +14 -9
  12. package/dist/plugin/runtime/python-worker.js.map +1 -1
  13. package/dist/rpc.js +11 -1
  14. package/dist/rpc.js.map +1 -1
  15. package/dist/runtime.js +8 -0
  16. package/dist/runtime.js.map +1 -1
  17. package/dist/scrypted-server-main.js +39 -42
  18. package/dist/scrypted-server-main.js.map +1 -1
  19. package/dist/services/plugin.js +8 -1
  20. package/dist/services/plugin.js.map +1 -1
  21. package/dist/services/service-control.js +10 -2
  22. package/dist/services/service-control.js.map +1 -1
  23. package/dist/state.js +3 -0
  24. package/dist/state.js.map +1 -1
  25. package/dist/usertoken.js +46 -0
  26. package/dist/usertoken.js.map +1 -0
  27. package/package.json +2 -2
  28. package/python/plugin-remote.py +120 -19
  29. package/python/rpc.py +24 -21
  30. package/src/mixin/mixin-cycle.ts +31 -0
  31. package/src/plugin/media.ts +13 -3
  32. package/src/plugin/plugin-host.ts +2 -2
  33. package/src/plugin/plugin-remote-worker.ts +1 -1
  34. package/src/plugin/plugin-state-check.ts +3 -0
  35. package/src/plugin/runtime/python-worker.ts +14 -9
  36. package/src/rpc.ts +12 -1
  37. package/src/runtime.ts +9 -0
  38. package/src/scrypted-server-main.ts +41 -46
  39. package/src/services/plugin.ts +8 -1
  40. package/src/services/service-control.ts +10 -2
  41. package/src/state.ts +3 -0
  42. package/src/usertoken.ts +38 -0
@@ -4,6 +4,7 @@ import asyncio
4
4
  import base64
5
5
  import gc
6
6
  import json
7
+ import mimetypes
7
8
  import os
8
9
  import platform
9
10
  import shutil
@@ -17,12 +18,11 @@ from asyncio.streams import StreamReader, StreamWriter
17
18
  from collections.abc import Mapping
18
19
  from io import StringIO
19
20
  from os import sys
20
- from typing import Any, Optional, Set, Tuple
21
+ from typing import Any, Dict, List, Optional, Set, Tuple
21
22
 
22
23
  import aiofiles
23
24
  import scrypted_python.scrypted_sdk.types
24
25
  from scrypted_python.scrypted_sdk.types import (Device, DeviceManifest,
25
- MediaManager,
26
26
  ScryptedInterfaceProperty,
27
27
  Storage)
28
28
  from typing_extensions import TypedDict
@@ -43,6 +43,50 @@ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
43
43
  async def getComponent(self, id: str) -> Any:
44
44
  return await self.api.getComponent(id)
45
45
 
46
+ class MediaObjectRemote(scrypted_python.scrypted_sdk.types.MediaObject):
47
+ def __init__(self, data, mimeType, sourceId):
48
+ self.mimeType = mimeType
49
+ self.data = data
50
+ setattr(self, '__proxy_props', {
51
+ 'mimeType': mimeType,
52
+ 'sourceId': sourceId,
53
+ })
54
+
55
+ async def getData(self):
56
+ return self.data
57
+
58
+ class MediaManager:
59
+ def __init__(self, mediaManager: scrypted_python.scrypted_sdk.types.MediaManager):
60
+ self.mediaManager = mediaManager
61
+
62
+ async def addConverter(self, converter: scrypted_python.scrypted_sdk.types.BufferConverter) -> None:
63
+ return await self.mediaManager.addConverter(converter)
64
+ async def clearConverters(self) -> None:
65
+ return await self.mediaManager.clearConverters()
66
+ async def convertMediaObject(self, mediaObject: scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> Any:
67
+ return await self.mediaManager.convertMediaObject(mediaObject, toMimeType)
68
+ async def convertMediaObjectToBuffer(self, mediaObject: scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> bytearray:
69
+ return await self.mediaManager.convertMediaObjectToBuffer(mediaObject, toMimeType)
70
+ async def convertMediaObjectToInsecureLocalUrl(self, mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> str:
71
+ return await self.mediaManager.convertMediaObjectToInsecureLocalUrl(mediaObject, toMimeType)
72
+ async def convertMediaObjectToJSON(self, mediaObject: scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> Any:
73
+ return await self.mediaManager.convertMediaObjectToJSON(mediaObject, toMimeType)
74
+ async def convertMediaObjectToLocalUrl(self, mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> str:
75
+ return await self.mediaManager.convertMediaObjectToLocalUrl(mediaObject, toMimeType)
76
+ async def convertMediaObjectToUrl(self, mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> str:
77
+ return await self.mediaManager.convertMediaObjectToUrl(mediaObject, toMimeType)
78
+ async def createFFmpegMediaObject(self, ffmpegInput: scrypted_python.scrypted_sdk.types.FFmpegInput, options: scrypted_python.scrypted_sdk.types.MediaObjectOptions = None) -> scrypted_python.scrypted_sdk.types.MediaObject:
79
+ return await self.mediaManager.createFFmpegMediaObject(ffmpegInput, options)
80
+ async def createMediaObject(self, data: Any, mimeType: str, options: scrypted_python.scrypted_sdk.types.MediaObjectOptions = None) -> scrypted_python.scrypted_sdk.types.MediaObject:
81
+ # return await self.createMediaObject(data, mimetypes, options)
82
+ return MediaObjectRemote(data, mimeType, options.get('sourceId', None) if options else None)
83
+ async def createMediaObjectFromUrl(self, data: str, options:scrypted_python.scrypted_sdk.types. MediaObjectOptions = None) -> scrypted_python.scrypted_sdk.types.MediaObject:
84
+ return await self.mediaManager.createMediaObjectFromUrl(data, options)
85
+ async def getFFmpegPath(self) -> str:
86
+ return await self.mediaManager.getFFmpegPath()
87
+ async def getFilesPath(self) -> str:
88
+ return await self.mediaManager.getFilesPath()
89
+
46
90
  class DeviceState(scrypted_python.scrypted_sdk.types.DeviceState):
47
91
  def __init__(self, id: str, nativeId: str, systemManager: SystemManager, deviceManager: scrypted_python.scrypted_sdk.types.DeviceManager) -> None:
48
92
  super().__init__()
@@ -139,13 +183,27 @@ class DeviceManager(scrypted_python.scrypted_sdk.types.DeviceManager):
139
183
  return self.nativeIds.get(nativeId, None)
140
184
 
141
185
  class BufferSerializer(rpc.RpcSerializer):
142
- def serialize(self, value):
186
+ def serialize(self, value, serializationContext):
143
187
  return base64.b64encode(value).decode('utf8')
144
188
 
145
- def deserialize(self, value):
189
+ def deserialize(self, value, serializationContext):
146
190
  return base64.b64decode(value)
147
191
 
148
192
 
193
+ class SidebandBufferSerializer(rpc.RpcSerializer):
194
+ def serialize(self, value, serializationContext):
195
+ buffers = serializationContext.get('buffers', None)
196
+ if not buffers:
197
+ buffers = []
198
+ serializationContext['buffers'] = buffers
199
+ buffers.append(value)
200
+ return len(buffers) - 1
201
+
202
+ def deserialize(self, value, serializationContext):
203
+ buffers: List = serializationContext.get('buffers', None)
204
+ buffer = buffers.pop()
205
+ return buffer
206
+
149
207
  class PluginRemote:
150
208
  systemState: Mapping[str, Mapping[str, SystemDeviceState]] = {}
151
209
  nativeIds: Mapping[str, DeviceStorage] = {}
@@ -278,7 +336,7 @@ class PluginRemote:
278
336
  from scrypted_sdk import sdk_init # type: ignore
279
337
  self.systemManager = SystemManager(self.api, self.systemState)
280
338
  self.deviceManager = DeviceManager(self.nativeIds, self.systemManager)
281
- self.mediaManager = await self.api.getMediaManager()
339
+ self.mediaManager = MediaManager(await self.api.getMediaManager())
282
340
  sdk_init(zip, self, self.systemManager,
283
341
  self.deviceManager, self.mediaManager)
284
342
  from main import create_scrypted_plugin # type: ignore
@@ -329,29 +387,72 @@ class PluginRemote:
329
387
  pass
330
388
 
331
389
 
332
- async def readLoop(loop, peer, reader):
333
- async for line in reader:
390
+ async def readLoop(loop, peer: rpc.RpcPeer, reader):
391
+ deserializationContext = {
392
+ 'buffers': []
393
+ }
394
+
395
+ while True:
334
396
  try:
335
- message = json.loads(line)
336
- asyncio.run_coroutine_threadsafe(peer.handleMessage(message), loop)
397
+ lengthBytes = await reader.read(4)
398
+ typeBytes = await reader.read(1)
399
+ type = typeBytes[0]
400
+ length = int.from_bytes(lengthBytes, 'big')
401
+ data = await reader.read(length - 1)
402
+
403
+ if type == 1:
404
+ deserializationContext['buffers'].append(data)
405
+ continue
406
+
407
+ message = json.loads(data)
408
+ asyncio.run_coroutine_threadsafe(peer.handleMessage(message, deserializationContext), loop)
409
+
410
+ deserializationContext = {
411
+ 'buffers': []
412
+ }
337
413
  except Exception as e:
338
414
  print('read loop error', e)
339
415
  sys.exit()
340
416
 
341
417
 
342
418
  async def async_main(loop: AbstractEventLoop):
343
- reader = await aiofiles.open(3, mode='r')
344
-
345
- def send(message, reject=None):
346
- jsonString = json.dumps(message)
347
- try:
348
- os.write(4, bytes(jsonString + '\n', 'utf8'))
349
- except Exception as e:
350
- if reject:
351
- reject(e)
419
+ reader = await aiofiles.open(3, mode='rb')
420
+
421
+ mutex = threading.Lock()
422
+
423
+ def send(message, reject=None, serializationContext = None):
424
+ with mutex:
425
+ if serializationContext:
426
+ buffers = serializationContext.get('buffers', None)
427
+ if buffers:
428
+ for buffer in buffers:
429
+ length = len(buffer) + 1
430
+ lb = length.to_bytes(4, 'big')
431
+ type = 1
432
+ try:
433
+ os.write(4, lb)
434
+ os.write(4, bytes([type]))
435
+ os.write(4, buffer)
436
+ except Exception as e:
437
+ if reject:
438
+ reject(e)
439
+ return
440
+
441
+ jsonString = json.dumps(message)
442
+ b = bytes(jsonString, 'utf8')
443
+ length = len(b) + 1
444
+ lb = length.to_bytes(4, 'big')
445
+ type = 0
446
+ try:
447
+ os.write(4, lb)
448
+ os.write(4, bytes([type]))
449
+ os.write(4, b)
450
+ except Exception as e:
451
+ if reject:
452
+ reject(e)
352
453
 
353
454
  peer = rpc.RpcPeer(send)
354
- peer.nameDeserializerMap['Buffer'] = BufferSerializer()
455
+ peer.nameDeserializerMap['Buffer'] = SidebandBufferSerializer()
355
456
  peer.constructorSerializerMap[bytes] = 'Buffer'
356
457
  peer.constructorSerializerMap[bytearray] = 'Buffer'
357
458
  peer.params['print'] = print
package/python/rpc.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from asyncio.futures import Future
2
- from typing import Any, Callable, Mapping, List
2
+ from typing import Any, Callable, Dict, Mapping, List
3
3
  import traceback
4
4
  import inspect
5
5
  from typing_extensions import TypedDict
@@ -31,10 +31,10 @@ class RpcResultException(Exception):
31
31
 
32
32
 
33
33
  class RpcSerializer:
34
- def serialize(self, value):
34
+ def serialize(self, value, serializationContext):
35
35
  pass
36
36
 
37
- def deserialize(self, value):
37
+ def deserialize(self, value, deserializationContext):
38
38
  pass
39
39
 
40
40
 
@@ -72,7 +72,7 @@ class RpcProxy(object):
72
72
 
73
73
  def __setattr__(self, name: str, value: Any) -> None:
74
74
  if name == '__proxy_finalizer_id':
75
- self.dict['__proxy_entry']['finalizerId'] = value
75
+ self.__dict__['__proxy_entry']['finalizerId'] = value
76
76
 
77
77
  return super().__setattr__(name, value)
78
78
 
@@ -85,7 +85,7 @@ class RpcProxy(object):
85
85
 
86
86
 
87
87
  class RpcPeer:
88
- def __init__(self, send: Callable[[object, Callable[[Exception], None]], None]) -> None:
88
+ def __init__(self, send: Callable[[object, Callable[[Exception], None], Dict], None]) -> None:
89
89
  self.send = send
90
90
  self.idCounter = 1
91
91
  self.peerName = 'Unnamed Peer'
@@ -99,9 +99,10 @@ class RpcPeer:
99
99
  self.nameDeserializerMap: Mapping[str, RpcSerializer] = {}
100
100
 
101
101
  def __apply__(self, proxyId: str, oneWayMethods: List[str], method: str, args: list):
102
+ serializationContext: Dict = {}
102
103
  serializedArgs = []
103
104
  for arg in args:
104
- serializedArgs.append(self.serialize(arg, False))
105
+ serializedArgs.append(self.serialize(arg, False, serializationContext))
105
106
 
106
107
  rpcApply = {
107
108
  'type': 'apply',
@@ -113,25 +114,25 @@ class RpcPeer:
113
114
 
114
115
  if oneWayMethods and method in oneWayMethods:
115
116
  rpcApply['oneway'] = True
116
- self.send(rpcApply)
117
+ self.send(rpcApply, None, serializationContext)
117
118
  future = Future()
118
119
  future.set_result(None)
119
120
  return future
120
121
 
121
122
  async def send(id: str, reject: Callable[[Exception], None]):
122
123
  rpcApply['id'] = id
123
- self.send(rpcApply, reject)
124
+ self.send(rpcApply, reject, serializationContext)
124
125
  return self.createPendingResult(send)
125
126
 
126
127
  def kill(self):
127
128
  self.killed = True
128
129
 
129
- def createErrorResult(self, result: any, name: str, message: str, tb: str):
130
+ def createErrorResult(self, result: Any, name: str, message: str, tb: str):
130
131
  result['stack'] = tb if tb else 'no stack'
131
132
  result['result'] = name if name else 'no name'
132
133
  result['message'] = message if message else 'no message'
133
134
 
134
- def serialize(self, value, requireProxy):
135
+ def serialize(self, value, requireProxy, serializationContext: Dict):
135
136
  if (not value or (not requireProxy and type(value) in jsonSerializable)):
136
137
  return value
137
138
 
@@ -164,7 +165,7 @@ class RpcPeer:
164
165
  if serializerMapName:
165
166
  __remote_constructor_name = serializerMapName
166
167
  serializer = self.nameDeserializerMap.get(serializerMapName, None)
167
- serialized = serializer.serialize(value)
168
+ serialized = serializer.serialize(value, serializationContext)
168
169
  ret = {
169
170
  '__remote_proxy_id': None,
170
171
  '__remote_proxy_finalizer_id': None,
@@ -216,7 +217,7 @@ class RpcPeer:
216
217
  weakref.finalize(proxy, lambda: self.finalize(localProxiedEntry))
217
218
  return proxy
218
219
 
219
- def deserialize(self, value):
220
+ def deserialize(self, value, deserializationContext: Dict):
220
221
  if not value:
221
222
  return value
222
223
 
@@ -240,7 +241,7 @@ class RpcPeer:
240
241
  if not proxy:
241
242
  proxy = self.newProxy(__remote_proxy_id, __remote_constructor_name,
242
243
  __remote_proxy_props, __remote_proxy_oneway_methods)
243
- proxy.__proxy_finalizer_id = __remote_proxy_finalizer_id
244
+ setattr(proxy, '__proxy_finalizer_id', __remote_proxy_finalizer_id)
244
245
  return proxy
245
246
 
246
247
  if __local_proxy_id:
@@ -253,11 +254,11 @@ class RpcPeer:
253
254
  deserializer = self.nameDeserializerMap.get(
254
255
  __remote_constructor_name, None)
255
256
  if deserializer:
256
- return deserializer.deserialize(__serialized_value)
257
+ return deserializer.deserialize(__serialized_value, deserializationContext)
257
258
 
258
259
  return value
259
260
 
260
- async def handleMessage(self, message: any):
261
+ async def handleMessage(self, message: Any, deserializationContext: Dict):
261
262
  try:
262
263
  messageType = message['type']
263
264
  if messageType == 'param':
@@ -266,17 +267,18 @@ class RpcPeer:
266
267
  'id': message['id'],
267
268
  }
268
269
 
270
+ serializationContext: Dict = {}
269
271
  try:
270
272
  value = self.params.get(message['param'], None)
271
273
  value = await maybe_await(value)
272
274
  result['result'] = self.serialize(
273
- value, message.get('requireProxy', None))
275
+ value, message.get('requireProxy', None), serializationContext)
274
276
  except Exception as e:
275
277
  tb = traceback.format_exc()
276
278
  self.createErrorResult(
277
279
  result, type(e).__name, str(e), tb)
278
280
 
279
- self.send(result)
281
+ self.send(result, None, serializationContext)
280
282
 
281
283
  elif messageType == 'apply':
282
284
  result = {
@@ -286,6 +288,7 @@ class RpcPeer:
286
288
  method = message.get('method', None)
287
289
 
288
290
  try:
291
+ serializationContext: Dict = {}
289
292
  target = self.localProxyMap.get(
290
293
  message['proxyId'], None)
291
294
  if not target:
@@ -294,7 +297,7 @@ class RpcPeer:
294
297
 
295
298
  args = []
296
299
  for arg in (message['args'] or []):
297
- args.append(self.deserialize(arg))
300
+ args.append(self.deserialize(arg, deserializationContext))
298
301
 
299
302
  value = None
300
303
  if method:
@@ -306,7 +309,7 @@ class RpcPeer:
306
309
  else:
307
310
  value = await maybe_await(target(*args))
308
311
 
309
- result['result'] = self.serialize(value, False)
312
+ result['result'] = self.serialize(value, False, serializationContext)
310
313
  except Exception as e:
311
314
  tb = traceback.format_exc()
312
315
  # print('failure', method, e, tb)
@@ -314,7 +317,7 @@ class RpcPeer:
314
317
  result, type(e).__name__, str(e), tb)
315
318
 
316
319
  if not message.get('oneway', False):
317
- self.send(result)
320
+ self.send(result, None, serializationContext)
318
321
 
319
322
  elif messageType == 'result':
320
323
  id = message['id']
@@ -331,7 +334,7 @@ class RpcPeer:
331
334
  future.set_exception(e)
332
335
  return
333
336
  future.set_result(self.deserialize(
334
- message.get('result', None)))
337
+ message.get('result', None), deserializationContext))
335
338
  elif messageType == 'finalize':
336
339
  finalizerId = message.get('__local_proxy_finalizer_id', None)
337
340
  proxyId = message['__local_proxy_id']
@@ -0,0 +1,31 @@
1
+ import { ScryptedInterfaceProperty } from "@scrypted/types";
2
+ import { ScryptedRuntime } from "../runtime";
3
+ import { getState } from "../state";
4
+
5
+ function getMixins(scrypted: ScryptedRuntime, id: string) {
6
+ const pluginDevice = scrypted.findPluginDeviceById(id);
7
+ if (!pluginDevice)
8
+ return [];
9
+ return getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
10
+ }
11
+
12
+ export function hasMixinCycle(scrypted: ScryptedRuntime, id: string, mixins?: string[]) {
13
+ mixins = mixins || getMixins(scrypted, id);
14
+
15
+ // given the mixins for a device, find all the mixins for those mixins,
16
+ // and create a visited graph.
17
+ // if the visited graphs includes the original device, that indicates
18
+ // a cyclical dependency for that device.
19
+ const visitedMixins = new Set(mixins);
20
+
21
+ while (mixins.length) {
22
+ const mixin = mixins.pop();
23
+ if (visitedMixins.has(mixin))
24
+ continue;
25
+ visitedMixins.add(mixin);
26
+ const providerMixins = getMixins(scrypted, mixin);
27
+ mixins.push(...providerMixins);
28
+ }
29
+
30
+ return visitedMixins.has(id);
31
+ }
@@ -201,7 +201,17 @@ export abstract class MediaManagerBase implements MediaManager {
201
201
  getConverters(): IdBufferConverter[] {
202
202
  const converters = Object.entries(this.getSystemState())
203
203
  .filter(([id, state]) => state[ScryptedInterfaceProperty.interfaces]?.value?.includes(ScryptedInterface.BufferConverter))
204
- .map(([id]) => this.getDeviceById<IdBufferConverter>(id));
204
+ .map(([id]) => {
205
+ const device = this.getDeviceById<BufferConverter>(id);
206
+ return {
207
+ id,
208
+ fromMimeType: device.fromMimeType,
209
+ toMimeType: device.toMimeType,
210
+ convert(data, fromMimeType, toMimeType, options?) {
211
+ return device.convert(data, fromMimeType, toMimeType, options);
212
+ },
213
+ } as IdBufferConverter;
214
+ });
205
215
 
206
216
  // builtins should be after system converters. these should not be overriden by system,
207
217
  // as it could cause system instability with misconfiguration.
@@ -328,7 +338,7 @@ export abstract class MediaManagerBase implements MediaManager {
328
338
  converterMap.set(c.id, c);
329
339
  }
330
340
 
331
- const nodes: any = {};
341
+ const nodes: { [node: string]: { [edge: string]: number } } = {};
332
342
  const mediaNode: any = {};
333
343
  nodes['mediaObject'] = mediaNode;
334
344
  nodes['output'] = {};
@@ -341,7 +351,7 @@ export abstract class MediaManagerBase implements MediaManager {
341
351
  // const convertedWeight = parseFloat(convertedMime.parameters.get('converter-weight')) || (convertedMime.essence === ScryptedMimeTypes.MediaObject ? 1000 : 1);
342
352
  // const conversionWeight = inputWeight + convertedWeight;
343
353
  const targetId = converter.id;
344
- const node: any = nodes[targetId] = {};
354
+ const node: { [edge: string]: number } = nodes[targetId] = {};
345
355
 
346
356
  // edge matches
347
357
  for (const candidate of converters) {
@@ -293,9 +293,9 @@ export class PluginHost {
293
293
  }
294
294
  }
295
295
 
296
- this.peer = new RpcPeer('host', this.pluginId, (message, reject) => {
296
+ this.peer = new RpcPeer('host', this.pluginId, (message, reject, serializationContext) => {
297
297
  if (connected) {
298
- this.worker.send(message, reject);
298
+ this.worker.send(message, reject, serializationContext);
299
299
  }
300
300
  else if (reject) {
301
301
  reject(new Error('peer disconnected'));
@@ -397,7 +397,7 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
397
397
  peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
398
398
 
399
399
  if (zipOptions?.fork) {
400
- pluginConsole?.log('plugin forked');
400
+ // pluginConsole?.log('plugin forked');
401
401
  const fork = exports.fork;
402
402
  const forked = await fork();
403
403
  forked[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
@@ -1,4 +1,5 @@
1
1
  import { ScryptedInterface, ScryptedInterfaceProperty } from "@scrypted/types";
2
+ import { RpcPeer } from "../rpc";
2
3
  import { propertyInterfaces } from "./descriptor";
3
4
 
4
5
  export function checkProperty(key: string, value: any) {
@@ -8,6 +9,8 @@ export function checkProperty(key: string, value: any) {
8
9
  throw new Error("mixins is read only");
9
10
  if (key === ScryptedInterfaceProperty.interfaces)
10
11
  throw new Error("interfaces is a read only post-mixin computed property, use providedInterfaces");
12
+ if (RpcPeer.isRpcProxy(value))
13
+ throw new Error('value must be a primitive type')
11
14
  const iface = propertyInterfaces[key.toString()];
12
15
  if (iface === ScryptedInterface.ScryptedDevice) {
13
16
  // only allow info to be set, since that doesn't actually change the descriptor
@@ -5,10 +5,12 @@ import path from 'path';
5
5
  import readline from 'readline';
6
6
  import { Readable, Writable } from 'stream';
7
7
  import { RpcMessage, RpcPeer } from "../../rpc";
8
+ import { createRpcDuplexSerializer } from '../../rpc-serializer';
8
9
  import { ChildProcessWorker } from "./child-process-worker";
9
10
  import { RuntimeWorkerOptions } from "./runtime-worker";
10
11
 
11
12
  export class PythonRuntimeWorker extends ChildProcessWorker {
13
+ serializer: ReturnType<typeof createRpcDuplexSerializer>;
12
14
 
13
15
  constructor(pluginId: string, options: RuntimeWorkerOptions) {
14
16
  super(pluginId, options);
@@ -62,21 +64,24 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
62
64
  const peerin = this.worker.stdio[3] as Writable;
63
65
  const peerout = this.worker.stdio[4] as Readable;
64
66
 
65
- peerin.on('error', e => this.emit('error', e));
66
- peerout.on('error', e => this.emit('error', e));
67
-
68
- const readInterface = readline.createInterface({
69
- input: peerout,
70
- terminal: false,
67
+ const serializer = this.serializer = createRpcDuplexSerializer(peerin);
68
+ serializer.setupRpcPeer(peer);
69
+ peerout.on('data', data => serializer.onData(data));
70
+ peerin.on('error', e => {
71
+ this.emit('error', e);
72
+ serializer.onDisconnected();
73
+ });
74
+ peerout.on('error', e => {
75
+ this.emit('error', e)
76
+ serializer.onDisconnected();
71
77
  });
72
- readInterface.on('line', line => peer.handleMessage(JSON.parse(line)));
73
78
  }
74
79
 
75
- send(message: RpcMessage, reject?: (e: Error) => void): void {
80
+ send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void {
76
81
  try {
77
82
  if (!this.worker)
78
83
  throw new Error('worked has been killed');
79
- (this.worker.stdio[3] as Writable).write(JSON.stringify(message) + '\n', e => e && reject?.(e));
84
+ this.serializer.sendMessage(message, reject, serializationContext);
80
85
  }
81
86
  catch (e) {
82
87
  reject?.(e);
package/src/rpc.ts CHANGED
@@ -84,7 +84,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
84
84
  }
85
85
 
86
86
  get(target: any, p: PropertyKey, receiver: any): any {
87
- if (p === '__proxy_id')
87
+ if (p === RpcPeer.PROPERTY_PROXY_ID)
88
88
  return this.entry.id;
89
89
  if (p === '__proxy_constructor')
90
90
  return this.constructorName;
@@ -211,9 +211,15 @@ export class RpcPeer {
211
211
  nameDeserializerMap = new Map<string, RpcSerializer>();
212
212
  constructorSerializerMap = new Map<any, string>();
213
213
  transportSafeArgumentTypes = RpcPeer.getDefaultTransportSafeArgumentTypes();
214
+ killed: Promise<void>;
215
+ killedDeferred: Deferred;
214
216
 
215
217
  static readonly finalizerIdSymbol = Symbol('rpcFinalizerId');
216
218
 
219
+ static isRpcProxy(value: any) {
220
+ return !!value?.[RpcPeer.PROPERTY_PROXY_ID];
221
+ }
222
+
217
223
  static getDefaultTransportSafeArgumentTypes() {
218
224
  const jsonSerializable = new Set<string>();
219
225
  jsonSerializable.add(Number.name);
@@ -242,6 +248,7 @@ export class RpcPeer {
242
248
  }
243
249
  }
244
250
 
251
+ static readonly PROPERTY_PROXY_ID = '__proxy_id';
245
252
  static readonly PROPERTY_PROXY_ONEWAY_METHODS = '__proxy_oneway_methods';
246
253
  static readonly PROPERTY_JSON_DISABLE_SERIALIZATION = '__json_disable_serialization';
247
254
  static readonly PROPERTY_PROXY_PROPERTIES = '__proxy_props';
@@ -259,6 +266,9 @@ export class RpcPeer {
259
266
  ]);
260
267
 
261
268
  constructor(public selfName: string, public peerName: string, public send: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
269
+ this.killed = new Promise((resolve, reject) => {
270
+ this.killedDeferred = { resolve, reject };
271
+ });
262
272
  }
263
273
 
264
274
  createPendingResult(cb: (id: string, reject: (e: Error) => void) => void): Promise<any> {
@@ -280,6 +290,7 @@ export class RpcPeer {
280
290
 
281
291
  kill(message?: string) {
282
292
  const error = new RPCResultError(this, message || 'peer was killed');
293
+ this.killedDeferred.reject(error);
283
294
  for (const result of Object.values(this.pendingResults)) {
284
295
  result.reject(error);
285
296
  }
package/src/runtime.ts CHANGED
@@ -20,6 +20,7 @@ import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefaul
20
20
  import { IOServer } from './io';
21
21
  import { Level } from './level';
22
22
  import { LogEntry, Logger, makeAlertId } from './logger';
23
+ import { hasMixinCycle } from './mixin/mixin-cycle';
23
24
  import { PluginDebug } from './plugin/plugin-debug';
24
25
  import { PluginDeviceProxyHandler } from './plugin/plugin-device';
25
26
  import { PluginHost } from './plugin/plugin-host';
@@ -812,6 +813,14 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
812
813
  }
813
814
  }
814
815
 
816
+ for (const id of Object.keys(this.stateManager.getSystemState())) {
817
+ if (hasMixinCycle(this, id)) {
818
+ console.warn(`initialize: ${id} has a mixin cycle. Clearing mixins.`);
819
+ const pluginDevice = this.findPluginDeviceById(id);
820
+ setState(pluginDevice, ScryptedInterfaceProperty.mixins, []);
821
+ }
822
+ }
823
+
815
824
  for await (const plugin of this.datastore.getAll(Plugin)) {
816
825
  try {
817
826
  const pluginDevice = this.findPluginDevice(plugin._id);