@scrypted/server 0.0.85 → 0.0.89

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.

@@ -9,114 +9,220 @@ import { allInterfaceProperties, isValidInterfaceMethod, methodInterfaces } from
9
9
 
10
10
  interface MixinTable {
11
11
  mixinProviderId: string;
12
- interfaces: string[];
13
- proxy: Promise<any>;
12
+ entry: Promise<MixinTableEntry>;
13
+ }
14
+
15
+ interface MixinTableEntry {
16
+ interfaces: Set<string>
17
+ allInterfaces: string[];
18
+ proxy: any;
19
+ error?: Error;
14
20
  }
15
21
 
16
22
  export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
17
23
  scrypted: ScryptedRuntime;
18
24
  id: string;
19
- mixinTable: Promise<MixinTable[]>;
25
+ mixinTable: MixinTable[];
20
26
 
21
27
  constructor(scrypted: ScryptedRuntime, id: string) {
22
28
  this.scrypted = scrypted;
23
29
  this.id = id;
24
30
  }
25
31
 
32
+ invalidateEntry(mixinEntry: MixinTable) {
33
+ if (!mixinEntry.mixinProviderId)
34
+ return;
35
+ (async () => {
36
+ const mixinProvider = this.scrypted.getDevice(mixinEntry.mixinProviderId) as ScryptedDevice & MixinProvider;
37
+ const { proxy } = await mixinEntry.entry;
38
+ mixinProvider?.releaseMixin(this.id, proxy);
39
+ })().catch(() => { });
40
+ }
41
+
26
42
  // should this be async?
27
43
  invalidate() {
28
44
  const mixinTable = this.mixinTable;
29
45
  this.mixinTable = undefined;
30
- (async () => {
31
- for (const mixinEntry of (await mixinTable || [])) {
32
- if (!mixinEntry.mixinProviderId)
33
- continue;
34
- (async () => {
35
- const mixinProvider = this.scrypted.getDevice(mixinEntry.mixinProviderId) as ScryptedDevice & MixinProvider;
36
- mixinProvider?.releaseMixin(this.id, await mixinEntry.proxy);
37
- })().catch(() => { });
38
- }
39
- })().catch(() => { });;
46
+ for (const mixinEntry of (mixinTable || [])) {
47
+ this.invalidateEntry(mixinEntry);
48
+ }
40
49
  }
41
50
 
