@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.
- 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-host.d.ts +1 -1
- package/dist/plugin/plugin-host.js +11 -10
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +18 -16
- 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/child-process-worker.d.ts +1 -1
- package/dist/plugin/runtime/child-process-worker.js +2 -2
- package/dist/plugin/runtime/child-process-worker.js.map +1 -1
- package/dist/plugin/runtime/cluster-fork-worker.d.ts +4 -4
- package/dist/plugin/runtime/cluster-fork-worker.js +8 -3
- package/dist/plugin/runtime/cluster-fork-worker.js.map +1 -1
- package/dist/plugin/runtime/custom-worker.d.ts +1 -1
- package/dist/plugin/runtime/custom-worker.js +3 -3
- package/dist/plugin/runtime/custom-worker.js.map +1 -1
- package/dist/plugin/runtime/node-fork-worker.d.ts +1 -1
- package/dist/plugin/runtime/node-fork-worker.js +2 -2
- package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
- package/dist/plugin/runtime/python-worker.d.ts +1 -1
- package/dist/plugin/runtime/python-worker.js +3 -3
- package/dist/plugin/runtime/python-worker.js.map +1 -1
- package/dist/plugin/runtime/runtime-host.d.ts +1 -1
- package/dist/plugin/runtime/runtime-host.js +3 -3
- package/dist/plugin/runtime/runtime-host.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 +14 -3
- package/dist/scrypted-cluster-main.js +42 -18
- package/dist/scrypted-cluster-main.js.map +1 -1
- package/dist/scrypted-server-main.js +1 -0
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/services/cluster-fork.d.ts +4 -3
- package/dist/services/cluster-fork.js +7 -5
- 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 +116 -34
- package/python/rpc.py +5 -4
- package/src/plugin/device.ts +261 -0
- package/src/plugin/endpoint.ts +109 -0
- package/src/plugin/plugin-host.ts +25 -21
- package/src/plugin/plugin-remote-worker.ts +30 -20
- package/src/plugin/plugin-remote.ts +6 -364
- package/src/plugin/runtime/child-process-worker.ts +3 -1
- package/src/plugin/runtime/cluster-fork-worker.ts +20 -12
- package/src/plugin/runtime/custom-worker.ts +3 -3
- package/src/plugin/runtime/node-fork-worker.ts +2 -2
- package/src/plugin/runtime/python-worker.ts +3 -3
- package/src/plugin/runtime/runtime-host.ts +4 -4
- package/src/plugin/system.ts +0 -1
- package/src/runtime.ts +4 -4
- package/src/scrypted-cluster-main.ts +54 -29
- package/src/scrypted-server-main.ts +1 -0
- package/src/services/cluster-fork.ts +10 -8
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: 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
|
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
|
-
|
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
|
856
|
+
async def waitClusterForkKilled():
|
795
857
|
try:
|
796
858
|
await clusterForkResult.waitKilled()
|
797
859
|
except:
|
798
860
|
pass
|
799
|
-
peerLiveness.killed
|
800
|
-
asyncio.ensure_future(
|
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 =
|
805
|
-
|
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
|
-
|
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.
|
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
|
-
|
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__[
|
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
|
+
}
|