@scrypted/server 0.0.105 → 0.0.109
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.
Potentially problematic release.
This version of @scrypted/server might be problematic. Click here for more details.
- package/dist/http-interfaces.js +2 -2
- package/dist/http-interfaces.js.map +1 -1
- package/dist/plugin/listen-zero.js +13 -1
- package/dist/plugin/listen-zero.js.map +1 -1
- package/dist/plugin/plugin-device.js +11 -2
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +8 -7
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +36 -87
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +100 -0
- package/dist/plugin/plugin-http.js.map +1 -0
- package/dist/plugin/plugin-lazy-remote.js +73 -0
- package/dist/plugin/plugin-lazy-remote.js.map +1 -0
- package/dist/plugin/plugin-npm-dependencies.js +16 -16
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
- package/dist/plugin/plugin-remote-websocket.js +40 -34
- package/dist/plugin/plugin-remote-websocket.js.map +1 -1
- package/dist/plugin/plugin-remote.js +35 -26
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/rpc.js +53 -26
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +45 -111
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +3 -0
- package/dist/scrypted-main.js.map +1 -1
- package/dist/services/plugin.js +2 -2
- package/dist/services/plugin.js.map +1 -1
- package/dist/state.js +2 -8
- package/dist/state.js.map +1 -1
- package/package.json +2 -2
- package/python/plugin-remote.py +4 -4
- package/python/rpc.py +68 -26
- package/src/http-interfaces.ts +3 -3
- package/src/plugin/listen-zero.ts +13 -0
- package/src/plugin/plugin-api.ts +1 -1
- package/src/plugin/plugin-device.ts +11 -2
- package/src/plugin/plugin-host-api.ts +9 -8
- package/src/plugin/plugin-host.ts +40 -95
- package/src/plugin/plugin-http.ts +117 -0
- package/src/plugin/plugin-lazy-remote.ts +70 -0
- package/src/plugin/plugin-npm-dependencies.ts +19 -18
- package/src/plugin/plugin-remote-websocket.ts +55 -60
- package/src/plugin/plugin-remote.ts +45 -38
- package/src/rpc.ts +62 -25
- package/src/runtime.ts +55 -128
- package/src/scrypted-main.ts +4 -0
- package/src/services/plugin.ts +2 -2
- package/src/state.ts +2 -10
package/python/plugin-remote.py
CHANGED
|
@@ -21,7 +21,7 @@ from asyncio.futures import Future
|
|
|
21
21
|
from asyncio.streams import StreamReader, StreamWriter
|
|
22
22
|
import os
|
|
23
23
|
from os import sys
|
|
24
|
-
|
|
24
|
+
import platform
|
|
25
25
|
|
|
26
26
|
class SystemDeviceState(TypedDict):
|
|
27
27
|
lastEventTime: int
|
|
@@ -164,8 +164,8 @@ class PluginRemote:
|
|
|
164
164
|
|
|
165
165
|
zip = zipfile.ZipFile(zipPath)
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
plugin_volume = os.environ.get('SCRYPTED_PLUGIN_VOLUME')
|
|
168
|
+
python_prefix = os.path.join(plugin_volume, 'python-%s-%s' % (platform.system(), platform.machine()))
|
|
169
169
|
if not os.path.exists(python_prefix):
|
|
170
170
|
os.makedirs(python_prefix)
|
|
171
171
|
|
|
@@ -179,7 +179,7 @@ class PluginRemote:
|
|
|
179
179
|
|
|
180
180
|
requirementstxt = os.path.join(python_prefix, 'requirements.txt')
|
|
181
181
|
installed_requirementstxt = os.path.join(
|
|
182
|
-
python_prefix, '
|
|
182
|
+
python_prefix, 'requirements.installed.txt')
|
|
183
183
|
|
|
184
184
|
need_pip = True
|
|
185
185
|
try:
|
package/python/rpc.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from asyncio.futures import Future
|
|
2
|
-
from typing import Callable, Mapping, List
|
|
2
|
+
from typing import Any, Callable, Mapping, List
|
|
3
3
|
import traceback
|
|
4
4
|
import inspect
|
|
5
|
+
from typing_extensions import TypedDict
|
|
5
6
|
import weakref
|
|
7
|
+
import sys
|
|
6
8
|
|
|
7
9
|
jsonSerializable = set()
|
|
8
10
|
jsonSerializable.add(float)
|
|
@@ -45,21 +47,35 @@ class RpcProxyMethod:
|
|
|
45
47
|
return self.__proxy.__apply__(self.__proxy_method_name, args)
|
|
46
48
|
|
|
47
49
|
|
|
50
|
+
class LocalProxiedEntry(TypedDict):
|
|
51
|
+
id: str
|
|
52
|
+
finalizerId: str
|
|
53
|
+
|
|
54
|
+
|
|
48
55
|
class RpcProxy(object):
|
|
49
|
-
def __init__(self, peer,
|
|
50
|
-
self.__dict__['__proxy_id'] =
|
|
56
|
+
def __init__(self, peer, entry: LocalProxiedEntry, proxyConstructorName: str, proxyProps: any, proxyOneWayMethods: List[str]):
|
|
57
|
+
self.__dict__['__proxy_id'] = entry['id']
|
|
58
|
+
self.__dict__['__proxy_entry'] = entry
|
|
51
59
|
self.__dict__['__proxy_constructor'] = proxyConstructorName
|
|
52
60
|
self.__dict__['__proxy_peer'] = peer
|
|
53
61
|
self.__dict__['__proxy_props'] = proxyProps
|
|
54
62
|
self.__dict__['__proxy_oneway_methods'] = proxyOneWayMethods
|
|
55
63
|
|
|
56
64
|
def __getattr__(self, name):
|
|
65
|
+
if name == '__proxy_finalizer_id':
|
|
66
|
+
return self.dict['__proxy_entry']['finalizerId']
|
|
57
67
|
if name in self.__dict__:
|
|
58
68
|
return self.__dict__[name]
|
|
59
69
|
if self.__dict__['__proxy_props'] and name in self.__dict__['__proxy_props']:
|
|
60
70
|
return self.__dict__['__proxy_props'][name]
|
|
61
71
|
return RpcProxyMethod(self, name)
|
|
62
72
|
|
|
73
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
74
|
+
if name == '__proxy_finalizer_id':
|
|
75
|
+
self.dict['__proxy_entry']['finalizerId'] = value
|
|
76
|
+
|
|
77
|
+
return super().__setattr__(name, value)
|
|
78
|
+
|
|
63
79
|
def __call__(self, *args, **kwargs):
|
|
64
80
|
print('call')
|
|
65
81
|
pass
|
|
@@ -69,20 +85,18 @@ class RpcProxy(object):
|
|
|
69
85
|
|
|
70
86
|
|
|
71
87
|
class RpcPeer:
|
|
72
|
-
# todo: these are all class statics lol, fix this.
|
|
73
|
-
idCounter = 1
|
|
74
|
-
peerName = 'Unnamed Peer'
|
|
75
|
-
params: Mapping[str, any] = {}
|
|
76
|
-
localProxied: Mapping[any, str] = {}
|
|
77
|
-
localProxyMap: Mapping[str, any] = {}
|
|
78
|
-
constructorSerializerMap = {}
|
|
79
|
-
proxyCounter = 1
|
|
80
|
-
pendingResults: Mapping[str, Future] = {}
|
|
81
|
-
remoteWeakProxies: Mapping[str, any] = {}
|
|
82
|
-
nameDeserializerMap: Mapping[str, RpcSerializer] = {}
|
|
83
|
-
|
|
84
88
|
def __init__(self, send: Callable[[object, Callable[[Exception], None]], None]) -> None:
|
|
85
89
|
self.send = send
|
|
90
|
+
self.idCounter = 1
|
|
91
|
+
self.peerName = 'Unnamed Peer'
|
|
92
|
+
self.params: Mapping[str, any] = {}
|
|
93
|
+
self.localProxied: Mapping[any, LocalProxiedEntry] = {}
|
|
94
|
+
self.localProxyMap: Mapping[str, any] = {}
|
|
95
|
+
self.constructorSerializerMap = {}
|
|
96
|
+
self.proxyCounter = 1
|
|
97
|
+
self.pendingResults: Mapping[str, Future] = {}
|
|
98
|
+
self.remoteWeakProxies: Mapping[str, any] = {}
|
|
99
|
+
self.nameDeserializerMap: Mapping[str, RpcSerializer] = {}
|
|
86
100
|
|
|
87
101
|
def __apply__(self, proxyId: str, oneWayMethods: List[str], method: str, args: list):
|
|
88
102
|
serializedArgs = []
|
|
@@ -120,12 +134,17 @@ class RpcPeer:
|
|
|
120
134
|
def serialize(self, value, requireProxy):
|
|
121
135
|
if (not value or (not requireProxy and type(value) in jsonSerializable)):
|
|
122
136
|
return value
|
|
137
|
+
|
|
123
138
|
__remote_constructor_name = 'Function' if callable(value) else value.__proxy_constructor if hasattr(
|
|
124
139
|
value, '__proxy_constructor') else type(value).__name__
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
|
|
141
|
+
proxiedEntry = self.localProxied.get(value, None)
|
|
142
|
+
if proxiedEntry:
|
|
143
|
+
proxiedEntry['finalizerId'] = str(self.proxyCounter)
|
|
144
|
+
self.proxyCounter = self.proxyCounter + 1
|
|
127
145
|
ret = {
|
|
128
|
-
'__remote_proxy_id':
|
|
146
|
+
'__remote_proxy_id': proxiedEntry['id'],
|
|
147
|
+
'__remote_proxy_finalizer_id': proxiedEntry['finalizerId'],
|
|
129
148
|
'__remote_constructor_name': __remote_constructor_name,
|
|
130
149
|
'__remote_proxy_props': getattr(value, '__proxy_props', None),
|
|
131
150
|
'__remote_proxy_oneway_methods': getattr(value, '__proxy_oneway_methods', None),
|
|
@@ -140,13 +159,15 @@ class RpcPeer:
|
|
|
140
159
|
}
|
|
141
160
|
return ret
|
|
142
161
|
|
|
143
|
-
serializerMapName = self.constructorSerializerMap.get(
|
|
162
|
+
serializerMapName = self.constructorSerializerMap.get(
|
|
163
|
+
type(value), None)
|
|
144
164
|
if serializerMapName:
|
|
145
165
|
__remote_constructor_name = serializerMapName
|
|
146
166
|
serializer = self.nameDeserializerMap.get(serializerMapName, None)
|
|
147
167
|
serialized = serializer.serialize(value)
|
|
148
168
|
ret = {
|
|
149
169
|
'__remote_proxy_id': None,
|
|
170
|
+
'__remote_proxy_finalizer_id': None,
|
|
150
171
|
'__remote_constructor_name': __remote_constructor_name,
|
|
151
172
|
'__remote_proxy_props': getattr(value, '__proxy_props', None),
|
|
152
173
|
'__remote_proxy_oneway_methods': getattr(value, '__proxy_oneway_methods', None),
|
|
@@ -156,11 +177,16 @@ class RpcPeer:
|
|
|
156
177
|
|
|
157
178
|
proxyId = str(self.proxyCounter)
|
|
158
179
|
self.proxyCounter = self.proxyCounter + 1
|
|
159
|
-
|
|
180
|
+
proxiedEntry = {
|
|
181
|
+
'id': proxyId,
|
|
182
|
+
'finalizerId': proxyId,
|
|
183
|
+
}
|
|
184
|
+
self.localProxied[value] = proxiedEntry
|
|
160
185
|
self.localProxyMap[proxyId] = value
|
|
161
186
|
|
|
162
187
|
ret = {
|
|
163
188
|
'__remote_proxy_id': proxyId,
|
|
189
|
+
'__remote_proxy_finalizer_id': proxyId,
|
|
164
190
|
'__remote_constructor_name': __remote_constructor_name,
|
|
165
191
|
'__remote_proxy_props': getattr(value, '__proxy_props', None),
|
|
166
192
|
'__remote_proxy_oneway_methods': getattr(value, '__proxy_oneway_methods', None),
|
|
@@ -168,20 +194,26 @@ class RpcPeer:
|
|
|
168
194
|
|
|
169
195
|
return ret
|
|
170
196
|
|
|
171
|
-
def finalize(self,
|
|
197
|
+
def finalize(self, localProxiedEntry: LocalProxiedEntry):
|
|
198
|
+
id = localProxiedEntry['id']
|
|
172
199
|
self.remoteWeakProxies.pop(id, None)
|
|
173
200
|
rpcFinalize = {
|
|
174
201
|
'__local_proxy_id': id,
|
|
202
|
+
'__local_proxy_finalizer_id': localProxiedEntry['finalizerId'],
|
|
175
203
|
'type': 'finalize',
|
|
176
204
|
}
|
|
177
205
|
self.send(rpcFinalize)
|
|
178
206
|
|
|
179
207
|
def newProxy(self, proxyId: str, proxyConstructorName: str, proxyProps: any, proxyOneWayMethods: List[str]):
|
|
180
|
-
|
|
208
|
+
localProxiedEntry: LocalProxiedEntry = {
|
|
209
|
+
'id': proxyId,
|
|
210
|
+
'finalizerId': None,
|
|
211
|
+
}
|
|
212
|
+
proxy = RpcProxy(self, localProxiedEntry, proxyConstructorName,
|
|
181
213
|
proxyProps, proxyOneWayMethods)
|
|
182
214
|
wr = weakref.ref(proxy)
|
|
183
215
|
self.remoteWeakProxies[proxyId] = wr
|
|
184
|
-
weakref.finalize(proxy, lambda: self.finalize(
|
|
216
|
+
weakref.finalize(proxy, lambda: self.finalize(localProxiedEntry))
|
|
185
217
|
return proxy
|
|
186
218
|
|
|
187
219
|
def deserialize(self, value):
|
|
@@ -192,6 +224,8 @@ class RpcPeer:
|
|
|
192
224
|
return value
|
|
193
225
|
|
|
194
226
|
__remote_proxy_id = value.get('__remote_proxy_id', None)
|
|
227
|
+
__remote_proxy_finalizer_id = value.get(
|
|
228
|
+
'__remote_proxy_finalizer_id', None)
|
|
195
229
|
__local_proxy_id = value.get('__local_proxy_id', None)
|
|
196
230
|
__remote_constructor_name = value.get(
|
|
197
231
|
'__remote_constructor_name', None)
|
|
@@ -206,6 +240,7 @@ class RpcPeer:
|
|
|
206
240
|
if not proxy:
|
|
207
241
|
proxy = self.newProxy(__remote_proxy_id, __remote_constructor_name,
|
|
208
242
|
__remote_proxy_props, __remote_proxy_oneway_methods)
|
|
243
|
+
proxy.__proxy_finalizer_id = __remote_proxy_finalizer_id
|
|
209
244
|
return proxy
|
|
210
245
|
|
|
211
246
|
if __local_proxy_id:
|
|
@@ -297,9 +332,16 @@ class RpcPeer:
|
|
|
297
332
|
future.set_result(self.deserialize(
|
|
298
333
|
message.get('result', None)))
|
|
299
334
|
elif messageType == 'finalize':
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
self.
|
|
335
|
+
finalizerId = message.get('__local_proxy_finalizer_id', None)
|
|
336
|
+
proxyId = message['__local_proxy_id']
|
|
337
|
+
local = self.localProxyMap.get(proxyId, None)
|
|
338
|
+
if local:
|
|
339
|
+
localProxiedEntry = self.localProxied.get(local)
|
|
340
|
+
if localProxiedEntry and finalizerId and localProxiedEntry['finalizerId'] != finalizerId:
|
|
341
|
+
print('mismatch finalizer id', file=sys.stderr)
|
|
342
|
+
return
|
|
343
|
+
self.localProxied.pop(local, None)
|
|
344
|
+
local = self.localProxyMap.pop(proxyId, None)
|
|
303
345
|
else:
|
|
304
346
|
raise RpcResultException(
|
|
305
347
|
None, 'unknown rpc message type %s' % type)
|
package/src/http-interfaces.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { HttpResponse, HttpResponseOptions } from "@scrypted/sdk/types";
|
|
2
2
|
import { Response } from "express";
|
|
3
|
-
import { PluginHost } from './plugin/plugin-host';
|
|
4
3
|
import mime from "mime";
|
|
4
|
+
import AdmZip from "adm-zip";
|
|
5
5
|
|
|
6
|
-
export function createResponseInterface(res: Response,
|
|
6
|
+
export function createResponseInterface(res: Response, zip: AdmZip): HttpResponse {
|
|
7
7
|
class HttpResponseImpl implements HttpResponse {
|
|
8
8
|
send(body: string): void;
|
|
9
9
|
send(body: string, options: HttpResponseOptions): void;
|
|
@@ -35,7 +35,7 @@ export function createResponseInterface(res: Response, plugin: PluginHost): Http
|
|
|
35
35
|
if (!res.getHeader('Content-Type'))
|
|
36
36
|
res.contentType(mime.lookup(path));
|
|
37
37
|
|
|
38
|
-
const data =
|
|
38
|
+
const data = zip.getEntry(`fs/${path}`)?.getData();
|
|
39
39
|
if (!data) {
|
|
40
40
|
res.status(404);
|
|
41
41
|
res.end();
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import { once } from 'events';
|
|
3
|
+
import express from 'express';
|
|
3
4
|
|
|
4
5
|
export async function listenZero(server: net.Server) {
|
|
5
6
|
server.listen(0);
|
|
6
7
|
await once(server, 'listening');
|
|
7
8
|
return (server.address() as net.AddressInfo).port;
|
|
8
9
|
}
|
|
10
|
+
|
|
11
|
+
export function listenZeroExpress(app: express.Express) {
|
|
12
|
+
const server = app.listen(0);
|
|
13
|
+
return {
|
|
14
|
+
server,
|
|
15
|
+
port: (async () => {
|
|
16
|
+
await once(server, 'listening');
|
|
17
|
+
const { port } = (server.address() as net.AddressInfo);
|
|
18
|
+
return port;
|
|
19
|
+
})()
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/plugin/plugin-api.ts
CHANGED
|
@@ -146,7 +146,7 @@ export interface PluginRemote {
|
|
|
146
146
|
|
|
147
147
|
createDeviceState(id: string, setState: (property: string, value: any) => Promise<any>): Promise<any>;
|
|
148
148
|
|
|
149
|
-
getServicePort(name: string): Promise<number>;
|
|
149
|
+
getServicePort(name: string, ...args: any[]): Promise<number>;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
export interface MediaObjectRemote extends MediaObject {
|
|
@@ -7,6 +7,7 @@ import { getState } from "../state";
|
|
|
7
7
|
import { getDisplayType } from "../infer-defaults";
|
|
8
8
|
import { allInterfaceProperties, isValidInterfaceMethod, methodInterfaces } from "./descriptor";
|
|
9
9
|
import { PluginError } from "./plugin-error";
|
|
10
|
+
import { sleep } from "../sleep";
|
|
10
11
|
|
|
11
12
|
interface MixinTable {
|
|
12
13
|
mixinProviderId: string;
|
|
@@ -28,6 +29,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
|
|
|
28
29
|
scrypted: ScryptedRuntime;
|
|
29
30
|
id: string;
|
|
30
31
|
mixinTable: MixinTable[];
|
|
32
|
+
releasing = new Set<any>();
|
|
31
33
|
|
|
32
34
|
constructor(scrypted: ScryptedRuntime, id: string) {
|
|
33
35
|
this.scrypted = scrypted;
|
|
@@ -40,12 +42,19 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
|
|
|
40
42
|
(async () => {
|
|
41
43
|
const mixinProvider = this.scrypted.getDevice(mixinEntry.mixinProviderId) as ScryptedDevice & MixinProvider;
|
|
42
44
|
const { proxy } = await mixinEntry.entry;
|
|
45
|
+
// allow mixins in the process of being released to manage final
|
|
46
|
+
// events, etc, before teardown.
|
|
47
|
+
this.releasing.add(proxy);
|
|
43
48
|
mixinProvider?.releaseMixin(this.id, proxy);
|
|
49
|
+
await sleep(1000);
|
|
50
|
+
this.releasing.delete(proxy);
|
|
44
51
|
})().catch(() => { });
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
async isMixin(id: string, mixinDevice: any) {
|
|
48
|
-
|
|
55
|
+
if (this.releasing.has(mixinDevice))
|
|
56
|
+
return true;
|
|
57
|
+
await this.scrypted.devices[id].handler.ensureProxy();
|
|
49
58
|
for (const mixin of this.scrypted.devices[id].handler.mixinTable) {
|
|
50
59
|
const { proxy } = await mixin.entry;
|
|
51
60
|
if (proxy === mixinDevice) {
|
|
@@ -228,7 +237,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
|
|
|
228
237
|
// todo: remove this and pass the setter directly.
|
|
229
238
|
const deviceState = await host.remote.createDeviceState(this.id,
|
|
230
239
|
async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value));
|
|
231
|
-
const mixinProxy = await mixinProvider.getMixin(wrappedProxy,
|
|
240
|
+
const mixinProxy = await mixinProvider.getMixin(wrappedProxy, previousInterfaces as ScryptedInterface[], deviceState);
|
|
232
241
|
if (!mixinProxy)
|
|
233
242
|
throw new PluginError(`mixin provider ${mixinId} did not return mixin for ${this.id}`);
|
|
234
243
|
|
|
@@ -8,7 +8,6 @@ import { PluginHost } from './plugin-host';
|
|
|
8
8
|
import debounce from 'lodash/debounce';
|
|
9
9
|
import { PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAPI {
|
|
13
12
|
pluginId: string;
|
|
14
13
|
|
|
@@ -46,23 +45,25 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
|
46
45
|
async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId|any, eventInterface: any, eventData?: any) {
|
|
47
46
|
// nativeId code path has been deprecated in favor of mixin object 12/10/2021
|
|
48
47
|
const device = this.scrypted.findPluginDeviceById(id);
|
|
48
|
+
|
|
49
49
|
if (!nativeIdOrMixinDevice || typeof nativeIdOrMixinDevice === 'string') {
|
|
50
|
+
const nativeId: string = nativeIdOrMixinDevice;
|
|
50
51
|
// todo: deprecate this code path
|
|
51
|
-
const mixinProvider = this.scrypted.findPluginDevice(this.pluginId,
|
|
52
|
+
const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
|
52
53
|
const mixins: string[] = getState(device, ScryptedInterfaceProperty.mixins) || [];
|
|
53
54
|
if (!mixins.includes(mixinProvider._id))
|
|
54
55
|
throw new Error(`${mixinProvider._id} is not a mixin provider for ${id}`);
|
|
55
56
|
|
|
56
|
-
this.scrypted.findPluginDevice(this.pluginId,
|
|
57
|
+
this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
|
57
58
|
const tableEntry = this.scrypted.devices[device._id].handler.mixinTable.find(entry => entry.mixinProviderId === mixinProvider._id);
|
|
58
59
|
const { interfaces } = await tableEntry.entry;
|
|
59
60
|
if (!interfaces.has(eventInterface))
|
|
60
61
|
throw new Error(`${mixinProvider._id} does not mixin ${eventInterface} for ${id}`);
|
|
61
62
|
}
|
|
62
63
|
else {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw new Error(`${
|
|
64
|
+
const mixin: object = nativeIdOrMixinDevice;
|
|
65
|
+
if (!await this.scrypted.devices[device._id]?.handler?.isMixin(id, mixin)) {
|
|
66
|
+
throw new Error(`${mixin} does not mixin ${eventInterface} for ${id}`);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
|
|
@@ -150,8 +151,8 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
|
150
151
|
async getDeviceById<T>(id: string): Promise<T & ScryptedDevice> {
|
|
151
152
|
return this.scrypted.getDevice(id);
|
|
152
153
|
}
|
|
153
|
-
async listen(
|
|
154
|
-
return this.manageListener(this.scrypted.stateManager.listen(
|
|
154
|
+
async listen(listener: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
|
|
155
|
+
return this.manageListener(this.scrypted.stateManager.listen(listener));
|
|
155
156
|
}
|
|
156
157
|
async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
|
|
157
158
|
const device = this.scrypted.findPluginDeviceById(id);
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { RpcMessage, RpcPeer } from '../rpc';
|
|
2
2
|
import AdmZip from 'adm-zip';
|
|
3
|
-
import { SystemManager, DeviceManager, ScryptedNativeId, Device, EventListenerRegister, EngineIOHandler,
|
|
3
|
+
import { SystemManager, DeviceManager, ScryptedNativeId, Device, EventListenerRegister, EngineIOHandler, ScryptedInterface, ScryptedInterfaceProperty } from '@scrypted/sdk/types'
|
|
4
4
|
import { ScryptedRuntime } from '../runtime';
|
|
5
5
|
import { Plugin } from '../db-types';
|
|
6
|
-
import io from 'engine.io';
|
|
6
|
+
import io, { Socket } from 'engine.io';
|
|
7
7
|
import { attachPluginRemote, setupPluginRemote } from './plugin-remote';
|
|
8
|
-
import { PluginAPI, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
8
|
+
import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
9
9
|
import { Logger } from '../logger';
|
|
10
10
|
import { MediaManagerHostImpl, MediaManagerImpl } from './media';
|
|
11
|
-
import
|
|
12
|
-
import WebSocket, { EventEmitter } from 'ws';
|
|
11
|
+
import WebSocket from 'ws';
|
|
13
12
|
import { PassThrough } from 'stream';
|
|
14
13
|
import { Console } from 'console'
|
|
15
14
|
import { sleep } from '../sleep';
|
|
@@ -25,6 +24,7 @@ import { ensurePluginVolume } from './plugin-volume';
|
|
|
25
24
|
import { installOptionalDependencies } from './plugin-npm-dependencies';
|
|
26
25
|
import { ConsoleServer, createConsoleServer } from './plugin-console';
|
|
27
26
|
import { createREPLServer } from './plugin-repl';
|
|
27
|
+
import { LazyRemote } from './plugin-lazy-remote';
|
|
28
28
|
|
|
29
29
|
export class PluginHost {
|
|
30
30
|
worker: child_process.ChildProcess;
|
|
@@ -41,7 +41,6 @@ export class PluginHost {
|
|
|
41
41
|
api: PluginHostAPI;
|
|
42
42
|
pluginName: string;
|
|
43
43
|
packageJson: any;
|
|
44
|
-
listener: EventListenerRegister;
|
|
45
44
|
stats: {
|
|
46
45
|
cpuUsage: NodeJS.CpuUsage,
|
|
47
46
|
memoryUsage: NodeJS.MemoryUsage,
|
|
@@ -51,7 +50,6 @@ export class PluginHost {
|
|
|
51
50
|
|
|
52
51
|
kill() {
|
|
53
52
|
this.killed = true;
|
|
54
|
-
this.listener.removeListener();
|
|
55
53
|
this.api.removeListeners();
|
|
56
54
|
this.worker.kill();
|
|
57
55
|
this.io.close();
|
|
@@ -77,7 +75,6 @@ export class PluginHost {
|
|
|
77
75
|
return this.pluginName || 'no plugin name';
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
|
|
81
78
|
async upsertDevice(upsert: Device) {
|
|
82
79
|
const pi = await this.scrypted.upsertDevice(this.pluginId, upsert, true);
|
|
83
80
|
await this.remote.setNativeId(pi.nativeId, pi._id, pi.storage || {});
|
|
@@ -101,6 +98,17 @@ export class PluginHost {
|
|
|
101
98
|
|
|
102
99
|
this.io.on('connection', async (socket) => {
|
|
103
100
|
try {
|
|
101
|
+
try {
|
|
102
|
+
if (socket.request.url.indexOf('/api') !== -1) {
|
|
103
|
+
await this.createRpcIoPeer(socket);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
socket.close();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
const {
|
|
105
113
|
endpointRequest,
|
|
106
114
|
pluginDevice,
|
|
@@ -138,8 +146,7 @@ export class PluginHost {
|
|
|
138
146
|
logger.log('i', `loading ${this.pluginName}`);
|
|
139
147
|
logger.log('i', 'pid ' + this.worker?.pid);
|
|
140
148
|
|
|
141
|
-
|
|
142
|
-
const remotePromise = setupPluginRemote(this.peer, this.api, self.pluginId);
|
|
149
|
+
const remotePromise = setupPluginRemote(this.peer, this.api, self.pluginId, () => this.scrypted.stateManager.getSystemState());
|
|
143
150
|
const init = (async () => {
|
|
144
151
|
const remote = await remotePromise;
|
|
145
152
|
|
|
@@ -190,19 +197,9 @@ export class PluginHost {
|
|
|
190
197
|
this.module = init.then(({ module }) => module);
|
|
191
198
|
this.remote = new LazyRemote(remotePromise, init.then(({ remote }) => remote));
|
|
192
199
|
|
|
193
|
-
this.listener = scrypted.stateManager.listen((id, eventDetails, eventData) => {
|
|
194
|
-
if (eventDetails.property) {
|
|
195
|
-
const device = scrypted.findPluginDeviceById(id);
|
|
196
|
-
this.remote.notify(id, eventDetails.eventTime, eventDetails.eventInterface, eventDetails.property, device?.state[eventDetails.property], eventDetails.changed);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
this.remote.notify(id, eventDetails.eventTime, eventDetails.eventInterface, eventDetails.property, eventData, eventDetails.changed);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
200
|
init.catch(e => {
|
|
204
201
|
console.error('plugin failed to load', e);
|
|
205
|
-
this.
|
|
202
|
+
this.api.removeListeners();
|
|
206
203
|
});
|
|
207
204
|
}
|
|
208
205
|
|
|
@@ -314,6 +311,27 @@ export class PluginHost {
|
|
|
314
311
|
}
|
|
315
312
|
};
|
|
316
313
|
}
|
|
314
|
+
|
|
315
|
+
async createRpcIoPeer(socket: Socket) {
|
|
316
|
+
let connected = true;
|
|
317
|
+
const rpcPeer = new RpcPeer(`api/${this.pluginId}`, 'web', (message, reject) => {
|
|
318
|
+
if (!connected)
|
|
319
|
+
reject?.(new Error('peer disconnected'));
|
|
320
|
+
else
|
|
321
|
+
socket.send(JSON.stringify(message))
|
|
322
|
+
});
|
|
323
|
+
socket.on('message', data => rpcPeer.handleMessage(JSON.parse(data as string)));
|
|
324
|
+
// wrap the host api with a connection specific api that can be torn down on disconnect
|
|
325
|
+
const api = new PluginAPIProxy(this.api, await this.peer.getParam('mediaManager'));
|
|
326
|
+
const kill = () => {
|
|
327
|
+
connected = false;
|
|
328
|
+
rpcPeer.kill('engine.io connection closed.')
|
|
329
|
+
api.removeListeners();
|
|
330
|
+
}
|
|
331
|
+
socket.on('close', kill);
|
|
332
|
+
socket.on('error', kill);
|
|
333
|
+
return setupPluginRemote(rpcPeer, api, null, () => this.scrypted.stateManager.getSystemState());
|
|
334
|
+
}
|
|
317
335
|
}
|
|
318
336
|
|
|
319
337
|
export function startPluginRemote() {
|
|
@@ -331,13 +349,6 @@ export function startPluginRemote() {
|
|
|
331
349
|
let api: PluginAPI;
|
|
332
350
|
let pluginId: string;
|
|
333
351
|
|
|
334
|
-
function idForNativeId(nativeId: ScryptedNativeId) {
|
|
335
|
-
if (!deviceManager)
|
|
336
|
-
return;
|
|
337
|
-
const ds = deviceManager.getDeviceState(nativeId);
|
|
338
|
-
return ds?.id;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
352
|
const getConsole = (hook: (stdout: PassThrough, stderr: PassThrough) => Promise<void>,
|
|
342
353
|
also?: Console, alsoPrefix?: string) => {
|
|
343
354
|
|
|
@@ -489,7 +500,7 @@ export function startPluginRemote() {
|
|
|
489
500
|
getPluginConsole,
|
|
490
501
|
getDeviceConsole,
|
|
491
502
|
getMixinConsole,
|
|
492
|
-
async getServicePort(name) {
|
|
503
|
+
async getServicePort(name, ...args: any[]) {
|
|
493
504
|
if (name === 'repl') {
|
|
494
505
|
if (!replPort)
|
|
495
506
|
throw new Error('REPL unavailable: Plugin not loaded.')
|
|
@@ -530,69 +541,3 @@ export function startPluginRemote() {
|
|
|
530
541
|
});
|
|
531
542
|
})
|
|
532
543
|
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Warning: do not await in any of these methods unless necessary, otherwise
|
|
536
|
-
* execution order of state reporting may fail.
|
|
537
|
-
*/
|
|
538
|
-
class LazyRemote implements PluginRemote {
|
|
539
|
-
remote: PluginRemote;
|
|
540
|
-
|
|
541
|
-
constructor(public remotePromise: Promise<PluginRemote>, public remoteReadyPromise: Promise<PluginRemote>) {
|
|
542
|
-
this.remoteReadyPromise = (async () => {
|
|
543
|
-
this.remote = await remoteReadyPromise;
|
|
544
|
-
return this.remote;
|
|
545
|
-
})();
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async loadZip(packageJson: any, zipData: Buffer, options?: PluginRemoteLoadZipOptions): Promise<any> {
|
|
549
|
-
if (!this.remote)
|
|
550
|
-
await this.remoteReadyPromise;
|
|
551
|
-
return this.remote.loadZip(packageJson, zipData, options);
|
|
552
|
-
}
|
|
553
|
-
async setSystemState(state: { [id: string]: { [property: string]: SystemDeviceState; }; }): Promise<void> {
|
|
554
|
-
if (!this.remote)
|
|
555
|
-
await this.remoteReadyPromise;
|
|
556
|
-
return this.remote.setSystemState(state);
|
|
557
|
-
}
|
|
558
|
-
async setNativeId(nativeId: ScryptedNativeId, id: string, storage: { [key: string]: any; }): Promise<void> {
|
|
559
|
-
if (!this.remote)
|
|
560
|
-
await this.remoteReadyPromise;
|
|
561
|
-
return this.remote.setNativeId(nativeId, id, storage);
|
|
562
|
-
}
|
|
563
|
-
async updateDeviceState(id: string, state: { [property: string]: SystemDeviceState; }): Promise<void> {
|
|
564
|
-
try {
|
|
565
|
-
if (!this.remote)
|
|
566
|
-
await this.remoteReadyPromise;
|
|
567
|
-
}
|
|
568
|
-
catch (e) {
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
return this.remote.updateDeviceState(id, state);
|
|
572
|
-
}
|
|
573
|
-
async notify(id: string, eventTime: number, eventInterface: string, property: string, propertyState: SystemDeviceState, changed?: boolean): Promise<void> {
|
|
574
|
-
try {
|
|
575
|
-
if (!this.remote)
|
|
576
|
-
await this.remoteReadyPromise;
|
|
577
|
-
}
|
|
578
|
-
catch (e) {
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
return this.remote.notify(id, eventTime, eventInterface, property, propertyState, changed);
|
|
582
|
-
}
|
|
583
|
-
async ioEvent(id: string, event: string, message?: any): Promise<void> {
|
|
584
|
-
if (!this.remote)
|
|
585
|
-
await this.remoteReadyPromise;
|
|
586
|
-
return this.remote.ioEvent(id, event, message);
|
|
587
|
-
}
|
|
588
|
-
async createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>): Promise<any> {
|
|
589
|
-
if (!this.remote)
|
|
590
|
-
await this.remoteReadyPromise;
|
|
591
|
-
return this.remote.createDeviceState(id, setState);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async getServicePort(name: string): Promise<number> {
|
|
595
|
-
const remote = await this.remotePromise;
|
|
596
|
-
return remote.getServicePort(name);
|
|
597
|
-
}
|
|
598
|
-
}
|