@scrypted/server 0.115.0 → 0.115.2

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 (49) hide show
  1. package/deno/deno-plugin-remote.ts +4 -0
  2. package/dist/cluster/cluster-hash.js +1 -1
  3. package/dist/cluster/cluster-hash.js.map +1 -1
  4. package/dist/cluster/connect-rpc-object.d.ts +2 -1
  5. package/dist/plugin/plugin-api.d.ts +1 -0
  6. package/dist/plugin/plugin-remote-stats.js +19 -15
  7. package/dist/plugin/plugin-remote-stats.js.map +1 -1
  8. package/dist/plugin/plugin-remote-worker.js +90 -54
  9. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  10. package/dist/plugin/runtime/deno-worker.d.ts +12 -0
  11. package/dist/plugin/runtime/deno-worker.js +84 -0
  12. package/dist/plugin/runtime/deno-worker.js.map +1 -0
  13. package/dist/plugin/runtime/node-fork-worker.d.ts +1 -1
  14. package/dist/plugin/runtime/node-fork-worker.js +2 -2
  15. package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
  16. package/dist/plugin/runtime/node-thread-worker.d.ts +3 -3
  17. package/dist/plugin/runtime/node-thread-worker.js +22 -7
  18. package/dist/plugin/runtime/node-thread-worker.js.map +1 -1
  19. package/dist/rpc-peer-eval.d.ts +4 -1
  20. package/dist/rpc-peer-eval.js +4 -10
  21. package/dist/rpc-peer-eval.js.map +1 -1
  22. package/dist/rpc.d.ts +4 -1
  23. package/dist/rpc.js +8 -4
  24. package/dist/rpc.js.map +1 -1
  25. package/dist/runtime.js +2 -0
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/scrypted-main-exports.js +2 -2
  28. package/dist/scrypted-main-exports.js.map +1 -1
  29. package/dist/scrypted-main.js +4 -1
  30. package/dist/scrypted-main.js.map +1 -1
  31. package/dist/scrypted-plugin-main.js +14 -4
  32. package/dist/scrypted-plugin-main.js.map +1 -1
  33. package/package.json +4 -3
  34. package/python/plugin_remote.py +423 -221
  35. package/python/rpc.py +10 -10
  36. package/src/cluster/cluster-hash.ts +1 -1
  37. package/src/cluster/connect-rpc-object.ts +2 -1
  38. package/src/plugin/plugin-api.ts +1 -0
  39. package/src/plugin/plugin-remote-stats.ts +20 -15
  40. package/src/plugin/plugin-remote-worker.ts +103 -56
  41. package/src/plugin/runtime/deno-worker.ts +91 -0
  42. package/src/plugin/runtime/node-fork-worker.ts +3 -5
  43. package/src/plugin/runtime/node-thread-worker.ts +22 -6
  44. package/src/rpc-peer-eval.ts +9 -14
  45. package/src/rpc.ts +17 -7
  46. package/src/runtime.ts +2 -0
  47. package/src/scrypted-main-exports.ts +2 -2
  48. package/src/scrypted-main.ts +4 -1
  49. package/src/scrypted-plugin-main.ts +14 -4
@@ -8,7 +8,6 @@ import multiprocessing
8
8
  import multiprocessing.connection
9
9
  import os
10
10
  import platform
11
- import shutil
12
11
  import sys
13
12
  import threading
14
13
  import time
@@ -39,11 +38,13 @@ ptpython
39
38
  wheel
