@scrypted/server 0.123.30 → 0.123.32

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 (43) hide show
  1. package/dist/cluster/cluster-labels.js +1 -0
  2. package/dist/cluster/cluster-labels.js.map +1 -1
  3. package/dist/plugin/device.d.ts +60 -0
  4. package/dist/plugin/device.js +249 -0
  5. package/dist/plugin/device.js.map +1 -0
  6. package/dist/plugin/endpoint.d.ts +45 -0
  7. package/dist/plugin/endpoint.js +97 -0
  8. package/dist/plugin/endpoint.js.map +1 -0
  9. package/dist/plugin/plugin-remote-worker.js +8 -7
  10. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  11. package/dist/plugin/plugin-remote.d.ts +2 -51
  12. package/dist/plugin/plugin-remote.js +6 -337
  13. package/dist/plugin/plugin-remote.js.map +1 -1
  14. package/dist/plugin/runtime/cluster-fork-worker.d.ts +3 -3
  15. package/dist/plugin/runtime/cluster-fork-worker.js +5 -1
  16. package/dist/plugin/runtime/cluster-fork-worker.js.map +1 -1
  17. package/dist/plugin/system.js +0 -1
  18. package/dist/plugin/system.js.map +1 -1
  19. package/dist/runtime.d.ts +4 -4
  20. package/dist/runtime.js +1 -1
  21. package/dist/runtime.js.map +1 -1
  22. package/dist/scrypted-cluster-main.d.ts +12 -2
  23. package/dist/scrypted-cluster-main.js +30 -4
  24. package/dist/scrypted-cluster-main.js.map +1 -1
  25. package/dist/services/cluster-fork.d.ts +2 -2
  26. package/dist/services/cluster-fork.js +4 -3
  27. package/dist/services/cluster-fork.js.map +1 -1
  28. package/package.json +2 -2
  29. package/python/cluster_labels.py +2 -2
  30. package/python/cluster_setup.py +1 -1
  31. package/python/plugin_console.py +8 -0
  32. package/python/plugin_remote.py +105 -34
  33. package/python/rpc.py +5 -4
  34. package/src/cluster/cluster-labels.ts +1 -0
  35. package/src/plugin/device.ts +261 -0
  36. package/src/plugin/endpoint.ts +109 -0
  37. package/src/plugin/plugin-remote-worker.ts +14 -10
  38. package/src/plugin/plugin-remote.ts +6 -364
  39. package/src/plugin/runtime/cluster-fork-worker.ts +10 -5
  40. package/src/plugin/system.ts +0 -1
  41. package/src/runtime.ts +4 -4
  42. package/src/scrypted-cluster-main.ts +39 -9
  43. package/src/services/cluster-fork.ts +5 -5
@@ -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 = 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,23 +790,45 @@ 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():
779
827
  forkComponent = await self.api.getComponent("cluster-fork")
780
- clusterForkResult = await forkComponent.fork(peerLiveness, options, packageJson, zipHash, lambda: zipAPI.getZip())
828
+ sanitizedOptions = options.copy()
829
+ sanitizedOptions["runtime"] = sanitizedOptions.get("runtime", "python")
830
+ sanitizedOptions["zipHash"] = zipHash
831
+ clusterForkResult = await forkComponent.fork(peerLiveness, sanitizedOptions, packageJson, zipHash, lambda: zipAPI.getZip())
781
832
 
782
833
  async def waitPeerLiveness():
783
834
  try:
@@ -789,20 +840,44 @@ class PluginRemote:
789
840
  pass
790
841
  asyncio.ensure_future(waitPeerLiveness(), loop=self.loop)
791
842
 
792
- async def waitClusterForkResult():
843
+ async def waitClusterForkKilled():
793
844
  try:
794
845
  await clusterForkResult.waitKilled()
795
846
  except:
796
847
  pass
797
- peerLiveness.killed.set_result(None)
798
- asyncio.ensure_future(waitClusterForkResult(), loop=self.loop)
848
+ safe_set_result(peerLiveness.killed, None)
849
+ asyncio.ensure_future(waitClusterForkKilled(), loop=self.loop)
850
+
851
+ clusterGetRemote = await self.clusterSetup.connectRPCObject(await clusterForkResult.getResult())
852
+ remoteDict = await clusterGetRemote()
853
+ asyncio.ensure_future(plugin_console.writeWorkerGenerator(remoteDict["stdout"], sys.stdout))
854
+ asyncio.ensure_future(plugin_console.writeWorkerGenerator(remoteDict["stderr"], sys.stderr))
855
+
856
+ getRemote = remoteDict["getRemote"]
857
+ directGetRemote = await self.clusterSetup.connectRPCObject(getRemote)
858
+ if directGetRemote is getRemote:
859
+ raise Exception("cluster fork peer not direct connected")
860
+
861
+ forkPeer = getattr(directGetRemote, rpc.RpcPeer.PROPERTY_PROXY_PEER)
862
+ return await finishFork(forkPeer)
863
+
799
864
 
