@scrypted/server 0.123.31 → 0.123.33

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 (67) hide show
  1. package/dist/plugin/device.d.ts +60 -0
  2. package/dist/plugin/device.js +249 -0
  3. package/dist/plugin/device.js.map +1 -0
  4. package/dist/plugin/endpoint.d.ts +45 -0
  5. package/dist/plugin/endpoint.js +97 -0
  6. package/dist/plugin/endpoint.js.map +1 -0
  7. package/dist/plugin/plugin-host.d.ts +1 -1
  8. package/dist/plugin/plugin-host.js +11 -10
  9. package/dist/plugin/plugin-host.js.map +1 -1
  10. package/dist/plugin/plugin-remote-worker.js +18 -16
  11. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  12. package/dist/plugin/plugin-remote.d.ts +2 -51
  13. package/dist/plugin/plugin-remote.js +6 -337
  14. package/dist/plugin/plugin-remote.js.map +1 -1
  15. package/dist/plugin/runtime/child-process-worker.d.ts +1 -1
  16. package/dist/plugin/runtime/child-process-worker.js +2 -2
  17. package/dist/plugin/runtime/child-process-worker.js.map +1 -1
  18. package/dist/plugin/runtime/cluster-fork-worker.d.ts +4 -4
  19. package/dist/plugin/runtime/cluster-fork-worker.js +8 -3
  20. package/dist/plugin/runtime/cluster-fork-worker.js.map +1 -1
  21. package/dist/plugin/runtime/custom-worker.d.ts +1 -1
  22. package/dist/plugin/runtime/custom-worker.js +3 -3
  23. package/dist/plugin/runtime/custom-worker.js.map +1 -1
  24. package/dist/plugin/runtime/node-fork-worker.d.ts +1 -1
  25. package/dist/plugin/runtime/node-fork-worker.js +2 -2
  26. package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
  27. package/dist/plugin/runtime/python-worker.d.ts +1 -1
  28. package/dist/plugin/runtime/python-worker.js +3 -3
  29. package/dist/plugin/runtime/python-worker.js.map +1 -1
  30. package/dist/plugin/runtime/runtime-host.d.ts +1 -1
  31. package/dist/plugin/runtime/runtime-host.js +3 -3
  32. package/dist/plugin/runtime/runtime-host.js.map +1 -1
  33. package/dist/plugin/system.js +0 -1
  34. package/dist/plugin/system.js.map +1 -1
  35. package/dist/runtime.d.ts +4 -4
  36. package/dist/runtime.js +1 -1
  37. package/dist/runtime.js.map +1 -1
  38. package/dist/scrypted-cluster-main.d.ts +14 -3
  39. package/dist/scrypted-cluster-main.js +42 -18
  40. package/dist/scrypted-cluster-main.js.map +1 -1
  41. package/dist/scrypted-server-main.js +1 -0
  42. package/dist/scrypted-server-main.js.map +1 -1
  43. package/dist/services/cluster-fork.d.ts +4 -3
  44. package/dist/services/cluster-fork.js +7 -5
  45. package/dist/services/cluster-fork.js.map +1 -1
  46. package/package.json +2 -2
  47. package/python/cluster_labels.py +2 -2
  48. package/python/cluster_setup.py +1 -1
  49. package/python/plugin_console.py +8 -0
  50. package/python/plugin_remote.py +116 -34
  51. package/python/rpc.py +5 -4
  52. package/src/plugin/device.ts +261 -0
  53. package/src/plugin/endpoint.ts +109 -0
  54. package/src/plugin/plugin-host.ts +25 -21
  55. package/src/plugin/plugin-remote-worker.ts +30 -20
  56. package/src/plugin/plugin-remote.ts +6 -364
  57. package/src/plugin/runtime/child-process-worker.ts +3 -1
  58. package/src/plugin/runtime/cluster-fork-worker.ts +20 -12
  59. package/src/plugin/runtime/custom-worker.ts +3 -3
  60. package/src/plugin/runtime/node-fork-worker.ts +2 -2
  61. package/src/plugin/runtime/python-worker.ts +3 -3
  62. package/src/plugin/runtime/runtime-host.ts +4 -4
  63. package/src/plugin/system.ts +0 -1
  64. package/src/runtime.ts +4 -4
  65. package/src/scrypted-cluster-main.ts +54 -29
  66. package/src/scrypted-server-main.ts +1 -0
  67. package/src/services/cluster-fork.ts +10 -8
