@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.

Files changed (49) hide show
  1. package/dist/http-interfaces.js +2 -2
  2. package/dist/http-interfaces.js.map +1 -1
  3. package/dist/plugin/listen-zero.js +13 -1
  4. package/dist/plugin/listen-zero.js.map +1 -1
  5. package/dist/plugin/plugin-device.js +11 -2
  6. package/dist/plugin/plugin-device.js.map +1 -1
  7. package/dist/plugin/plugin-host-api.js +8 -7
  8. package/dist/plugin/plugin-host-api.js.map +1 -1
  9. package/dist/plugin/plugin-host.js +36 -87
  10. package/dist/plugin/plugin-host.js.map +1 -1
  11. package/dist/plugin/plugin-http.js +100 -0
  12. package/dist/plugin/plugin-http.js.map +1 -0
  13. package/dist/plugin/plugin-lazy-remote.js +73 -0
  14. package/dist/plugin/plugin-lazy-remote.js.map +1 -0
  15. package/dist/plugin/plugin-npm-dependencies.js +16 -16
  16. package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
  17. package/dist/plugin/plugin-remote-websocket.js +40 -34
  18. package/dist/plugin/plugin-remote-websocket.js.map +1 -1
  19. package/dist/plugin/plugin-remote.js +35 -26
  20. package/dist/plugin/plugin-remote.js.map +1 -1
  21. package/dist/rpc.js +53 -26
  22. package/dist/rpc.js.map +1 -1
  23. package/dist/runtime.js +45 -111
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/scrypted-main.js +3 -0
  26. package/dist/scrypted-main.js.map +1 -1
  27. package/dist/services/plugin.js +2 -2
  28. package/dist/services/plugin.js.map +1 -1
  29. package/dist/state.js +2 -8
  30. package/dist/state.js.map +1 -1
  31. package/package.json +2 -2
  32. package/python/plugin-remote.py +4 -4
  33. package/python/rpc.py +68 -26
  34. package/src/http-interfaces.ts +3 -3
  35. package/src/plugin/listen-zero.ts +13 -0
  36. package/src/plugin/plugin-api.ts +1 -1
  37. package/src/plugin/plugin-device.ts +11 -2
  38. package/src/plugin/plugin-host-api.ts +9 -8
  39. package/src/plugin/plugin-host.ts +40 -95
  40. package/src/plugin/plugin-http.ts +117 -0
  41. package/src/plugin/plugin-lazy-remote.ts +70 -0
  42. package/src/plugin/plugin-npm-dependencies.ts +19 -18
  43. package/src/plugin/plugin-remote-websocket.ts +55 -60
  44. package/src/plugin/plugin-remote.ts +45 -38
  45. package/src/rpc.ts +62 -25
  46. package/src/runtime.ts +55 -128
  47. package/src/scrypted-main.ts +4 -0
  48. package/src/services/plugin.ts +2 -2
  49. package/src/state.ts +2 -10
@@ -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
- from sys import stderr, stdout
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
- python_prefix = os.path.join(os.environ.get(
168
- 'SCRYPTED_PLUGIN_VOLUME'), 'python')
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, 'installed-requirements.txt')
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, proxyId: str, proxyConstructorName: str, proxyProps: any, proxyOneWayMethods: List[str]):
50
- self.__dict__['__proxy_id'] = proxyId
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
- proxyId = self.localProxied.get(value, None)
126
- if proxyId:
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': proxyId,
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(type(value), None)
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
- self.localProxied[value] = proxyId
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, id: str):
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
- proxy = RpcProxy(self, proxyId, proxyConstructorName,
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(proxyId))
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
- local = self.localProxyMap.pop(
301
- message['__local_proxy_id'], None)
302
- self.localProxied.pop(local, None)
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)
@@ -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, plugin: PluginHost): HttpResponse {
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 = plugin.zip.getEntry(`fs/${path}`)?.getData();
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
+ }
@@ -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
- let found = false;
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, allInterfaces as ScryptedInterface[], deviceState);
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, nativeIdOrMixinDevice);
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, nativeIdOrMixinDevice);
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
- if (!await this.scrypted.devices[device._id]?.handler?.isMixin(id, nativeIdOrMixinDevice)) {
64
- const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeIdOrMixinDevice);
65
- throw new Error(`${mixinProvider?._id} does not mixin ${eventInterface} for ${id}`);
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(EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
154
- return this.manageListener(this.scrypted.stateManager.listen(EventListener));
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, ScryptedInterfaceProperty, SystemDeviceState } from '@scrypted/sdk/types'
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 { getState } from '../state';
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.listener.removeListener();
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
- }