42
- // this must not be async, because it potentially changes execution order.
43
- ensureProxy(): Promise<PluginDevice> {
51
+ invalidateMixinTable() {
52
+ if (!this.mixinTable)
53
+ return this.invalidate();
54
+
55
+ let previousMixinIds = this.mixinTable?.map(entry => entry.mixinProviderId) || [];
56
+ previousMixinIds.pop();
57
+ previousMixinIds = previousMixinIds.reverse();
58
+
44
59
  const pluginDevice = this.scrypted.findPluginDeviceById(this.id);
45
60
  if (!pluginDevice)
46
- throw new Error(`device ${this.id} does not exist`);
61
+ return this.invalidate();
62
+
63
+ const mixins = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
64
+ // iterate the new mixin table to find the last good mixin,
65
+ // and resume creation from there.
66
+ let lastValidMixinId: string;
67
+ for (const mixinId of mixins) {
68
+ if (!previousMixinIds.length) {
69
+ // reached of the previous mixin table, meaning
70
+ // mixins were added.
71
+ break;
72
+ }
73
+ const check = previousMixinIds.shift();
74
+ if (check !== mixinId)
75
+ break;
47
76
 
48
- if (this.mixinTable)
49
- return Promise.resolve(pluginDevice);
77
+ lastValidMixinId = mixinId;
78
+ }
50
79
 
51
- this.mixinTable = (async () => {
52
- let proxy: any;
53
- if (!pluginDevice.nativeId) {
54
- const plugin = this.scrypted.plugins[pluginDevice.pluginId];
55
- proxy = await plugin.module;
56
- }
57
- else {
58
- const providerId = getState(pluginDevice, ScryptedInterfaceProperty.providerId);
59
- const provider = this.scrypted.getDevice(providerId) as ScryptedDevice & DeviceProvider;
60
- proxy = await provider.getDevice(pluginDevice.nativeId);
61
- }
80
+ if (!lastValidMixinId)
81
+ return this.invalidate();
62
82
 
63
- if (!proxy)
64
- console.error('null proxy', this.id);
65
- // after creating an actual device, apply all the mixins
66
- const type = getDisplayType(pluginDevice);
67
- const allInterfaces: ScryptedInterface[] = getState(pluginDevice, ScryptedInterfaceProperty.providedInterfaces) || [];
83
+ // invalidate and remove everything up to lastValidMixinId
84
+ while (true) {
85
+ const entry = this.mixinTable[0];
86
+ if (entry.mixinProviderId === lastValidMixinId)
87
+ break;
88
+ this.mixinTable.shift();
89
+ this.invalidateEntry(entry);
90
+ }
68
91
 
69
- const mixinTable: MixinTable[] = [];
70
- mixinTable.unshift({
71
- mixinProviderId: undefined,
72
- interfaces: allInterfaces.slice(),
73
- proxy,
74
- })
92
+ this.ensureProxy(lastValidMixinId);
93
+ }
75
94
 
76
- for (const mixinId of getState(pluginDevice, ScryptedInterfaceProperty.mixins) || []) {
77
- const mixinProvider = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider;
95
+ // this must not be async, because it potentially changes execution order.
96
+ ensureProxy(lastValidMixinId?: string): Promise<PluginDevice> {
97
+ const pluginDevice = this.scrypted.findPluginDeviceById(this.id);
98
+ if (!pluginDevice)
99
+ throw new Error(`device ${this.id} does not exist`);
100
+
101
+ let previousEntry: Promise<MixinTableEntry>;
102
+ if (!lastValidMixinId) {
103
+ if (this.mixinTable)
104
+ return Promise.resolve(pluginDevice);
105
+
106
+ this.mixinTable = [];
78
107
 
108
+ previousEntry = (async () => {
109
+ let proxy;
79
110
  try {
80
- const interfaces = await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
81
- if (!interfaces) {
82
- console.warn(`mixin provider ${mixinId} can no longer mixin ${this.id}`);
83
- const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
84
- this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
85
- this.scrypted.datastore.upsert(pluginDevice);
86
- continue;
111
+ if (!pluginDevice.nativeId) {
112
+ const plugin = this.scrypted.plugins[pluginDevice.pluginId];
113
+ proxy = await plugin.module;
114
+ }
115
+ else {
116
+ const providerId = getState(pluginDevice, ScryptedInterfaceProperty.providerId);
117
+ const provider = this.scrypted.getDevice(providerId) as ScryptedDevice & DeviceProvider;
118
+ proxy = await provider.getDevice(pluginDevice.nativeId);
87
119
  }
88
120
 
89
- const wrappedHandler = new PluginDeviceProxyHandler(this.scrypted, this.id);
90
- wrappedHandler.mixinTable = Promise.resolve(mixinTable.slice());
91
- const wrappedProxy = new Proxy(wrappedHandler, wrappedHandler);
92
-
93
- const host = this.scrypted.getPluginHostForDeviceId(mixinId);
94
- const deviceState = await host.remote.createDeviceState(this.id,
95
- async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value));
96
- const mixinProxy = await mixinProvider.getMixin(wrappedProxy, allInterfaces, deviceState);
97
- if (!mixinProxy)
98
- throw new Error(`mixin provider ${mixinId} did not return mixin for ${this.id}`);
99
- allInterfaces.push(...interfaces);
100
- proxy = mixinProxy;
101
-
102
- mixinTable.unshift({
103
- mixinProviderId: mixinId,
104
- interfaces,
105
- proxy,
106
- })
121
+ if (!proxy)
122
+ console.warn('no device was returned by the plugin', this.id);
107
123
  }
108
124
  catch (e) {
109
- console.warn("mixin provider failure", mixinId, e);
125
+ console.warn('error occured retrieving device from plugin');
110
126
  }
111
- }
112
127
 
113
- const mixinInterfaces = [...new Set(allInterfaces)].sort();
114
- this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.interfaces, mixinInterfaces);
128
+ const interfaces: ScryptedInterface[] = getState(pluginDevice, ScryptedInterfaceProperty.providedInterfaces) || [];
129
+ return {
130
+ proxy,
131
+ interfaces: new Set<string>(interfaces),
132
+ allInterfaces: interfaces,
133
+ }
134
+ })();
135
+
136
+ this.mixinTable.unshift({
137
+ mixinProviderId: undefined,
138
+ entry: previousEntry,
139
+ });
140
+ }
141
+ else {
142
+ if (!this.mixinTable)
143
+ throw new Error('mixin table partial invalidation was called with empty mixin table');
144
+ const prevTable = this.mixinTable.find(table => table.mixinProviderId === lastValidMixinId);
145
+ if (!prevTable)
146
+ throw new Error('mixin table partial invalidation was called with invalid lastValidMixinId');
147
+ previousEntry = prevTable.entry;
148
+ }
149
+
150
+ const type = getDisplayType(pluginDevice);
151
+
152
+ for (const mixinId of getState(pluginDevice, ScryptedInterfaceProperty.mixins) || []) {
153
+ if (lastValidMixinId) {
154
+ if (mixinId === lastValidMixinId)
155
+ lastValidMixinId = undefined;
156
+ continue;
157
+ }
115
158
 
116
- return mixinTable;
117
- })();
159
+ const wrappedMixinTable = this.mixinTable.slice();
160
+
161
+ this.mixinTable.unshift({
162
+ mixinProviderId: mixinId,
163
+ entry: (async () => {
164
+ let { allInterfaces } = await previousEntry;
165
+ try {
166
+ const mixinProvider = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider;
167
+ const interfaces = await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
168
+ if (!interfaces) {
169
+ // this is not an error
170
+ // do not advertise interfaces so it is skipped during
171
+ // vtable lookup.
172
+ console.log(`mixin provider ${mixinId} can no longer mixin ${this.id}`);
173
+ const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
174
+ this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
175
+ this.scrypted.datastore.upsert(pluginDevice);
176
+ return {
177
+ allInterfaces,
178
+ interfaces: new Set<string>(),
179
+ proxy: undefined as any,
180
+ };
181
+ }
182
+
183
+ allInterfaces = allInterfaces.slice();
184
+ allInterfaces.push(...interfaces);
185
+ const combinedInterfaces = [...new Set(allInterfaces)];
186
+
187
+ const wrappedHandler = new PluginDeviceProxyHandler(this.scrypted, this.id);
188
+ wrappedHandler.mixinTable = wrappedMixinTable;
189
+ const wrappedProxy = new Proxy(wrappedHandler, wrappedHandler);
190
+
191
+ const host = this.scrypted.getPluginHostForDeviceId(mixinId);
192
+ const deviceState = await host.remote.createDeviceState(this.id,
193
+ async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value));
194
+ const mixinProxy = await mixinProvider.getMixin(wrappedProxy, allInterfaces as ScryptedInterface[], deviceState);
195
+ if (!mixinProxy)
196
+ throw new Error(`mixin provider ${mixinId} did not return mixin for ${this.id}`);
197
+
198
+ return {
199
+ interfaces: new Set<string>(interfaces),
200
+ allInterfaces: combinedInterfaces,
201
+ proxy: mixinProxy,
202
+ };
203
+ }
204
+ catch (e) {
205
+ // on any error, do not advertise interfaces
206
+ // on this mixin, so as to prevent total failure?
207
+ // this has been the behavior for a while,
208
+ // but maybe interfaces implemented by that mixin
209
+ // should rethrow the error caught here in applyMixin.
210
+ console.warn(e);
211
+ return {
212
+ allInterfaces,
213
+ interfaces: new Set<string>(),
214
+ error: e,
215
+ proxy: undefined as any,
216
+ };
217
+ }
218
+ })(),
219
+ });
220
+ }
118
221
 