@@ -18,7 +18,7 @@ from asyncio.streams import StreamReader, StreamWriter
18
18
  from collections.abc import Mapping
19
19
  from io import StringIO
20
20
  from typing import Any, Callable, Coroutine, Optional, Set, Tuple, TypedDict
21
-
21
+ import plugin_console
22
22
  import plugin_volume as pv
23
23
  import rpc
24
24
  import rpc_reader
@@ -202,6 +202,21 @@ class EventRegistry(object):
202
202
 
203
203
  return True
204
204
 
205
+ class ClusterManager(scrypted_python.scrypted_sdk.types.ClusterManager):
206
+ def __init__(self, api: Any):
207
+ self.api = api
208
+ self.clusterService = None
209
+
210
+ def getClusterMode(self) -> Any | Any:
211
+ return os.getenv("SCRYPTED_CLUSTER_MODE", None)
212
+
213
+ def getClusterWorkerId(self) -> str:
214
+ return os.getenv("SCRYPTED_CLUSTER_WORKER_ID", None)
215
+
216
+ async def getClusterWorkers(self) -> Mapping[str, scrypted_python.scrypted_sdk.types.ClusterWorker]:
217
+ self.clusterService = self.clusterService or asyncio.ensure_future(self.api.getComponent("cluster-fork"))
218
+ cs = await self.clusterService
219
+ return await cs.getClusterWorkers()
205
220
 
206
221
  class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