40
39
  """.strip()
41
40
 
41
+
42
42
  class ClusterObject(TypedDict):
43
43
  id: str
44
+ address: str
44
45
  port: int
45
46
  proxyId: str
46
- sourcePort: int
47
+ sourceKey: str
47
48
  sha256: str
48
49
 
49
50
 
@@ -60,7 +61,7 @@ class DeviceProxy(object):
60
61
  self.device: asyncio.Future[rpc.RpcProxy] = None
61
62
 
62
63
  def __getattr__(self, name):
63
- if name == 'id':
64
+ if name == "id":
64
65
  return self.id
65
66
 
66
67
  if hasattr(ScryptedInterfaceProperty, name):
@@ -70,28 +71,33 @@ class DeviceProxy(object):
70
71
  p = state.get(name)
71
72
  if not p:
72
73
  return
73
- return p.get('value', None)
74
+ return p.get("value", None)
74
75
  if hasattr(ScryptedInterfaceMethods, name):
75
76
  return rpc.RpcProxyMethod(self, name)
76
77
 
77
78
  def __setattr__(self, name: str, value: Any) -> None:
78
- if name == '__proxy_finalizer_id':
79
- self.__dict__['__proxy_entry']['finalizerId'] = value
79
+ if name == "__proxy_finalizer_id":
80
+ self.__dict__["__proxy_entry"]["finalizerId"] = value
80
81
 
81
82
  return super().__setattr__(name, value)
82
83
 
83
84
  def __apply__(self, method: str, args: list):
84
85
  if not self.device:
85
- self.device = asyncio.ensure_future(self.systemManager.api.getDeviceById(self.id))
86
+ self.device = asyncio.ensure_future(
87
+ self.systemManager.api.getDeviceById(self.id)
88
+ )
86
89
 
87
90
  async def apply():
88
91
  device = await self.device
89
92
  return await device.__apply__(method, args)
93
+
90
94
  return apply()
91
95
 
92
96
 
93
97
  class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
94
- def __init__(self, api: Any, systemState: Mapping[str, Mapping[str, SystemDeviceState]]) -> None:
98
+ def __init__(
99
+ self, api: Any, systemState: Mapping[str, Mapping[str, SystemDeviceState]]
100
+ ) -> None:
95
101
  super().__init__()
96
102
  self.api = api
97
103
  self.systemState = systemState
@@ -103,7 +109,9 @@ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
103
109
  def getSystemState(self) -> Any:
104
110
  return self.systemState
105
111
 
106
- def getDeviceById(self, idOrPluginId: str, nativeId: str = None) -> scrypted_python.scrypted_sdk.ScryptedDevice:
112
+ def getDeviceById(
113
+ self, idOrPluginId: str, nativeId: str = None
114
+ ) -> scrypted_python.scrypted_sdk.ScryptedDevice:
107
115
  id: str = None
108
116
  if self.systemState.get(idOrPluginId, None):
109
117
  if nativeId is not None:
@@ -114,15 +122,15 @@ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
114
122
  state = self.systemState.get(check, None)
115
123
  if not state:
116
124
  continue
117
- pluginId = state.get('pluginId', None)
125
+ pluginId = state.get("pluginId", None)
118
126
  if not pluginId:
119
127
  continue
120
- pluginId = pluginId.get('value', None)
128
+ pluginId = pluginId.get("value", None)
121
129
  if pluginId == idOrPluginId:
122
- checkNativeId = state.get('nativeId', None)
130
+ checkNativeId = state.get("nativeId", None)
123
131
  if not checkNativeId:
124
132
  continue
125
- checkNativeId = checkNativeId.get('value', None)
133
+ checkNativeId = checkNativeId.get("value", None)
126
134
  if nativeId == checkNativeId:
127
135
  id = idOrPluginId
128
136
  break
@@ -140,31 +148,38 @@ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
140
148
  state = self.systemState.get(check, None)
141
149
  if not state:
142
150
  continue
143
- checkInterfaces = state.get('interfaces', None)
151
+ checkInterfaces = state.get("interfaces", None)
144
152
  if not checkInterfaces:
145
153
  continue
146
- interfaces = checkInterfaces.get('value', [])
154
+ interfaces = checkInterfaces.get("value", [])
147
155
  if ScryptedInterface.ScryptedPlugin.value in interfaces:
148
- checkPluginId = state.get('pluginId', None)
156
+ checkPluginId = state.get("pluginId", None)
149
157
  if not checkPluginId:
150
158
  continue
151
- pluginId = checkPluginId.get('value', None)
159
+ pluginId = checkPluginId.get("value", None)
152
160
  if not pluginId:
153
161
  continue
154
162
  if pluginId == name:
155
163
  return self.getDeviceById(check)
156
- checkName = state.get('name', None)
164
+ checkName = state.get("name", None)
157
165
  if not checkName:
158
166
  continue
159
- if checkName.get('value', None) == name:
167
+ if checkName.get("value", None) == name:
160
168
  return self.getDeviceById(check)
161
169
 
162
170
  # TODO
163
- async def listen(self, callback: scrypted_python.scrypted_sdk.EventListener) -> scrypted_python.scrypted_sdk.EventListenerRegister:
171
+ async def listen(
172
+ self, callback: scrypted_python.scrypted_sdk.EventListener
173
+ ) -> scrypted_python.scrypted_sdk.EventListenerRegister:
164
174
  return super().listen(callback)
165
175
 
166
176
  # TODO
167
- async def listenDevice(self, id: str, event: str | scrypted_python.scrypted_sdk.EventListenerOptions, callback: scrypted_python.scrypted_sdk.EventListener) -> scrypted_python.scrypted_sdk.EventListenerRegister:
177
+ async def listenDevice(
178
+ self,
179
+ id: str,
180
+ event: str | scrypted_python.scrypted_sdk.EventListenerOptions,
181
+ callback: scrypted_python.scrypted_sdk.EventListener,
182
+ ) -> scrypted_python.scrypted_sdk.EventListenerRegister:
168
183
  return super().listenDevice(id, event, callback)
169
184
 
170
185
  async def removeDevice(self, id: str) -> None:
@@ -179,7 +194,7 @@ class MediaObject(scrypted_python.scrypted_sdk.types.MediaObject):
179
194
  setattr(self, rpc.RpcPeer.PROPERTY_PROXY_PROPERTIES, proxyProps)
180
195
 
181
196
  options = options or {}
182
- options['mimeType'] = mimeType
197
+ options["mimeType"] = mimeType
183
198
 
184
199
  for key, value in options.items():
185
200
  if rpc.RpcPeer.isTransportSafe(value):
@@ -194,38 +209,83 @@ class MediaManager:
194
209
  def __init__(self, mediaManager: scrypted_python.scrypted_sdk.types.MediaManager):
195
210
  self.mediaManager = mediaManager
196
211
 
197
- async def addConverter(self, converter: scrypted_python.scrypted_sdk.types.BufferConverter) -> None:
212
+ async def addConverter(
213
+ self, converter: scrypted_python.scrypted_sdk.types.BufferConverter
214
+ ) -> None:
198
215
  return await self.mediaManager.addConverter(converter)
199
216
 
200
217
  async def clearConverters(self) -> None:
201
218
  return await self.mediaManager.clearConverters()
202
219
 
203
- async def convertMediaObject(self, mediaObject: scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> Any:
220
+ async def convertMediaObject(
221
+ self,
222
+ mediaObject: scrypted_python.scrypted_sdk.types.MediaObject,
223
+ toMimeType: str,
224
+ ) -> Any:
204
225
  return await self.mediaManager.convertMediaObject(mediaObject, toMimeType)
205
226
 
206
- async def convertMediaObjectToBuffer(self, mediaObject: scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> bytearray:
207
- return await self.mediaManager.convertMediaObjectToBuffer(mediaObject, toMimeType)
208
-
209
- async def convertMediaObjectToInsecureLocalUrl(self, mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> str:
210
- return await self.mediaManager.convertMediaObjectToInsecureLocalUrl(mediaObject, toMimeType)
211
-
212
- async def convertMediaObjectToJSON(self, mediaObject: scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> Any:
227
+ async def convertMediaObjectToBuffer(
228
+ self,
229
+ mediaObject: scrypted_python.scrypted_sdk.types.MediaObject,
230
+ toMimeType: str,
231
+ ) -> bytearray:
232
+ return await self.mediaManager.convertMediaObjectToBuffer(
233
+ mediaObject, toMimeType
234
+ )
235
+
236
+ async def convertMediaObjectToInsecureLocalUrl(
237
+ self,
238
+ mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject,
239
+ toMimeType: str,
240
+ ) -> str:
241
+ return await self.mediaManager.convertMediaObjectToInsecureLocalUrl(
242
+ mediaObject, toMimeType
243
+ )
244
+
245
+ async def convertMediaObjectToJSON(
246
+ self,
247
+ mediaObject: scrypted_python.scrypted_sdk.types.MediaObject,
248
+ toMimeType: str,
249
+ ) -> Any:
213
250
  return await self.mediaManager.convertMediaObjectToJSON(mediaObject, toMimeType)
214
251
 
215
- async def convertMediaObjectToLocalUrl(self, mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> str:
216
- return await self.mediaManager.convertMediaObjectToLocalUrl(mediaObject, toMimeType)
217
-
218
- async def convertMediaObjectToUrl(self, mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject, toMimeType: str) -> str:
252
+ async def convertMediaObjectToLocalUrl(
253
+ self,
254
+ mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject,
255
+ toMimeType: str,
256
+ ) -> str:
257
+ return await self.mediaManager.convertMediaObjectToLocalUrl(
258
+ mediaObject, toMimeType
259
+ )
260
+
261
+ async def convertMediaObjectToUrl(
262
+ self,
263
+ mediaObject: str | scrypted_python.scrypted_sdk.types.MediaObject,
264
+ toMimeType: str,
265
+ ) -> str:
219
266
  return await self.mediaManager.convertMediaObjectToUrl(mediaObject, toMimeType)
220
267
 
221
- 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:
268
+ async def createFFmpegMediaObject(
269
+ self,
270
+ ffmpegInput: scrypted_python.scrypted_sdk.types.FFmpegInput,
271
+ options: scrypted_python.scrypted_sdk.types.MediaObjectOptions = None,
272
+ ) -> scrypted_python.scrypted_sdk.types.MediaObject:
222
273
  return await self.mediaManager.createFFmpegMediaObject(ffmpegInput, options)
223
274
 
224
- async def createMediaObject(self, data: Any, mimeType: str, options: scrypted_python.scrypted_sdk.types.MediaObjectOptions = None) -> scrypted_python.scrypted_sdk.types.MediaObject:
275
+ async def createMediaObject(
276
+ self,
277
+ data: Any,
278
+ mimeType: str,
279
+ options: scrypted_python.scrypted_sdk.types.MediaObjectOptions = None,
280
+ ) -> scrypted_python.scrypted_sdk.types.MediaObject:
225
281
  # return await self.createMediaObject(data, mimetypes, options)
226
282
  return MediaObject(data, mimeType, options)
227
283
 
228
- async def createMediaObjectFromUrl(self, data: str, options: scrypted_python.scrypted_sdk.types.MediaObjectOptions = None) -> scrypted_python.scrypted_sdk.types.MediaObject:
284
+ async def createMediaObjectFromUrl(
285
+ self,
286
+ data: str,
287
+ options: scrypted_python.scrypted_sdk.types.MediaObjectOptions = None,
288
+ ) -> scrypted_python.scrypted_sdk.types.MediaObject:
229
289
  return await self.mediaManager.createMediaObjectFromUrl(data, options)
230
290
 
231
291
  async def getFFmpegPath(self) -> str:
@@ -236,7 +296,13 @@ class MediaManager:
236
296
 
237
297
 
238
298
  class DeviceState(scrypted_python.scrypted_sdk.types.DeviceState):
239
- def __init__(self, id: str, nativeId: str, systemManager: SystemManager, deviceManager: scrypted_python.scrypted_sdk.types.DeviceManager) -> None:
299
+ def __init__(
300
+ self,
301
+ id: str,
302
+ nativeId: str,
303
+ systemManager: SystemManager,
304
+ deviceManager: scrypted_python.scrypted_sdk.types.DeviceManager,
305
+ ) -> None:
240
306
  super().__init__()
241
307
  self._id = id
242
308
  self.nativeId = nativeId
@@ -253,7 +319,7 @@ class DeviceState(scrypted_python.scrypted_sdk.types.DeviceState):
253
319
  sdd = deviceState.get(property, None)
254
320
  if not sdd:
255
321
  return None
256
- return sdd.get('value', None)
322
+ return sdd.get("value", None)
257
323
 
258
324
  def setScryptedProperty(self, property: str, value: Any):
259
325
  if property == ScryptedInterfaceProperty.id.value:
@@ -262,18 +328,26 @@ class DeviceState(scrypted_python.scrypted_sdk.types.DeviceState):
262
328
  raise Exception("mixins is read only")
263
329
  if property == ScryptedInterfaceProperty.interfaces.value:
264
330
  raise Exception(
265
- "interfaces is a read only post-mixin computed property, use providedInterfaces")
331
+ "interfaces is a read only post-mixin computed property, use providedInterfaces"
332
+ )
266
333
 
267
334
  now = int(time.time() * 1000)
268
335
  self.systemManager.systemState[self._id][property] = {
269
336
  "lastEventTime": now,
270
337
  "stateTime": now,
271
- "value": value
338
+ "value": value,
272
339
  }
273
340
 
274
341
  self.systemManager.api.setState(self.nativeId, property, value)
275
342
 
276
343
 
344
+ class WritableDeviceState(scrypted_python.scrypted_sdk.types.WritableDeviceState):
345
+
346
+ def __init__(self, id, setState) -> None:
347
+ self.id = id
348
+ self.setState = setState
349
+
350
+
277
351
  class DeviceStorage(Storage):
278
352
  id: str
279
353
  nativeId: str
@@ -304,7 +378,9 @@ class DeviceStorage(Storage):
304
378
 
305
379
 
306
380
  class DeviceManager(scrypted_python.scrypted_sdk.types.DeviceManager):
307
- def __init__(self, nativeIds: Mapping[str, DeviceStorage], systemManager: SystemManager) -> None:
381
+ def __init__(
382
+ self, nativeIds: Mapping[str, DeviceStorage], systemManager: SystemManager
383
+ ) -> None:
308
384
  super().__init__()
309
385
  self.nativeIds = nativeIds
310
386
  self.systemManager = systemManager
@@ -313,7 +389,9 @@ class DeviceManager(scrypted_python.scrypted_sdk.types.DeviceManager):
313
389
  id = self.nativeIds[nativeId].id
314
390
  return DeviceState(id, nativeId, self.systemManager, self)
315
391
 
316
- async def onDeviceEvent(self, nativeId: str, eventInterface: str, eventData: Any = None) -> None:
392
+ async def onDeviceEvent(
393
+ self, nativeId: str, eventInterface: str, eventData: Any = None
394
+ ) -> None:
317
395
  await self.systemManager.api.onDeviceEvent(nativeId, eventInterface, eventData)
318
396
 
319
397
  async def onDevicesChanged(self, devices: DeviceManifest) -> None:
@@ -325,8 +403,12 @@ class DeviceManager(scrypted_python.scrypted_sdk.types.DeviceManager):
325
403
  async def onDeviceRemoved(self, nativeId: str) -> None:
326
404
  return await self.systemManager.api.onDeviceRemoved(nativeId)
327
405
 
328
- async def onMixinEvent(self, id: str, mixinDevice: Any, eventInterface: str, eventData: Any) -> None:
329
- return await self.systemManager.api.onMixinEvent(id, mixinDevice, eventInterface, eventData)
406
+ async def onMixinEvent(
407
+ self, id: str, mixinDevice: Any, eventInterface: str, eventData: Any
408
+ ) -> None:
409
+ return await self.systemManager.api.onMixinEvent(
410
+ id, mixinDevice, eventInterface, eventData
411
+ )
330
412
 
331
413
  async def requestRestart(self) -> None:
332
414
  return await self.systemManager.api.requestRestart()
@@ -336,7 +418,9 @@ class DeviceManager(scrypted_python.scrypted_sdk.types.DeviceManager):
336
418
 
337
419
 
338
420
  class PluginRemote:
339
- def __init__(self, peer: rpc.RpcPeer, api, pluginId: str, hostInfo, loop: AbstractEventLoop):
421
+ def __init__(
422
+ self, peer: rpc.RpcPeer, api, pluginId: str, hostInfo, loop: AbstractEventLoop
423
+ ):
340
424
  self.systemState: Mapping[str, Mapping[str, SystemDeviceState]] = {}
341
425
  self.nativeIds: Mapping[str, DeviceStorage] = {}
342
426
  self.mediaManager: MediaManager
@@ -349,199 +433,267 @@ class PluginRemote:
349
433
  self.hostInfo = hostInfo
350
434
  self.loop = loop
351
435
  self.replPort = None
352
- self.__dict__['__proxy_oneway_methods'] = [
353
- 'notify',
354
- 'updateDeviceState',
355
- 'setSystemState',
356
- 'ioEvent',
357
- 'setNativeId',
436
+ self.__dict__["__proxy_oneway_methods"] = [
437
+ "notify",
438
+ "updateDeviceState",
439
+ "setSystemState",
440
+ "ioEvent",
441
+ "setNativeId",
358
442
  ]
359
443
 
360
- async def print_async(self, nativeId: str, *values: object, sep: Optional[str] = ' ',
361
- end: Optional[str] = '\n',
362
- flush: bool = False,):
444
+ async def print_async(
445
+ self,
446
+ nativeId: str,
447
+ *values: object,
448
+ sep: Optional[str] = " ",
449
+ end: Optional[str] = "\n",
450
+ flush: bool = False,
451
+ ):
363
452
  consoleFuture = self.consoles.get(nativeId)
364
453
  if not consoleFuture:
365
454
  consoleFuture = Future()
366
455
  self.consoles[nativeId] = consoleFuture
367
- plugins = await self.api.getComponent('plugins')
368
- port = await plugins.getRemoteServicePort(self.pluginId, 'console-writer')
456
+ plugins = await self.api.getComponent("plugins")
457
+ port = await plugins.getRemoteServicePort(self.pluginId, "console-writer")
369
458
  connection = await asyncio.open_connection(port=port)
370
459
  _, writer = connection
371
460
  if not nativeId:
372
- nid = 'undefined'
461
+ nid = "undefined"
373
462
  else:
374
463
  nid = nativeId
375
- nid += '\n'
376
- writer.write(nid.encode('utf8'))
464
+ nid += "\n"
465
+ writer.write(nid.encode("utf8"))
377
466
  consoleFuture.set_result(connection)
378
467
  _, writer = await consoleFuture
379
468
  strio = StringIO()
380
469
  print(*values, sep=sep, end=end, flush=flush, file=strio)
381
470
  strio.seek(0)
382
- b = strio.read().encode('utf8')
471
+ b = strio.read().encode("utf8")
383
472
  writer.write(b)
384
473
 
385
- def print(self, nativeId: str, *values: object, sep: Optional[str] = ' ',
386
- end: Optional[str] = '\n',
387
- flush: bool = False,):
388
- asyncio.run_coroutine_threadsafe(self.print_async(
389
- nativeId, *values, sep=sep, end=end, flush=flush), self.loop)
474
+ def print(
475
+ self,
476
+ nativeId: str,
477
+ *values: object,
478
+ sep: Optional[str] = " ",
479
+ end: Optional[str] = "\n",
480
+ flush: bool = False,
481
+ ):
482
+ asyncio.run_coroutine_threadsafe(
483
+ self.print_async(nativeId, *values, sep=sep, end=end, flush=flush),
484
+ self.loop,
485
+ )
390
486
 
391
487
  async def loadZip(self, packageJson, getZip: Any, options: dict):
392
488
  try:
393
489
  return await self.loadZipWrapped(packageJson, getZip, options)
394
490
  except:
395
- print('plugin start/fork failed')
491
+ print("plugin start/fork failed")
396
492
  traceback.print_exc()
397
493
  raise
398
494
 
399
495
  async def loadZipWrapped(self, packageJson, getZip: Any, options: dict):
400
496
  sdk = ScryptedStatic()
401
497
 
402
- clusterId = options['clusterId']
403
- clusterSecret = options['clusterSecret']
498
+ clusterId = options["clusterId"]
499
+ clusterSecret = options["clusterSecret"]
500
+ SCRYPTED_CLUSTER_ADDRESS = os.environ.get("SCRYPTED_CLUSTER_ADDRESS", None)
404
501
 
405
502
  def computeClusterObjectHash(o: ClusterObject) -> str:
406
503
  m = hashlib.sha256()
407
- m.update(bytes(f"{o['id']}{o['port']}{o.get('sourcePort') or ''}{o['proxyId']}{clusterSecret}", 'utf8'))
408
- return base64.b64encode(m.digest()).decode('utf-8')
409
-
410
- def onProxySerialization(value: Any, proxyId: str, sourcePeerPort: int = None):
504
+ m.update(
505
+ bytes(
506
+ f"{o['id']}{o.get('address') or ''}{o['port']}{o.get('sourceKey', None) or ''}{o['proxyId']}{clusterSecret}",
507
+ "utf8",
508
+ )
509
+ )
510
+ return base64.b64encode(m.digest()).decode("utf-8")
511
+
512
+ def onProxySerialization(value: Any, sourceKey: str = None):
411
513
  properties: dict = rpc.RpcPeer.prepareProxyProperties(value) or {}
412
- clusterEntry = properties.get('__cluster', None)
413
- if not properties.get('__cluster', None):
514
+ clusterEntry = properties.get("__cluster", None)
515
+ proxyId: str = (
516
+ clusterEntry and clusterEntry.get("proxyId", None)
517
+ ) or rpc.RpcPeer.generateId()
518
+
519
+ if (
520
+ clusterEntry
521
+ and clusterPort == clusterEntry["port"]
522
+ and sourceKey != clusterEntry.get("sourceKey", None)
523
+ ):
524
+ clusterEntry = None
525
+
526
+ if not clusterEntry:
414
527
  clusterEntry: ClusterObject = {
415
- 'id': clusterId,
416
- 'proxyId': proxyId,
417
- 'port': clusterPort,
418
- 'sourcePort': sourcePeerPort,
528
+ "id": clusterId,
529
+ "proxyId": proxyId,
530
+ "address": SCRYPTED_CLUSTER_ADDRESS,
531
+ "port": clusterPort,
532
+ "sourceKey": sourceKey,
419
533
  }
420
- clusterEntry['sha256'] = computeClusterObjectHash(clusterEntry)
421
- properties['__cluster'] = clusterEntry
534
+ clusterEntry["sha256"] = computeClusterObjectHash(clusterEntry)
535
+ properties["__cluster"] = clusterEntry
422
536
 
423
- return properties
537
+ return proxyId, properties
424
538
 
425
539
  self.peer.onProxySerialization = onProxySerialization
426
540
 
427
- async def resolveObject(id: str, sourcePeerPort: int):
428
- sourcePeer: rpc.RpcPeer = self.peer if not sourcePeerPort else await rpc.maybe_await(clusterPeers.get(sourcePeerPort))
541
+ async def resolveObject(id: str, sourceKey: str):
542
+ sourcePeer: rpc.RpcPeer = (
543
+ self.peer
544
+ if not sourceKey
545
+ else await rpc.maybe_await(clusterPeers.get(sourceKey, None))
546
+ )
429
547
  if not sourcePeer:
430
548
  return
431
549
  return sourcePeer.localProxyMap.get(id, None)
432
550
 
433
- clusterPeers: Mapping[int, asyncio.Future[rpc.RpcPeer]] = {}
551
+ clusterPeers: Mapping[str, asyncio.Future[rpc.RpcPeer]] = {}
434
552
 
435
- async def handleClusterClient(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
436
- _, clusterPeerPort = writer.get_extra_info('peername')
553
+ def getClusterPeerKey(address: str, port: int):
554
+ return f"{address}:{port}"
555
+
556
+ async def handleClusterClient(
557
+ reader: asyncio.StreamReader, writer: asyncio.StreamWriter
558
+ ):
559
+ clusterPeerAddress, clusterPeerPort = writer.get_extra_info("peername")
560
+ clusterPeerKey = getClusterPeerKey(clusterPeerAddress, clusterPeerPort)
437
561
  rpcTransport = rpc_reader.RpcStreamTransport(reader, writer)
438
562
  peer: rpc.RpcPeer
439
- peer, peerReadLoop = await rpc_reader.prepare_peer_readloop(self.loop, rpcTransport)
440
- peer.onProxySerialization = lambda value, proxyId: onProxySerialization(
441
- value, proxyId, clusterPeerPort)
563
+ peer, peerReadLoop = await rpc_reader.prepare_peer_readloop(
564
+ self.loop, rpcTransport
565
+ )
566
+ peer.onProxySerialization = lambda value: onProxySerialization(
567
+ value, clusterPeerPort
568
+ )
442
569
  future: asyncio.Future[rpc.RpcPeer] = asyncio.Future()
443
570
  future.set_result(peer)
444
- clusterPeers[clusterPeerPort] = future
571
+ clusterPeers[clusterPeerKey] = future
445
572
 
446
573
  async def connectRPCObject(o: ClusterObject):
447
574
  sha256 = computeClusterObjectHash(o)
448
- if sha256 != o['sha256']:
449
- raise Exception('secret incorrect')
450
- return await resolveObject(o['proxyId'], o.get('sourcePort'))
575
+ if sha256 != o["sha256"]:
576
+ raise Exception("secret incorrect")
577
+ return await resolveObject(o["proxyId"], o.get("sourceKey", None))
451
578
 
452
- peer.params['connectRPCObject'] = connectRPCObject
579
+ peer.params["connectRPCObject"] = connectRPCObject
453
580
  try:
454
581
  await peerReadLoop()
455
582
  except:
456
583
  pass
457
584
  finally:
458
- clusterPeers.pop(clusterPeerPort)
459
- peer.kill('cluster client killed')
585
+ clusterPeers.pop(clusterPeerKey)
586
+ peer.kill("cluster client killed")
460
587
  writer.close()
461
588
 
462
- clusterRpcServer = await asyncio.start_server(handleClusterClient, '127.0.0.1', 0)
589
+ listenAddress = "0.0.0.0" if SCRYPTED_CLUSTER_ADDRESS else "127.0.0.1"
590
+ clusterRpcServer = await asyncio.start_server(
591
+ handleClusterClient, listenAddress, 0
592
+ )
463
593
  clusterPort = clusterRpcServer.sockets[0].getsockname()[1]
464
594
 
465
- def ensureClusterPeer(port: int):
466
- clusterPeerPromise = clusterPeers.get(port)
467
- if not clusterPeerPromise:
468
- async def connectClusterPeer():
469
- reader, writer = await asyncio.open_connection(
470
- '127.0.0.1', port)
471
- _, clusterPeerPort = writer.get_extra_info('sockname')
472
- rpcTransport = rpc_reader.RpcStreamTransport(
473
- reader, writer)
474
- clusterPeer, peerReadLoop = await rpc_reader.prepare_peer_readloop(self.loop, rpcTransport)
475
- clusterPeer.tags['localPort'] = clusterPeerPort
476
- clusterPeer.onProxySerialization = lambda value, proxyId: onProxySerialization(
477
- value, proxyId, clusterPeerPort)
478
-
479
- async def run_loop():
480
- try:
481
- await peerReadLoop()
482
- except:
483
- pass
484
- finally:
485
- clusterPeers.pop(port)
486
- asyncio.run_coroutine_threadsafe(run_loop(), self.loop)
487
- return clusterPeer
488
- clusterPeerPromise = self.loop.create_task(
489
- connectClusterPeer())
490
- clusterPeers[port] = clusterPeerPromise
595
+ def ensureClusterPeer(address: str, port: int):
596
+ if not address or address == SCRYPTED_CLUSTER_ADDRESS:
597
+ address = "127.0.0.1"
598
+ clusterPeerKey = getClusterPeerKey(address, port)
599
+ clusterPeerPromise = clusterPeers.get(clusterPeerKey)
600
+ if clusterPeerPromise:
601
+ return clusterPeerPromise
602
+
603
+ async def connectClusterPeer():
604
+ reader, writer = await asyncio.open_connection(address, port)
605
+ sourceAddress, sourcePort = writer.get_extra_info("sockname")
606
+ if (
607
+ sourceAddress != SCRYPTED_CLUSTER_ADDRESS
608
+ and sourceAddress != "127.0.0.1"
609
+ ):
610
+ print("source address mismatch", sourceAddress)
611
+ sourceKey = getClusterPeerKey(sourceAddress, sourcePort)
612
+ rpcTransport = rpc_reader.RpcStreamTransport(reader, writer)
613
+ clusterPeer, peerReadLoop = await rpc_reader.prepare_peer_readloop(
614
+ self.loop, rpcTransport
615
+ )
616
+ clusterPeer.onProxySerialization = lambda value: onProxySerialization(
617
+ value, sourceKey
618
+ )
619
+
620
+ async def run_loop():
621
+ try:
622
+ await peerReadLoop()
623
+ except:
624
+ pass
625
+ finally:
626
+ clusterPeers.pop(clusterPeerKey)
627
+
628
+ asyncio.run_coroutine_threadsafe(run_loop(), self.loop)
629
+ return clusterPeer
630
+
631
+ clusterPeerPromise = self.loop.create_task(connectClusterPeer())
632
+
633
+ clusterPeers[clusterPeerKey] = clusterPeerPromise
491
634
  return clusterPeerPromise
492
635
 
493
636
  async def connectRPCObject(value):
494
- clusterObject = getattr(value, '__cluster')
495
- if type(clusterObject) is not dict:
637
+ __cluster = getattr(value, "__cluster")
638
+ if type(__cluster) is not dict:
496
639
  return value
497
640
 
498
- if clusterObject.get('id', None) != clusterId:
641
+ clusterObject: ClusterObject = __cluster
642
+
643
+ if clusterObject.get("id", None) != clusterId:
499
644
  return value
500
645
 
501
- port = clusterObject['port']
502
- proxyId = clusterObject['proxyId']
503
- sourcePort = clusterObject.get('sourcePort', None)
646
+ address = clusterObject.get("address", None)
647
+ port = clusterObject["port"]
648
+ proxyId = clusterObject["proxyId"]
504
649
  if port == clusterPort:
505
- return await resolveObject(proxyId, sourcePort)
650
+ return await resolveObject(
651
+ proxyId, clusterObject.get("sourceKey", None)
652
+ )
506
653
 
507
- clusterPeerPromise = ensureClusterPeer(port)
654
+ clusterPeerPromise = ensureClusterPeer(address, port)
508
655
 
509
656
  try:
510
657
  clusterPeer = await clusterPeerPromise
511
- if clusterPeer.tags.get('localPort') == sourcePort:
512
- return value
513
- peerConnectRPCObject = clusterPeer.tags.get('connectRPCObject')
658
+ weakref = clusterPeer.remoteWeakProxies.get(proxyId, None)
659
+ existing = weakref() if weakref else None
660
+ if existing:
661
+ return existing
662
+
663
+ peerConnectRPCObject = clusterPeer.tags.get("connectRPCObject")
514
664
  if not peerConnectRPCObject:
515
- peerConnectRPCObject = await clusterPeer.getParam('connectRPCObject')
516
- clusterPeer.tags['connectRPCObject'] = peerConnectRPCObject
665
+ peerConnectRPCObject = await clusterPeer.getParam(
666
+ "connectRPCObject"
667
+ )
668
+ clusterPeer.tags["connectRPCObject"] = peerConnectRPCObject
517
669
  newValue = await peerConnectRPCObject(clusterObject)
518
670
  if not newValue:
519
- raise Exception('ipc object not found?')
671
+ raise Exception("rpc object not found?")
520
672
  return newValue
521
673
  except Exception as e:
522
674
  return value
523
675
 
524
676
  sdk.connectRPCObject = connectRPCObject
525
677
 
526
- forkMain = options and options.get('fork')
527
- debug = options.get('debug', None)
678
+ forkMain = options and options.get("fork")
679
+ debug = options.get("debug", None)
528
680
  plugin_volume = pv.ensure_plugin_volume(self.pluginId)
529
- plugin_zip_paths = pv.prep(plugin_volume, options.get('zipHash'))
681
+ plugin_zip_paths = pv.prep(plugin_volume, options.get("zipHash"))
530
682
 
531
683
  if debug:
532
684
  scrypted_volume = pv.get_scrypted_volume()
533
685
  # python debugger needs a predictable path for the plugin.zip,
534
686
  # as the vscode python extension doesn't seem to have a way
535
687
  # to read the package.json to configure the python remoteRoot.
536
- zipPath = os.path.join(scrypted_volume, 'plugin.zip')
688
+ zipPath = os.path.join(scrypted_volume, "plugin.zip")
537
689
  else:
538
- zipPath = plugin_zip_paths.get('zip_file')
690
+ zipPath = plugin_zip_paths.get("zip_file")
539
691
 
540
692
  if not os.path.exists(zipPath) or debug:
541
693
  os.makedirs(os.path.dirname(zipPath), exist_ok=True)
542
694
  zipData = await getZip()
543
- zipPathTmp = zipPath + '.tmp'
544
- with open(zipPathTmp, 'wb') as f:
695
+ zipPathTmp = zipPath + ".tmp"
696
+ with open(zipPathTmp, "wb") as f:
545
697
  f.write(zipData)
546
698
  try:
547
699
  os.remove(zipPath)
@@ -552,69 +704,98 @@ class PluginRemote:
552
704
  zip = zipfile.ZipFile(zipPath)
553
705
 
554
706
  if not forkMain:
555
- multiprocessing.set_start_method('spawn')
707
+ multiprocessing.set_start_method("spawn")
556
708
 
557
709
  # it's possible to run 32bit docker on aarch64, which cause pip requirements
558
710
  # to fail because pip only allows filtering on machine, even if running a different architeture.
559
711
  # this will cause prebuilt wheel installation to fail.
560
- if platform.machine() == 'aarch64' and platform.architecture()[0] == '32bit':
561
- print('=============================================')
712
+ if (
713
+ platform.machine() == "aarch64"
714
+ and platform.architecture()[0] == "32bit"
715
+ ):
716
+ print("=============================================")
562
717
  print(
563
- 'Python machine vs architecture mismatch detected. Plugin installation may fail.')
718
+ "Python machine vs architecture mismatch detected. Plugin installation may fail."
719
+ )
564
720
  print(
565
- 'This issue occurs if a 32bit system was upgraded to a 64bit kernel.')
721
+ "This issue occurs if a 32bit system was upgraded to a 64bit kernel."
722
+ )
566
723
  print(
567
- 'Reverting to the 32bit kernel (or reflashing as native 64 bit is recommended.')
568
- print('https://github.com/koush/scrypted/issues/678')
569
- print('=============================================')
724
+ "Reverting to the 32bit kernel (or reflashing as native 64 bit is recommended."
725
+ )
726
+ print("https://github.com/koush/scrypted/issues/678")
727
+ print("=============================================")
570
728
 
571
- python_version = 'python%s' % str(
572
- sys.version_info[0])+"."+str(sys.version_info[1])
573
- print('python version:', python_version)
574
- print('interpreter:', sys.executable)
729
+ python_version = (
730
+ "python%s" % str(sys.version_info[0]) + "." + str(sys.version_info[1])
731
+ )
732
+ print("python version:", python_version)
733
+ print("interpreter:", sys.executable)
575
734
 
576
- python_versioned_directory = '%s-%s-%s' % (
577
- python_version, platform.system(), platform.machine())
578
- SCRYPTED_PYTHON_VERSION = os.environ.get('SCRYPTED_PYTHON_VERSION')
579
- python_versioned_directory += '-' + SCRYPTED_PYTHON_VERSION
735
+ python_versioned_directory = "%s-%s-%s" % (
736
+ python_version,
737
+ platform.system(),
738
+ platform.machine(),
739
+ )
740
+ SCRYPTED_PYTHON_VERSION = os.environ.get("SCRYPTED_PYTHON_VERSION")
741
+ python_versioned_directory += "-" + SCRYPTED_PYTHON_VERSION
580
742
 
581
- pip_target = os.path.join(
582
- plugin_volume, python_versioned_directory)
743
+ pip_target = os.path.join(plugin_volume, python_versioned_directory)
583
744
 
584
- print('pip target: %s' % pip_target)
745
+ print("pip target: %s" % pip_target)
585
746
 
586
747
  if not os.path.exists(pip_target):
587
748
  os.makedirs(pip_target, exist_ok=True)
588
749
 
589
-
590
750
  def read_requirements(filename: str) -> str:
591
751
  if filename in zip.namelist():
592
- return zip.open(filename).read().decode('utf8')
593
- return ''
752
+ return zip.open(filename).read().decode("utf8")
753
+ return ""
594
754
 
595
- str_requirements = read_requirements('requirements.txt')
596
- str_optional_requirements = read_requirements('requirements.optional.txt')
755
+ str_requirements = read_requirements("requirements.txt")
756
+ str_optional_requirements = read_requirements("requirements.optional.txt")
597
757
 
598
758
  scrypted_requirements_basename = os.path.join(
599
- pip_target, 'requirements.scrypted')
600
- requirements_basename = os.path.join(
601
- pip_target, 'requirements')
759
+ pip_target, "requirements.scrypted"
760
+ )
761
+ requirements_basename = os.path.join(pip_target, "requirements")
602
762
  optional_requirements_basename = os.path.join(
603
- pip_target, 'requirements.optional')
763
+ pip_target, "requirements.optional"
764
+ )
604
765
 
605
766
  need_pip = True
606
767
  if str_requirements:
607
768
  need_pip = need_requirements(requirements_basename, str_requirements)
608
769
  if not need_pip:
609
- need_pip = need_requirements(scrypted_requirements_basename, SCRYPTED_REQUIREMENTS)
770
+ need_pip = need_requirements(
771
+ scrypted_requirements_basename, SCRYPTED_REQUIREMENTS
772
+ )
610
773
 
611
774
  if need_pip:
612
775
  remove_pip_dirs(plugin_volume)
613
- install_with_pip(pip_target, packageJson, SCRYPTED_REQUIREMENTS, scrypted_requirements_basename, ignore_error=True)
614
- install_with_pip(pip_target, packageJson, str_requirements, requirements_basename, ignore_error=False)
615
- install_with_pip(pip_target, packageJson, str_optional_requirements, optional_requirements_basename, ignore_error=True)
776
+ install_with_pip(
777
+ pip_target,
778
+ packageJson,
779
+ SCRYPTED_REQUIREMENTS,
780
+ scrypted_requirements_basename,
781
+ ignore_error=True,
782
+ )
783
+ install_with_pip(
784
+ pip_target,
785
+ packageJson,
786
+ str_requirements,
787
+ requirements_basename,
788
+ ignore_error=False,
789
+ )
790
+ install_with_pip(
791
+ pip_target,
792
+ packageJson,
793
+ str_optional_requirements,
794
+ optional_requirements_basename,
795
+ ignore_error=True,
796
+ )
616
797
  else:
617
- print('requirements.txt (up to date)')
798
+ print("requirements.txt (up to date)")
618
799
  print(str_requirements)
619
800
 
620
801
  sys.path.insert(0, zipPath)
@@ -639,9 +820,10 @@ class PluginRemote:
639
820
  def host_fork() -> PluginFork:
640
821
  parent_conn, child_conn = multiprocessing.Pipe()
641
822
  pluginFork = PluginFork()
642
- print('new fork')
823
+ print("new fork")
643
824
  pluginFork.worker = multiprocessing.Process(
644
- target=plugin_fork, args=(child_conn,), daemon=True)
825
+ target=plugin_fork, args=(child_conn,), daemon=True
826
+ )
645
827
  pluginFork.worker.start()
646
828
 
647
829
  def schedule_exit_check():
@@ -650,42 +832,47 @@ class PluginRemote:
650
832
  pluginFork.worker.join()
651
833
  else:
652
834
  schedule_exit_check()
835
+
653
836
  self.loop.call_later(2, exit_check)
654
837
 
655
838
  schedule_exit_check()
656
839
 
657
840
  async def getFork():
658
- rpcTransport = rpc_reader.RpcConnectionTransport(
659
- parent_conn)
660
- forkPeer, readLoop = await rpc_reader.prepare_peer_readloop(self.loop, rpcTransport)
661
- forkPeer.peerName = 'thread'
841
+ rpcTransport = rpc_reader.RpcConnectionTransport(parent_conn)
842
+ forkPeer, readLoop = await rpc_reader.prepare_peer_readloop(
843
+ self.loop, rpcTransport
844
+ )
845
+ forkPeer.peerName = "thread"
662
846
 
663
847
  async def updateStats(stats):
664
- self.ptimeSum += stats['cpu']['user']
848
+ self.ptimeSum += stats["cpu"]["user"]
665
849
  self.allMemoryStats[forkPeer] = stats
666
- forkPeer.params['updateStats'] = updateStats
850
+
851
+ forkPeer.params["updateStats"] = updateStats
667
852
 
668
853
  async def forkReadLoop():
669
854
  try:
670
855
  await readLoop()
671
856
  except:
672
857
  # traceback.print_exc()
673
- print('fork read loop exited')
858
+ print("fork read loop exited")
674
859
  finally:
675
860
  self.allMemoryStats.pop(forkPeer)
676
861
  parent_conn.close()
677
862
  rpcTransport.executor.shutdown()
678
863
  pluginFork.worker.kill()
679
- asyncio.run_coroutine_threadsafe(
680
- forkReadLoop(), loop=self.loop)
681
- getRemote = await forkPeer.getParam('getRemote')
682
- remote: PluginRemote = await getRemote(self.api, self.pluginId, self.hostInfo)
864
+
865
+ asyncio.run_coroutine_threadsafe(forkReadLoop(), loop=self.loop)
866
+ getRemote = await forkPeer.getParam("getRemote")
867
+ remote: PluginRemote = await getRemote(
868
+ self.api, self.pluginId, self.hostInfo
869
+ )
683
870
  await remote.setSystemState(self.systemManager.getSystemState())
684
871
  for nativeId, ds in self.nativeIds.items():
685
872
  await remote.setNativeId(nativeId, ds.id, ds.storage)
686
873
  forkOptions = options.copy()
687
- forkOptions['fork'] = True
688
- forkOptions['debug'] = debug
874
+ forkOptions["fork"] = True
875
+ forkOptions["debug"] = debug
689
876
  return await remote.loadZip(packageJson, getZip, forkOptions)
690
877
 
691
878
  pluginFork.result = asyncio.create_task(getFork())
@@ -697,14 +884,18 @@ class PluginRemote:
697
884
  sdk_init2(sdk)
698
885
  except:
699
886
  from scrypted_sdk import sdk_init # type: ignore
700
- sdk_init(zip, self, self.systemManager,
701
- self.deviceManager, self.mediaManager)
887
+
888
+ sdk_init(
889
+ zip, self, self.systemManager, self.deviceManager, self.mediaManager
890
+ )
702
891
 
703
892
  if not forkMain:
704
893
  from main import create_scrypted_plugin # type: ignore
894
+
705
895
  pluginInstance = await rpc.maybe_await(create_scrypted_plugin())
706
896
  try:
707
897
  from plugin_repl import createREPLServer
898
+
708
899
  self.replPort = await createREPLServer(sdk, pluginInstance)
709
900
  except Exception as e:
710
901
  print(f"Warning: Python REPL cannot be loaded: {e}")
@@ -712,6 +903,7 @@ class PluginRemote:
712
903
  return pluginInstance
713
904
 
714
905
  from main import fork # type: ignore
906
+
715
907
  forked = await rpc.maybe_await(fork())
716
908
  if type(forked) == dict:
717
909
  forked[rpc.RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN] = True
@@ -739,13 +931,13 @@ class PluginRemote:
739
931
  self.systemState[id] = state
740
932
 
741
933
  async def notify(self, id, eventDetails: EventDetails, value):
742
- property = eventDetails.get('property')
934
+ property = eventDetails.get("property")
743
935
  if property:
744
936
  state = None
745
937
  if self.systemState:
746
938
  state = self.systemState.get(id, None)
747
939
  if not state:
748
- print('state not found for %s' % id)
940
+ print("state not found for %s" % id)
749
941
  return
750
942
  state[property] = value
751
943
  # systemManager.events.notify(id, eventTime, eventInterface, property, value.value, changed);
@@ -757,54 +949,57 @@ class PluginRemote:
757
949
  pass
758
950
 
759
951
  async def createDeviceState(self, id, setState):
760
- pass
952
+ return WritableDeviceState(id, setState)
761
953
 
762
954
  async def getServicePort(self, name):
763
955
  if name == "repl":
764
956
  if self.replPort is None:
765
- raise Exception('REPL unavailable: Plugin not loaded.')
957
+ raise Exception("REPL unavailable: Plugin not loaded.")
766
958
  if self.replPort == 0:
767
- raise Exception('REPL unavailable: Python REPL not available.')
959
+ raise Exception("REPL unavailable: Python REPL not available.")
768
960
  return self.replPort
769
- raise Exception(f'unknown service {name}')
961
+ raise Exception(f"unknown service {name}")
770
962
 
771
963
  async def start_stats_runner(self):
772
964
  pong = None
965
+
773
966
  async def ping(time: int):
774
967
  nonlocal pong
775
- pong = pong or await self.peer.getParam('pong')
968
+ pong = pong or await self.peer.getParam("pong")
776
969
  await pong(time)
777
- self.peer.params['ping'] = ping
778
970
 
779
- update_stats = await self.peer.getParam('updateStats')
971
+ self.peer.params["ping"] = ping
972
+
973
+ update_stats = await self.peer.getParam("updateStats")
780
974
  if not update_stats:
781
- print('host did not provide update_stats')
975
+ print("host did not provide update_stats")
782
976
  return
783
977
 
784
978
  def stats_runner():
785
979
  ptime = round(time.process_time() * 1000000) + self.ptimeSum
786
980
  try:
787
981
  import psutil
982
+
788
983
  process = psutil.Process(os.getpid())
789
984
  heapTotal = process.memory_info().rss
790
985
  except:
791
986
  try:
792
987
  import resource
793
- heapTotal = resource.getrusage(
794
- resource.RUSAGE_SELF).ru_maxrss
988
+
989
+ heapTotal = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
795
990
  except:
796
991
  heapTotal = 0
797
992
 
798
993
  for _, stats in self.allMemoryStats.items():
799
- heapTotal += stats['memoryUsage']['heapTotal']
994
+ heapTotal += stats["memoryUsage"]["heapTotal"]
800
995
 
801
996
  stats = {
802
- 'cpu': {
803
- 'user': ptime,
804
- 'system': 0,
997
+ "cpu": {
998
+ "user": ptime,
999
+ "system": 0,
805
1000
  },
806
- 'memoryUsage': {
807
- 'heapTotal': heapTotal,
1001
+ "memoryUsage": {
1002
+ "heapTotal": heapTotal,
808
1003
  },
809
1004
  }
810
1005
  asyncio.run_coroutine_threadsafe(update_stats(stats), self.loop)
@@ -813,11 +1008,14 @@ class PluginRemote:
813
1008
  stats_runner()
814
1009
 
815
1010
 
816
- async def plugin_async_main(loop: AbstractEventLoop, rpcTransport: rpc_reader.RpcTransport):
1011
+ async def plugin_async_main(
1012
+ loop: AbstractEventLoop, rpcTransport: rpc_reader.RpcTransport
1013
+ ):
817
1014
  peer, readLoop = await rpc_reader.prepare_peer_readloop(loop, rpcTransport)
818
- peer.params['print'] = print
819
- peer.params['getRemote'] = lambda api, pluginId, hostInfo: PluginRemote(
820
- peer, api, pluginId, hostInfo, loop)
1015
+ peer.params["print"] = print
1016
+ peer.params["getRemote"] = lambda api, pluginId, hostInfo: PluginRemote(
1017
+ peer, api, pluginId, hostInfo, loop
1018
+ )
821
1019
 
822
1020
  try:
823
1021
  await readLoop()
@@ -831,6 +1029,7 @@ def main(rpcTransport: rpc_reader.RpcTransport):
831
1029
  def gc_runner():
832
1030
  gc.collect()
833
1031
  loop.call_later(10, gc_runner)
1032
+
834
1033
  gc_runner()
835
1034
 
836
1035
  loop.run_until_complete(plugin_async_main(loop, rpcTransport))
@@ -850,8 +1049,10 @@ def plugin_main(rpcTransport: rpc_reader.RpcTransport):
850
1049
  # if it does, try starting without it.
851
1050
  try:
852
1051
  import gi
853
- gi.require_version('Gst', '1.0')
1052
+
1053
+ gi.require_version("Gst", "1.0")
854
1054
  from gi.repository import GLib, Gst
1055
+
855
1056
  Gst.init(None)
856
1057
 
857
1058
  # can't remember why starting the glib main loop is necessary.
@@ -859,8 +1060,9 @@ def plugin_main(rpcTransport: rpc_reader.RpcTransport):
859
1060
  # seems optional on other platforms.
860
1061
  loop = GLib.MainLoop()
861
1062
 
862
- worker = threading.Thread(target=main, args=(
863
- rpcTransport,), name="asyncio-main")
1063
+ worker = threading.Thread(
1064
+ target=main, args=(rpcTransport,), name="asyncio-main"
1065
+ )
864
1066
  worker.start()
865
1067
 
866
1068
  loop.run()