119
- return this.mixinTable.then(_ => pluginDevice);
222
+ return this.mixinTable[0].entry.then(entry => {
223
+ this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.interfaces, entry.allInterfaces);
224
+ return pluginDevice;
225
+ });
120
226
  }
121
227
 
122
228
  get(target: any, p: PropertyKey, receiver: any): any {
@@ -180,12 +286,14 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
180
286
  if (!iface)
181
287
  throw new Error(`unknown method ${method}`);
182
288
 
183
- const mixinTable = await this.mixinTable;
184
- for (const mixin of mixinTable) {
185
-
289
+ for (const mixin of this.mixinTable) {
290
+ const { interfaces, proxy } = await mixin.entry;
186
291
  // this could be null?
187
- if (mixin.interfaces.includes(iface))
188
- return (await mixin.proxy)[method](...argArray);
292
+ if (interfaces.has(iface)) {
293
+ if (!proxy)
294
+ throw new Error(`device is unavailable ${this.id} (mixin ${mixin.mixinProviderId})`);
295
+ return proxy[method](...argArray);
296
+ }
189
297
  }
190
298
 
191
299
  throw new Error(`${method} not implemented`)
@@ -47,8 +47,9 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
47
47
  const mixins: string[] = getState(device, ScryptedInterfaceProperty.mixins) || [];
48
48
  if (!mixins.includes(mixinProvider._id))
49
49
  throw new Error(`${mixinProvider._id} is not a mixin provider for ${id}`);
50
- const tableEntry = (await this.scrypted.devices[device._id].handler.mixinTable).find(entry => entry.mixinProviderId === mixinProvider._id);
51
- if (!tableEntry.interfaces.includes(eventInterface))
50
+ const tableEntry = this.scrypted.devices[device._id].handler.mixinTable.find(entry => entry.mixinProviderId === mixinProvider._id);
51
+ const { interfaces } = await tableEntry.entry;
52
+ if (!interfaces.has(eventInterface))
52
53
  throw new Error(`${mixinProvider._id} does not mixin ${eventInterface} for ${id}`);
53
54
  this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
54
55
  }
@@ -297,6 +297,11 @@ export class PluginHost {
297
297
  this.worker.stderr.on('data', data => console.error(data.toString()));
298
298
  this.consoleServer = createConsoleServer(this.worker.stdout, this.worker.stderr);
299
299
 
300
+ this.consoleServer.then(cs => {
301
+ const { pluginConsole } = cs;
302
+ pluginConsole.log('starting plugin', this.pluginId, this.packageJson.version);
303
+ });
304
+
300
305
  this.worker.on('disconnect', () => {
301
306
  connected = false;
302
307
  logger.log('e', `${this.pluginName} disconnected`);
@@ -384,6 +389,11 @@ export function startPluginRemote() {
384
389
  }
385
390
 
386
391
  const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
392
+ // the the plugin console is simply the default console
393
+ // and gets read from stderr/stdout.
394
+ if (!nativeId)
395
+ return console;
396
+
387
397
  return getConsole(async (stdout, stderr) => {
388
398
  const plugins = await api.getComponent('plugins');
389
399
  const connect = async () => {
@@ -459,9 +469,9 @@ export function startPluginRemote() {
459
469
 
460
470
  let _pluginConsole: Console;
461
471
  const getPluginConsole = () => {
462
- if (_pluginConsole)
463
- return _pluginConsole;
464
- _pluginConsole = getDeviceConsole(undefined);
472
+ if (!_pluginConsole)
473
+ _pluginConsole = getDeviceConsole(undefined);
474
+ return _pluginConsole;
465
475
  }
466
476
 
467
477
  attachPluginRemote(peer, {
@@ -474,7 +484,7 @@ export function startPluginRemote() {
474
484
  pluginId = _pluginId;
475
485
  peer.selfName = pluginId;
476
486
  },
477
- onPluginReady: async(scrypted, params, plugin) => {
487
+ onPluginReady: async (scrypted, params, plugin) => {
478
488
  replPort = createREPLServer(scrypted, params, plugin);
479
489
  },
480
490
  getPluginConsole,
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/sdk/types'
5
5
  import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
6
6
  import { SystemManagerImpl } from './system';
7
- import { RpcPeer, RPCResultError, PROPERTY_PROXY_ONEWAY_METHODS, PROPERTY_JSON_DISABLE_SERIALIZATION } from '../rpc';
7
+ import { RpcPeer, RPCResultError, PROPERTY_PROXY_ONEWAY_METHODS, PROPERTY_JSON_DISABLE_SERIALIZATION } from '../rpc';
8
8
  import { BufferSerializer } from './buffer-serializer';
9
9
  import { EventEmitter } from 'events';
10
10
  import { createWebSocketClass } from './plugin-remote-websocket';
@@ -407,7 +407,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
407
407
 
408
408
  async loadZip(packageJson: any, zipData: Buffer, zipOptions?: PluginRemoteLoadZipOptions) {
409
409
  const pluginConsole = getPluginConsole?.();
410
- pluginConsole?.log('starting plugin', pluginId, packageJson.version);
411
410
  const zip = new AdmZip(zipData);
412
411
  await options?.onLoadZip?.(zip, packageJson);
413
412
  const main = zip.getEntry('main.nodejs.js');
package/src/runtime.ts CHANGED
@@ -424,6 +424,15 @@ export class ScryptedRuntime {
424
424
  return proxyPair;
425
425
  }
426
426
 
427
+ // should this be async?
428
+ invalidatePluginDeviceMixins(id: string) {
429
+ const proxyPair = this.devices[id];
430
+ if (!proxyPair)
431
+ return;
432
+ proxyPair.handler.invalidateMixinTable();
433
+ return proxyPair;
434
+ }
435
+
427
436
  async installNpm(pkg: string, version?: string): Promise<PluginHost> {
428
437
  const registry = (await axios(`https://registry.npmjs.org/${pkg}`)).data;
429
438
  if (!version) {
@@ -29,7 +29,7 @@ export class PluginComponent {
29
29
  const pluginDevice = this.scrypted.findPluginDeviceById(id);
30
30
  this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, [...new Set(mixins)] || []);
31
31
  await this.scrypted.datastore.upsert(pluginDevice);
32
- const device = this.scrypted.invalidatePluginDevice(id);
32
+ const device = this.scrypted.invalidatePluginDeviceMixins(id);
33
33
  await device?.handler.ensureProxy();
34
34
  }
35
35
  async getMixins(id: string) {