207
222
  def __init__(
@@ -547,6 +562,12 @@ class PeerLiveness:
547
562
  async def waitKilled(self):
548
563
  await self.killed
549
564
 
565
+ def safe_set_result(fut: Future, result: Any):
566
+ try:
567
+ fut.set_result(result)
568
+ except:
569
+ pass
570
+
550
571
  class PluginRemote:
551
572
  def __init__(
552
573
  self, clusterSetup: ClusterSetup, api, pluginId: str, hostInfo, loop: AbstractEventLoop
@@ -554,6 +575,7 @@ class PluginRemote:
554
575
  self.systemState: Mapping[str, Mapping[str, SystemDeviceState]] = {}
555
576
  self.nativeIds: Mapping[str, DeviceStorage] = {}
556
577
  self.mediaManager: MediaManager
578
+ self.clusterManager: ClusterManager
557
579
  self.consoles: Mapping[str, Future[Tuple[StreamReader, StreamWriter]]] = {}
558
580
  self.peer = clusterSetup.peer
559
581
  self.clusterSetup = clusterSetup
@@ -622,17 +644,17 @@ class PluginRemote:
622
644
  traceback.print_exc()
623
645
  raise
624
646
 
625
- async def loadZipWrapped(self, packageJson, zipAPI: Any, options: dict):
626
- await self.clusterSetup.initializeCluster(options)
647
+ async def loadZipWrapped(self, packageJson, zipAPI: Any, zipOptions: dict):
648
+ await self.clusterSetup.initializeCluster(zipOptions)
627
649
 
628
650
  sdk = ScryptedStatic()
629
651
 
630
652
  sdk.connectRPCObject = lambda v: self.clusterSetup.connectRPCObject(v)
631
653
 
632
- forkMain = options and options.get("fork")
633
- debug = options.get("debug", None)
654
+ forkMain = zipOptions and zipOptions.get("fork")
655
+ debug = zipOptions.get("debug", None)
634
656
  plugin_volume = pv.ensure_plugin_volume(self.pluginId)
635
- zipHash = options.get("zipHash")
657
+ zipHash: str = zipOptions.get("zipHash")
636
658
  plugin_zip_paths = pv.prep(plugin_volume, zipHash)
637
659
 
638
660
  if debug:
@@ -660,6 +682,13 @@ class PluginRemote:
660
682
 
661
683
  if not forkMain:
662
684
  multiprocessing.set_start_method("spawn")
685
+
686
+ # forkMain may be set to true, but the environment may not be initialized
687
+ # if the plugin is loaded in another cluster worker.
688
+ # instead rely on a environemnt variable that will be passed to
689
+ # child processes.
690
+ if not os.environ.get("SCRYPTED_PYTHON_INITIALIZED", None):
691
+ os.environ["SCRYPTED_PYTHON_INITIALIZED"] = "1"
663
692
 
664
693
  # it's possible to run 32bit docker on aarch64, which cause pip requirements
665
694
  # to fail because pip only allows filtering on machine, even if running a different architeture.
@@ -761,25 +790,58 @@ class PluginRemote:
761
790
  self.systemManager = SystemManager(self.api, self.systemState)
762
791
  self.deviceManager = DeviceManager(self.nativeIds, self.systemManager)
763
792
  self.mediaManager = MediaManager(await self.api.getMediaManager())
793
+ self.clusterManager = ClusterManager(self.api)
764
794
 
765
795
  try:
766
- from scrypted_sdk import sdk_init2 # type: ignore
767
-
768
796
  sdk.systemManager = self.systemManager
769
797
  sdk.deviceManager = self.deviceManager
770
798
  sdk.mediaManager = self.mediaManager
799
+ sdk.clusterManager = self.clusterManager
771
800
  sdk.remote = self
772
801
  sdk.api = self.api
773
802
  sdk.zip = zip
774
803
 
775
804
  def host_fork(options: dict = None) -> PluginFork:
805
+ async def finishFork(forkPeer: rpc.RpcPeer):
806
+ getRemote = await forkPeer.getParam("getRemote")
807
+ remote: PluginRemote = await getRemote(
808
+ self.api, self.pluginId, self.hostInfo
809
+ )
810
+ await remote.setSystemState(self.systemManager.getSystemState())
811
+ for nativeId, ds in self.nativeIds.items():
812
+ await remote.setNativeId(nativeId, ds.id, ds.storage)
813
+ forkOptions = zipOptions.copy()
814
+ forkOptions["fork"] = True
815
+ forkOptions["debug"] = debug
816
+
817
+ class PluginZipAPI:
818
+
819
+ async def getZip(self):
820
+ return await zipAPI.getZip()
821
+
822
+ return await remote.loadZip(packageJson, PluginZipAPI(), forkOptions)
823
+
776
824
  if cluster_labels.needs_cluster_fork_worker(options):
777
825
  peerLiveness = PeerLiveness(self.loop)
778
- async def startClusterFork():
826
+ async def getClusterFork():
827
+ runtimeWorkerOptions = {
828
+ "packageJson": packageJson,
829
+ "env": None,
830
+ "pluginDebug": None,
831
+ "zipFile": None,
832
+ "unzippedPath": None,
833
+ "zipHash": zipHash,
834
+ }
835
+
779
836
  forkComponent = await self.api.getComponent("cluster-fork")
780
837
  sanitizedOptions = options.copy()
781
838
  sanitizedOptions["runtime"] = sanitizedOptions.get("runtime", "python")
782
- clusterForkResult = await forkComponent.fork(peerLiveness, sanitizedOptions, packageJson, zipHash, lambda: zipAPI.getZip())
839
+ sanitizedOptions["zipHash"] = zipHash
840
+ clusterForkResult = await forkComponent.fork(
841
+ runtimeWorkerOptions,
842
+ sanitizedOptions,
843
+ peerLiveness, lambda: zipAPI.getZip()
844
+ )
783
845
 
784
846
  async def waitPeerLiveness():
785
847
  try:
@@ -791,20 +853,44 @@ class PluginRemote:
791
853
  pass
792
854
  asyncio.ensure_future(waitPeerLiveness(), loop=self.loop)
793
855
 
794
- async def waitClusterForkResult():
856
+ async def waitClusterForkKilled():
795
857
  try:
796
858
  await clusterForkResult.waitKilled()
797
859
  except:
798
860
  pass
799
- peerLiveness.killed.set_result(None)
800
- asyncio.ensure_future(waitClusterForkResult(), loop=self.loop)
861
+ safe_set_result(peerLiveness.killed, None)
862
+ asyncio.ensure_future(waitClusterForkKilled(), loop=self.loop)
863
+
864
+ clusterGetRemote = await self.clusterSetup.connectRPCObject(await clusterForkResult.getResult())
865
+ remoteDict = await clusterGetRemote()
866
+ asyncio.ensure_future(plugin_console.writeWorkerGenerator(remoteDict["stdout"], sys.stdout))
867
+ asyncio.ensure_future(plugin_console.writeWorkerGenerator(remoteDict["stderr"], sys.stderr))
868
+
869
+ getRemote = remoteDict["getRemote"]
870
+ directGetRemote = await self.clusterSetup.connectRPCObject(getRemote)
871
+ if directGetRemote is getRemote:
872
+ raise Exception("cluster fork peer not direct connected")
873
+
874
+ forkPeer = getattr(directGetRemote, rpc.RpcPeer.PROPERTY_PROXY_PEER)
875
+ return await finishFork(forkPeer)
876
+
801
877
 
802
- result = asyncio.ensure_future(startClusterFork(), loop=self.loop)
803
878
  pluginFork = PluginFork()
804
- pluginFork.result = result
805
- pluginFork.terminate = lambda: peerLiveness.killed.set_result(None)
879
+ pluginFork.result = asyncio.create_task(getClusterFork())
880
+ async def waitKilled():
881
+ await peerLiveness.killed
882
+ pluginFork.exit = asyncio.create_task(waitKilled())
883
+ def terminate():
884
+ safe_set_result(peerLiveness.killed, None)
885
+ pluginFork.worker.terminate()
886
+ pluginFork.terminate = terminate
887
+
888
+ pluginFork.worker = None
889
+
806
890
  return pluginFork
807
891
 
892
+ t: asyncio.Task
893
+ t.cancel()
808
894
  if options:
809
895
  runtime = options.get("runtime", None)
810
896
  if runtime and runtime != "python":
@@ -813,8 +899,17 @@ class PluginRemote:
813
899
  raise Exception("python fork to filename not supported")
814
900
 
815
901
  parent_conn, child_conn = multiprocessing.Pipe()
902
+
816
903
  pluginFork = PluginFork()
817
- print("new fork")
904
+ killed = Future(loop=self.loop)
905
+ async def waitKilled():
906
+ await killed
907
+ pluginFork.exit = asyncio.create_task(waitKilled())
908
+ def terminate():
909
+ safe_set_result(killed, None)
910
+ pluginFork.worker.kill()
911
+ pluginFork.terminate = terminate
912
+
818
913
  pluginFork.worker = multiprocessing.Process(
819
914
  target=plugin_fork, args=(child_conn,), daemon=True
820
915
  )
@@ -823,6 +918,7 @@ class PluginRemote:
823
918
  def schedule_exit_check():
824
919
  def exit_check():
825
920
  if pluginFork.worker.exitcode != None:
921
+ safe_set_result(killed, None)
826
922
  pluginFork.worker.join()
827
923
  else:
828
924
  schedule_exit_check()
@@ -847,26 +943,11 @@ class PluginRemote:
847
943
  finally:
848
944
  parent_conn.close()
849
945
  rpcTransport.executor.shutdown()
850
- pluginFork.worker.kill()
946
+ pluginFork.terminate()
851
947
 
852
948
  asyncio.run_coroutine_threadsafe(forkReadLoop(), loop=self.loop)
853
- getRemote = await forkPeer.getParam("getRemote")
854
- remote: PluginRemote = await getRemote(
855
- self.api, self.pluginId, self.hostInfo
856
- )
857
- await remote.setSystemState(self.systemManager.getSystemState())
858
- for nativeId, ds in self.nativeIds.items():
859
- await remote.setNativeId(nativeId, ds.id, ds.storage)
860
- forkOptions = options.copy()
861
- forkOptions["fork"] = True
862
- forkOptions["debug"] = debug
863
949
 
864
- class PluginZipAPI:
865
-
866
- async def getZip(self):
867
- return await zipAPI.getZip()
868
-
869
- return await remote.loadZip(packageJson, PluginZipAPI(), forkOptions)
950
+ return await finishFork(forkPeer)
870
951
 
871
952
  pluginFork.result = asyncio.create_task(getFork())
872
953
  return pluginFork
@@ -874,6 +955,7 @@ class PluginRemote:
874
955
  sdk.fork = host_fork
875
956
  # sdk.
876
957
 
958
+ from scrypted_sdk import sdk_init2 # type: ignore
877
959
  sdk_init2(sdk)
878
960
  except:
879
961
  from scrypted_sdk import sdk_init # type: ignore
package/python/rpc.py CHANGED
@@ -62,7 +62,7 @@ class RpcProxy(object):
62
62
  self.__dict__['__proxy_id'] = entry['id']
63
63
  self.__dict__['__proxy_entry'] = entry
64
64
  self.__dict__['__proxy_constructor'] = proxyConstructorName
65
- self.__dict__['__proxy_peer'] = peer
65
+ self.__dict__[RpcPeer.PROPERTY_PROXY_PEER] = peer
66
66
  self.__dict__[RpcPeer.PROPERTY_PROXY_PROPERTIES] = proxyProps
67
67
  self.__dict__['__proxy_oneway_methods'] = proxyOneWayMethods
68
68
 
@@ -105,17 +105,18 @@ class RpcProxy(object):
105
105
  return super().__setattr__(name, value)
106
106
 
107
107
  def __call__(self, *args, **kwargs):
108
- return self.__dict__['__proxy_peer'].__apply__(self.__dict__['__proxy_id'], self.__dict__['__proxy_oneway_methods'], None, args)
108
+ return self.__dict__[RpcPeer.PROPERTY_PROXY_PEER].__apply__(self.__dict__['__proxy_id'], self.__dict__['__proxy_oneway_methods'], None, args)
109
109
 
110
110
 
111
111
  def __apply__(self, method: str, args: list):
112
- return self.__dict__['__proxy_peer'].__apply__(self.__dict__['__proxy_id'], self.__dict__['__proxy_oneway_methods'], method, args)
112
+ return self.__dict__[RpcPeer.PROPERTY_PROXY_PEER].__apply__(self.__dict__['__proxy_id'], self.__dict__['__proxy_oneway_methods'], method, args)
113
113
 
114
114
 
115
115
  class RpcPeer:
116
116
  RPC_RESULT_ERROR_NAME = 'RPCResultError'
117
117
  PROPERTY_PROXY_PROPERTIES = '__proxy_props'
118
118
  PROPERTY_JSON_COPY_SERIALIZE_CHILDREN = '__json_copy_serialize_children'
119
+ PROPERTY_PROXY_PEER = '__proxy_peer'
119
120
 
120
121
  def __init__(self, send: Callable[[object, Callable[[Exception], None], Dict], None]) -> None:
121
122
  self.send = send
@@ -288,7 +289,7 @@ class RpcPeer:
288
289
  return ret
289
290
 
290
291
  __proxy_id = getattr(value, '__proxy_id', None)
291
- __proxy_peer = getattr(value, '__proxy_peer', None)
292
+ __proxy_peer = getattr(value, RpcPeer.PROPERTY_PROXY_PEER, None)
292
293
  if __proxy_id and __proxy_peer == self:
293
294
  ret = {
294
295
  '__local_proxy_id': __proxy_id,
@@ -0,0 +1,261 @@
1
+ import { Device, DeviceManager, DeviceManifest, DeviceState, Logger, ScryptedNativeId, WritableDeviceState } from '@scrypted/types';
2
+ import { RpcPeer } from '../rpc';
3
+ import { PluginAPI, PluginLogger } from './plugin-api';
4
+ import { checkProperty } from './plugin-state-check';
5
+ import { SystemManagerImpl } from './system';
6
+
7
+ class DeviceLogger implements Logger {
8
+ nativeId: ScryptedNativeId;
9
+ api: PluginAPI;
10
+ logger: Promise<PluginLogger>;
11
+
12
+ constructor(api: PluginAPI, nativeId: ScryptedNativeId, public console: any) {
13
+ this.api = api;
14
+ this.nativeId = nativeId;
15
+ }
16
+
17
+ async ensureLogger(): Promise<PluginLogger> {
18
+ if (!this.logger)
19
+ this.logger = this.api.getLogger(this.nativeId);
20
+ return await this.logger;
21
+ }
22
+
23
+ async log(level: string, message: string) {
24
+ (await this.ensureLogger()).log(level, message);
25
+ }
26
+
27
+ a(msg: string): void {
28
+ this.log('a', msg);
29
+ }
30
+ async clear() {
31
+ (await this.ensureLogger()).clear();
32
+ }
33
+ async clearAlert(msg: string) {
34
+ (await this.ensureLogger()).clearAlert(msg);
35
+ }
36
+ async clearAlerts() {
37
+ (await this.ensureLogger()).clearAlerts();
38
+ }
39
+ d(msg: string): void {
40
+ this.log('d', msg);
41
+ }
42
+ e(msg: string): void {
43
+ this.log('e', msg);
44
+ }
45
+ i(msg: string): void {
46
+ this.log('i', msg);
47
+ }
48
+ v(msg: string): void {
49
+ this.log('v', msg);
50
+ }
51
+ w(msg: string): void {
52
+ this.log('w', msg);
53
+ }
54
+ }
55
+
56
+ export class DeviceStateProxyHandler implements ProxyHandler<any> {
57
+ constructor(public deviceManager: DeviceManagerImpl, public id: string,
58
+ public setState: (property: string, value: any) => Promise<void>) {
59
+ }
60
+
61
+ get?(target: any, p: PropertyKey, receiver: any) {
62
+ if (p === 'id')
63
+ return this.id;
64
+ if (p === RpcPeer.PROPERTY_PROXY_PROPERTIES)
65
+ return { id: this.id }
66
+ if (p === 'setState')
67
+ return this.setState;
68
+ return this.deviceManager.systemManager.state[this.id][p as string]?.value;
69
+ }
70
+
71
+ set?(target: any, p: PropertyKey, value: any, receiver: any) {
72
+ checkProperty(p.toString(), value);
73
+ this.deviceManager.systemManager.state[this.id][p as string] = {
74
+ value,
75
+ };
76
+ this.setState(p.toString(), value);
77
+ return true;
78
+ }
79
+ }
80
+
81
+ interface DeviceManagerDevice {
82
+ id: string;
83
+ storage: { [key: string]: any };
84
+ }
85
+
86
+ export class DeviceManagerImpl implements DeviceManager {
87
+ api: PluginAPI;
88
+ nativeIds = new Map<string, DeviceManagerDevice>();
89
+ deviceStorage = new Map<string, StorageImpl>();
90
+ mixinStorage = new Map<string, Map<string, StorageImpl>>();
91
+
92
+ constructor(public systemManager: SystemManagerImpl,
93
+ public getDeviceConsole: (nativeId?: ScryptedNativeId) => Console,
94
+ public getMixinConsole: (mixinId: string, nativeId?: ScryptedNativeId) => Console) {
95
+ }
96
+
97
+ async requestRestart() {
98
+ return this.api.requestRestart();
99
+ }
100
+
101
+ getDeviceLogger(nativeId?: ScryptedNativeId): Logger {
102
+ return new DeviceLogger(this.api, nativeId, this.getDeviceConsole?.(nativeId) || console);
103
+ }
104
+
105
+ getDeviceState(nativeId?: any): DeviceState {
106
+ const handler = new DeviceStateProxyHandler(this, this.nativeIds.get(nativeId).id,
107
+ (property, value) => this.api.setState(nativeId, property, value));
108
+ return new Proxy(handler, handler);
109
+ }
110
+
111
+ createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>): WritableDeviceState {
112
+ const handler = new DeviceStateProxyHandler(this, id, setState);
113
+ return new Proxy(handler, handler);
114
+ }
115
+
116
+ getDeviceStorage(nativeId?: any): StorageImpl {
117
+ let ret = this.deviceStorage.get(nativeId);
118
+ if (!ret) {
119
+ ret = new StorageImpl(this, nativeId);
120
+ this.deviceStorage.set(nativeId, ret);
121
+ }
122
+ return ret;
123
+ }
124
+ getMixinStorage(id: string, nativeId?: ScryptedNativeId) {
125
+ let ms = this.mixinStorage.get(nativeId);
126
+ if (!ms) {
127
+ ms = new Map();
128
+ this.mixinStorage.set(nativeId, ms);
129
+ }
130
+ let ret = ms.get(id);
131
+ if (!ret) {
132
+ ret = new StorageImpl(this, nativeId, `mixin:${id}:`);
133
+ ms.set(id, ret);
134
+ }
135
+ return ret;
136
+ }
137
+ pruneMixinStorage() {
138
+ for (const nativeId of this.nativeIds.keys()) {
139
+ const storage = this.nativeIds.get(nativeId).storage;
140
+ for (const key of Object.keys(storage)) {
141
+ if (!key.startsWith('mixin:'))
142
+ continue;
143
+ const [, id,] = key.split(':');
144
+ // there's no rush to persist this, it will happen automatically on the plugin
145
+ // persisting something at some point.
146
+ // the key itself is unreachable due to the device no longer existing.
147
+ if (id && !this.systemManager.state[id])
148
+ delete storage[key];
149
+ }
150
+ }
151
+ }
152
+ async onMixinEvent(id: string, nativeId: ScryptedNativeId, eventInterface: string, eventData: any) {
153
+ return this.api.onMixinEvent(id, nativeId, eventInterface, eventData);
154
+ }
155
+ getNativeIds(): string[] {
156
+ return Array.from(this.nativeIds.keys());
157
+ }
158
+ async onDeviceDiscovered(device: Device) {
159
+ return this.api.onDeviceDiscovered(device);
160
+ }
161
+ async onDeviceRemoved(nativeId: string) {
162
+ return this.api.onDeviceRemoved(nativeId);
163
+ }
164
+ async onDeviceEvent(nativeId: any, eventInterface: any, eventData?: any) {
165
+ return this.api.onDeviceEvent(nativeId, eventInterface, eventData);
166
+ }
167
+ async onDevicesChanged(devices: DeviceManifest) {
168
+ return this.api.onDevicesChanged(devices);
169
+ }
170
+ }
171
+
172
+
173
+ function toStorageString(value: any) {
174
+ if (value === null)
175
+ return 'null';
176
+ if (value === undefined)
177
+ return 'undefined';
178
+
179
+ return value.toString();
180
+ }
181
+
182
+ export class StorageImpl implements Storage {
183
+ api: PluginAPI;
184
+ [name: string]: any;
185
+
186
+ private static allowedMethods = [
187
+ 'length',
188
+ 'clear',
189
+ 'getItem',
190
+ 'setItem',
191
+ 'key',
192
+ 'removeItem',
193
+ ];
194
+ private static indexedHandler: ProxyHandler<StorageImpl> = {
195
+ get(target, property) {
196
+ const keyString = property.toString();
197
+ if (StorageImpl.allowedMethods.includes(keyString)) {
198
+ const f = target[keyString];
199
+ if (keyString === 'length')
200
+ return f;
201
+ return f.bind(target);
202
+ }
203
+ return target.getItem(toStorageString(property));
204
+ },
205
+ set(target, property, value): boolean {
206
+ target.setItem(toStorageString(property), value);
207
+ return true;
208
+ }
209
+ };
210
+
211
+ constructor(public deviceManager: DeviceManagerImpl, public nativeId: ScryptedNativeId, public prefix?: string) {
212
+ this.deviceManager = deviceManager;
213
+ this.api = deviceManager.api;
214
+ this.nativeId = nativeId;
215
+ if (!this.prefix)
216
+ this.prefix = '';
217
+
218
+ return new Proxy(this, StorageImpl.indexedHandler);
219
+ }
220
+
221
+ get storage(): { [key: string]: any } {
222
+ return this.deviceManager.nativeIds.get(this.nativeId).storage;
223
+ }
224
+
225
+ get length(): number {
226
+ return Object.keys(this.storage).filter(key => key.startsWith(this.prefix)).length;
227
+ }
228
+
229
+ clear(): void {
230
+ if (!this.prefix) {
231
+ this.deviceManager.nativeIds.get(this.nativeId).storage = {};
232
+ }
233
+ else {
234
+ const storage = this.storage;
235
+ Object.keys(this.storage).filter(key => key.startsWith(this.prefix)).forEach(key => delete storage[key]);
236
+ }
237
+ this.api.setStorage(this.nativeId, this.storage);
238
+ }
239
+
240
+ getItem(key: string): string {
241
+ return this.storage[this.prefix + key];
242
+ }
243
+ key(index: number): string {
244
+ if (!this.prefix) {
245
+ return Object.keys(this.storage)[index];
246
+ }
247
+ return Object.keys(this.storage).filter(key => key.startsWith(this.prefix))[index].substring(this.prefix.length);
248
+ }
249
+ removeItem(key: string): void {
250
+ delete this.storage[this.prefix + key];
251
+ this.api.setStorage(this.nativeId, this.storage);
252
+ }
253
+ setItem(key: string, value: string): void {
254
+ key = toStorageString(key);
255
+ value = toStorageString(value);
256
+ if (this.storage[this.prefix + key] === value)
257
+ return;
258
+ this.storage[this.prefix + key] = value;
259
+ this.api.setStorage(this.nativeId, this.storage);
260
+ }
261
+ }
@@ -0,0 +1,109 @@
1
+ import { EndpointAccessControlAllowOrigin, MediaManager, ScryptedMimeTypes, type EndpointManager, type ScryptedNativeId } from "@scrypted/types";
2
+ import type { DeviceManagerImpl } from "./device";
3
+ import type { PluginAPI } from "./plugin-api";
4
+
5
+ export class EndpointManagerImpl implements EndpointManager {
6
+ deviceManager: DeviceManagerImpl;
7
+ api: PluginAPI;
8
+ pluginId: string;
9
+ mediaManager: MediaManager;
10
+
11
+ getEndpoint(nativeId?: ScryptedNativeId) {
12
+ if (!nativeId)
13
+ return this.pluginId;
14
+ const id = this.deviceManager.nativeIds.get(nativeId)?.id;
15
+ if (!id)
16
+ throw new Error('invalid nativeId ' + nativeId);
17
+ if (!nativeId)
18
+ return this.pluginId;
19
+ return id;
20
+ }
21
+
22
+ async getUrlSafeIp() {
23
+ // ipv6 addresses have colons and need to be bracketed for url safety
24
+ const ip: string = await this.api.getComponent('SCRYPTED_IP_ADDRESS')
25
+ return ip?.includes(':') ? `[${ip}]` : ip;
26
+ }
27
+
28
+ /**
29
+ * @deprecated
30
+ */
31
+ async getAuthenticatedPath(nativeId?: ScryptedNativeId): Promise<string> {
32
+ return this.getPath(nativeId);
33
+ }
34
+
35
+ /**
36
+ * @deprecated
37
+ */
38
+ async getInsecurePublicLocalEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
39
+ return this.getLocalEndpoint(nativeId, {
40
+ insecure: true,
41
+ public: true,
42
+ })
43
+ }
44
+
45
+ /**
46
+ * @deprecated
47
+ */
48
+ async getPublicCloudEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
49
+ return this.getCloudEndpoint(nativeId, {
50
+ public: true,
51
+ });
52
+ }
53
+
54
+ /**
55
+ * @deprecated
56
+ */
57
+ async getPublicLocalEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
58
+ return this.getLocalEndpoint(nativeId, {
59
+ public: true,
60
+ })
61
+ }
62
+
63
+ /**
64
+ * @deprecated
65
+ */
66
+ async getPublicPushEndpoint(nativeId?: ScryptedNativeId): Promise<string> {
67
+ const mo = await this.mediaManager.createMediaObject(Buffer.from(this.getEndpoint(nativeId)), ScryptedMimeTypes.PushEndpoint);
68
+ return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.PushEndpoint);
69
+ }
70
+
71
+ async getPath(nativeId?: string, options?: { public?: boolean; }): Promise<string> {
72
+ return `/endpoint/${this.getEndpoint(nativeId)}/${options?.public ? 'public/' : ''}`
73
+ }
74
+
75
+ async getLocalEndpoint(nativeId?: string, options?: { public?: boolean; insecure?: boolean; }): Promise<string> {
76
+ const protocol = options?.insecure ? 'http' : 'https';
77
+ const port = await this.api.getComponent(options?.insecure ? 'SCRYPTED_INSECURE_PORT' : 'SCRYPTED_SECURE_PORT');
78
+ const path = await this.getPath(nativeId, options);
79
+ const url = `${protocol}://${await this.getUrlSafeIp()}:${port}${path}`;
80
+ return url;
81
+ }
82
+
83
+ async getCloudEndpoint(nativeId?: string, options?: { public?: boolean; }): Promise<string> {
84
+ const local = await this.getLocalEndpoint(nativeId, options);
85
+ const mo = await this.mediaManager.createMediaObject(Buffer.from(local), ScryptedMimeTypes.LocalUrl);
86
+ return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.LocalUrl);
87
+ }
88
+
89
+ async getCloudPushEndpoint(nativeId?: string): Promise<string> {
90
+ const mo = await this.mediaManager.createMediaObject(Buffer.from(this.getEndpoint(nativeId)), ScryptedMimeTypes.PushEndpoint);
91
+ return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.PushEndpoint);
92
+ }
93
+
94
+ async setLocalAddresses(addresses: string[]): Promise<void> {
95
+ const addressSettings = await this.api.getComponent('addresses');
96
+ return addressSettings.setLocalAddresses(addresses);
97
+ }
98
+
99
+ async getLocalAddresses(): Promise<string[]> {
100
+ const addressSettings = await this.api.getComponent('addresses');
101
+ return await addressSettings.getLocalAddresses() as string[];
102
+ }
103
+
104
+ async setAccessControlAllowOrigin(options: EndpointAccessControlAllowOrigin): Promise<void> {
105
+ const self = this;
106
+ const setAccessControlAllowOrigin = await this.deviceManager.systemManager.getComponent('setAccessControlAllowOrigin') as typeof self.setAccessControlAllowOrigin;
107
+ return setAccessControlAllowOrigin(options);
108
+ }
109
+ }