@scrypted/server 0.123.31 → 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.
- package/dist/plugin/device.d.ts +60 -0
- package/dist/plugin/device.js +249 -0
- package/dist/plugin/device.js.map +1 -0
- package/dist/plugin/endpoint.d.ts +45 -0
- package/dist/plugin/endpoint.js +97 -0
- package/dist/plugin/endpoint.js.map +1 -0
- package/dist/plugin/plugin-remote-worker.js +8 -7
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.d.ts +2 -51
- package/dist/plugin/plugin-remote.js +6 -337
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/runtime/cluster-fork-worker.d.ts +3 -3
- package/dist/plugin/runtime/cluster-fork-worker.js +1 -0
- package/dist/plugin/runtime/cluster-fork-worker.js.map +1 -1
- package/dist/plugin/system.js +0 -1
- package/dist/plugin/system.js.map +1 -1
- package/dist/runtime.d.ts +4 -4
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-cluster-main.d.ts +12 -2
- package/dist/scrypted-cluster-main.js +30 -4
- package/dist/scrypted-cluster-main.js.map +1 -1
- package/dist/services/cluster-fork.d.ts +2 -2
- package/dist/services/cluster-fork.js +4 -3
- package/dist/services/cluster-fork.js.map +1 -1
- package/package.json +2 -2
- package/python/cluster_labels.py +2 -2
- package/python/cluster_setup.py +1 -1
- package/python/plugin_console.py +8 -0
- package/python/plugin_remote.py +102 -33
- package/python/rpc.py +5 -4
- package/src/plugin/device.ts +261 -0
- package/src/plugin/endpoint.ts +109 -0
- package/src/plugin/plugin-remote-worker.ts +14 -10
- package/src/plugin/plugin-remote.ts +6 -364
- package/src/plugin/runtime/cluster-fork-worker.ts +6 -4
- package/src/plugin/system.ts +0 -1
- package/src/runtime.ts +4 -4
- package/src/scrypted-cluster-main.ts +39 -9
- package/src/services/cluster-fork.ts +5 -5
package/python/plugin_remote.py
CHANGED
@@ -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,
|
626
|
-
await self.clusterSetup.initializeCluster(
|
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 =
|
633
|
-
debug =
|
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 =
|
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,24 +790,44 @@ 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
|
826
|
+
async def getClusterFork():
|
779
827
|
forkComponent = await self.api.getComponent("cluster-fork")
|
780
828
|
sanitizedOptions = options.copy()
|
781
829
|
sanitizedOptions["runtime"] = sanitizedOptions.get("runtime", "python")
|
830
|
+
sanitizedOptions["zipHash"] = zipHash
|
782
831
|
clusterForkResult = await forkComponent.fork(peerLiveness, sanitizedOptions, packageJson, zipHash, lambda: zipAPI.getZip())
|
783
832
|
|
784
833
|
async def waitPeerLiveness():
|
@@ -791,20 +840,44 @@ class PluginRemote:
|
|
791
840
|
pass
|
792
841
|
asyncio.ensure_future(waitPeerLiveness(), loop=self.loop)
|
793
842
|
|
794
|
-
async def
|
843
|
+
async def waitClusterForkKilled():
|
795
844
|
try:
|
796
845
|
await clusterForkResult.waitKilled()
|
797
846
|
except:
|
798
847
|
pass
|
799
|
-
peerLiveness.killed
|
800
|
-
asyncio.ensure_future(
|
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
|
+
|
801
864
|
|
802
|
-
result = asyncio.ensure_future(startClusterFork(), loop=self.loop)
|
803
865
|
pluginFork = PluginFork()
|
804
|
-
pluginFork.result =
|
805
|
-
|
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
|
+
|
806
877
|
return pluginFork
|
807
878
|
|
879
|
+
t: asyncio.Task
|
880
|
+
t.cancel()
|
808
881
|
if options:
|
809
882
|
runtime = options.get("runtime", None)
|
810
883
|
if runtime and runtime != "python":
|
@@ -813,8 +886,17 @@ class PluginRemote:
|
|
813
886
|
raise Exception("python fork to filename not supported")
|
814
887
|
|
815
888
|
parent_conn, child_conn = multiprocessing.Pipe()
|
889
|
+
|
816
890
|
pluginFork = PluginFork()
|
817
|
-
|
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
|
+
|
818
900
|
pluginFork.worker = multiprocessing.Process(
|
819
901
|
target=plugin_fork, args=(child_conn,), daemon=True
|
820
902
|
)
|
@@ -823,6 +905,7 @@ class PluginRemote:
|
|
823
905
|
def schedule_exit_check():
|
824
906
|
def exit_check():
|
825
907
|
if pluginFork.worker.exitcode != None:
|
908
|
+
safe_set_result(killed, None)
|
826
909
|
pluginFork.worker.join()
|
827
910
|
else:
|
828
911
|
schedule_exit_check()
|
@@ -847,26 +930,11 @@ class PluginRemote:
|
|
847
930
|
finally:
|
848
931
|
parent_conn.close()
|
849
932
|
rpcTransport.executor.shutdown()
|
850
|
-
pluginFork.
|
933
|
+
pluginFork.terminate()
|
851
934
|
|
852
935
|
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
|
-
|
864
|
-
class PluginZipAPI:
|
865
|
-
|
866
|
-
async def getZip(self):
|
867
|
-
return await zipAPI.getZip()
|
868
936
|
|
869
|
-
return await
|
937
|
+
return await finishFork(forkPeer)
|
870
938
|
|
871
939
|
pluginFork.result = asyncio.create_task(getFork())
|
872
940
|
return pluginFork
|
@@ -874,6 +942,7 @@ class PluginRemote:
|
|
874
942
|
sdk.fork = host_fork
|
875
943
|
# sdk.
|
876
944
|
|
945
|
+
from scrypted_sdk import sdk_init2 # type: ignore
|
877
946
|
sdk_init2(sdk)
|
878
947
|
except:
|
879
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__[
|
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__[
|
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__[
|
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,
|
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
|
+
}
|