800
- result = asyncio.ensure_future(startClusterFork(), loop=self.loop)
801
865
  pluginFork = PluginFork()
802
- pluginFork.result = result
803
- pluginFork.terminate = lambda: peerLiveness.killed.set_result(None)
866
+ pluginFork.result = asyncio.create_task(getClusterFork())
867
+ async def waitKilled():
868
+ await peerLiveness.killed
869
+ pluginFork.exit = asyncio.create_task(waitKilled())
870
+ def terminate():
871
+ safe_set_result(peerLiveness.killed, None)
872
+ pluginFork.worker.terminate()
873
+ pluginFork.terminate = terminate
874
+
875
+ pluginFork.worker = None
876
+
804
877
  return pluginFork
805
878
 
879
+ t: asyncio.Task
880
+ t.cancel()
806
881
  if options:
807
882
  runtime = options.get("runtime", None)
808
883
  if runtime and runtime != "python":
@@ -811,8 +886,17 @@ class PluginRemote:
811
886
  raise Exception("python fork to filename not supported")
812
887
 
813
888
  parent_conn, child_conn = multiprocessing.Pipe()
889
+
814
890
  pluginFork = PluginFork()
815
- print("new fork")
891
+ killed = Future(loop=self.loop)
892
+ async def waitKilled():
893
+ await killed
894
+ pluginFork.exit = asyncio.create_task(waitKilled())
895
+ def terminate():
896
+ safe_set_result(killed, None)
897
+ pluginFork.worker.kill()
898
+ pluginFork.terminate = terminate
899
+
816
900
  pluginFork.worker = multiprocessing.Process(
817
901
  target=plugin_fork, args=(child_conn,), daemon=True
818
902
  )
@@ -821,6 +905,7 @@ class PluginRemote:
821
905
  def schedule_exit_check():
822
906
  def exit_check():
823
907
  if pluginFork.worker.exitcode != None:
908
+ safe_set_result(killed, None)
824
909
  pluginFork.worker.join()
825
910
  else:
826
911
  schedule_exit_check()
@@ -845,26 +930,11 @@ class PluginRemote:
845
930
  finally:
846
931
  parent_conn.close()
847
932
  rpcTransport.executor.shutdown()
848
- pluginFork.worker.kill()
933
+ pluginFork.terminate()
849
934
 
850
935
  asyncio.run_coroutine_threadsafe(forkReadLoop(), loop=self.loop)
851
- getRemote = await forkPeer.getParam("getRemote")
852
- remote: PluginRemote = await getRemote(
853
- self.api, self.pluginId, self.hostInfo
854
- )
855
- await remote.setSystemState(self.systemManager.getSystemState())
856
- for nativeId, ds in self.nativeIds.items():
857
- await remote.setNativeId(nativeId, ds.id, ds.storage)
858
- forkOptions = options.copy()
859
- forkOptions["fork"] = True
860
- forkOptions["debug"] = debug
861
-
862
- class PluginZipAPI:
863
-
864
- async def getZip(self):
865
- return await zipAPI.getZip()
866
936
 
867
- return await remote.loadZip(packageJson, PluginZipAPI(), forkOptions)
937
+ return await finishFork(forkPeer)
868
938
 
869
939
  pluginFork.result = asyncio.create_task(getFork())
870
940
  return pluginFork
@@ -872,6 +942,7 @@ class PluginRemote:
872
942
  sdk.fork = host_fork
873
943
  # sdk.
874
944
 
945
+ from scrypted_sdk import sdk_init2 # type: ignore
875
946
  sdk_init2(sdk)
876
947
  except:
877
948
  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,
@@ -37,5 +37,6 @@ export function getClusterLabels() {
37
37
 
38
38
  export function needsClusterForkWorker(options: ClusterForkOptions) {
39
39
  return process.env.SCRYPTED_CLUSTER_ADDRESS
40
+ && options
40
41
  && (!matchesClusterLabels(options, getClusterLabels()) || options.clusterWorkerId);
41
42
  }
@@ -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
+ }