@scrypted/server 0.0.86 → 0.0.90

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.

@@ -4,40 +4,67 @@ import { listenZero } from './listen-zero';
4
4
  import { Server } from 'net';
5
5
  import { once } from 'events';
6
6
  import net from 'net'
7
- import { Readable } from 'stream';
7
+ import { Readable, PassThrough } from 'stream';
8
+ import { Console } from 'console';
8
9
 
9
10
  export interface ConsoleServer {
11
+ pluginConsole: Console;
10
12
  readPort: number,
11
13
  writePort: number,
12
14
  readServer: net.Server,
13
15
  writeServer: net.Server,
14
16
  sockets: Set<net.Socket>;
15
17
  }
16
- export async function createConsoleServer(stdout: Readable, stderr: Readable) {
17
- const outputs = new Map<string, Buffer[]>();
18
- const appendOutput = (data: Buffer, nativeId: ScryptedNativeId) => {
18
+
19
+ export interface StdPassThroughs {
20
+ stdout: PassThrough;
21
+ stderr: PassThrough;
22
+ buffers: Buffer[];
23
+ }
24
+
25
+ export async function createConsoleServer(remoteStdout: Readable, remoteStderr: Readable) {
26
+ const outputs = new Map<string, StdPassThroughs>();
27
+
28
+ const getPassthroughs = (nativeId?: ScryptedNativeId) => {
19
29
  if (!nativeId)
20
30
  nativeId = undefined;
21
- let buffers = outputs.get(nativeId);
22
- if (!buffers) {
23
- buffers = [];
24
- outputs.set(nativeId, buffers);
31
+ let pts = outputs.get(nativeId)
32
+ if (!pts) {
33
+ const stdout = new PassThrough();
34
+ const stderr = new PassThrough();
35
+
36
+ pts = {
37
+ stdout,
38
+ stderr,
39
+ buffers: [],
40
+ }
41
+ outputs.set(nativeId, pts);
42
+
43
+ const appendOutput = (data: Buffer) => {
44
+ const { buffers } = pts;
45
+ buffers.push(data);
46
+ // when we're over 4000 lines or whatever these buffer are,
47
+ // truncate down to 2000.
48
+ if (buffers.length > 4000)
49
+ pts.buffers = buffers.slice(buffers.length - 2000);
50
+ };
51
+
52
+ stdout.on('data', appendOutput);
53
+ stderr.on('data', appendOutput);
25
54
  }
26
- buffers.push(data);
27
- // when we're over 4000 lines or whatever these buffer are,
28
- // truncate down to 2000.
29
- if (buffers.length > 4000)
30
- outputs.set(nativeId, buffers.slice(buffers.length - 2000))
31
- };
32
55
 
33
- const sockets = new Set<net.Socket>();
56
+ return pts;
57
+ }
34
58
 
35
- const events = new EventEmitter();
36
- events.on('stdout', appendOutput);
37
- events.on('stderr', appendOutput);
59
+ let pluginConsole: Console;
60
+ {
61
+ const { stdout, stderr } = getPassthroughs();
62
+ remoteStdout.pipe(stdout);
63
+ remoteStderr.pipe(stderr);
64
+ pluginConsole = new Console(stdout, stderr);
65
+ }
38
66
 
39
- stdout.on('data', data => events.emit('stdout', data));
40
- stderr.on('data', data => events.emit('stderr', data));
67
+ const sockets = new Set<net.Socket>();
41
68
 
42
69
  const readServer = new Server(async (socket) => {
43
70
  sockets.add(socket);
@@ -47,24 +74,22 @@ export async function createConsoleServer(stdout: Readable, stderr: Readable) {
47
74
  if (filter === 'undefined')
48
75
  filter = undefined;
49
76
 
50
- const buffers = outputs.get(filter);
77
+ const pts = outputs.get(filter);
78
+ const buffers = pts?.buffers;
51
79
  if (buffers) {
52
80
  const concat = Buffer.concat(buffers);
53
- outputs.set(filter, [concat]);
81
+ pts.buffers = [concat];
54
82
  socket.write(concat);
55
83
  }
56
84
 
57
- const cb = (data: Buffer, nativeId: ScryptedNativeId) => {
58
- if (nativeId !== filter)
59
- return;
60
- socket.write(data);
61
- };
62
- events.on('stdout', cb)
63
- events.on('stderr', cb)
85
+ const cb = (data: Buffer) => socket.write(data);
86
+ const { stdout, stderr } = getPassthroughs(filter);
87
+ stdout.on('data', cb);
88
+ stderr.on('data', cb);
64
89
 
65
90
  const cleanup = () => {
66
- events.removeListener('stdout', cb);
67
- events.removeListener('stderr', cb);
91
+ stdout.removeListener('data', cb);
92
+ stderr.removeListener('data', cb);
68
93
  socket.destroy();
69
94
  socket.removeAllListeners();
70
95
  sockets.delete(socket);
@@ -88,9 +113,8 @@ export async function createConsoleServer(stdout: Readable, stderr: Readable) {
88
113
  if (filter === 'undefined')
89
114
  filter = undefined;
90
115
 
91
- const cb = (data: Buffer) => events.emit('stdout', data, filter);
92
-
93
- socket.on('data', cb);
116
+ const { stdout } = getPassthroughs(filter);
117
+ socket.pipe(stdout, { end: false });
94
118
 
95
119
  const cleanup = () => {
96
120
  socket.destroy();
@@ -106,6 +130,7 @@ export async function createConsoleServer(stdout: Readable, stderr: Readable) {
106
130
  const writePort = await listenZero(writeServer);
107
131
 
108
132
  return {
133
+ pluginConsole,
109
134
  readPort,
110
135
  writePort,
111
136
  readServer,
@@ -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
  }
@@ -117,7 +117,6 @@ export class PluginHost {
117
117
  } = (socket.request as any).scrypted;
118
118
 
119
119
  const handler = this.scrypted.getDevice<EngineIOHandler>(pluginDevice._id);
120
- handler.onConnection(endpointRequest, `io://${socket.id}`);
121
120
 
122
121
  socket.on('message', message => {
123
122
  this.remote.ioEvent(socket.id, 'message', message)
@@ -125,6 +124,8 @@ export class PluginHost {
125
124
  socket.on('close', reason => {
126
125
  this.remote.ioEvent(socket.id, 'close');
127
126
  });
127
+
128
+ await handler.onConnection(endpointRequest, `io://${socket.id}`);
128
129
  }
129
130
  catch (e) {
130
131
  console.error('engine.io plugin error', e);
@@ -235,7 +236,7 @@ export class PluginHost {
235
236
  path.join(__dirname, '../../python', 'plugin-remote.py'),
236
237
  )
237
238
 
238
- this.worker = child_process.spawn('python3', args, {
239
+ this.worker = child_process.spawn('python3.7', args, {
239
240
  // stdin, stdout, stderr, peer in, peer out
240
241
  stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
241
242
  env: Object.assign({}, process.env, env),
@@ -297,6 +298,11 @@ export class PluginHost {
297
298
  this.worker.stderr.on('data', data => console.error(data.toString()));
298
299
  this.consoleServer = createConsoleServer(this.worker.stdout, this.worker.stderr);
299
300
 
301
+ this.consoleServer.then(cs => {
302
+ const { pluginConsole } = cs;
303
+ pluginConsole.log('starting plugin', this.pluginId, this.packageJson.version);
304
+ });
305
+
300
306
  this.worker.on('disconnect', () => {
301
307
  connected = false;
302
308
  logger.log('e', `${this.pluginName} disconnected`);
@@ -384,6 +390,11 @@ export function startPluginRemote() {
384
390
  }
385
391
 
386
392
  const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
393
+ // the the plugin console is simply the default console
394
+ // and gets read from stderr/stdout.
395
+ if (!nativeId)
396
+ return console;
397
+
387
398
  return getConsole(async (stdout, stderr) => {
388
399
  const plugins = await api.getComponent('plugins');
389
400
  const connect = async () => {
@@ -459,9 +470,9 @@ export function startPluginRemote() {
459
470
 
460
471
  let _pluginConsole: Console;
461
472
  const getPluginConsole = () => {
462
- if (_pluginConsole)
463
- return _pluginConsole;
464
- _pluginConsole = getDeviceConsole(undefined);
473
+ if (!_pluginConsole)
474
+ _pluginConsole = getDeviceConsole(undefined);
475
+ return _pluginConsole;
465
476
  }
466
477
 
467
478
  attachPluginRemote(peer, {
@@ -474,7 +485,7 @@ export function startPluginRemote() {
474
485
  pluginId = _pluginId;
475
486
  peer.selfName = pluginId;
476
487
  },
477
- onPluginReady: async(scrypted, params, plugin) => {
488
+ onPluginReady: async (scrypted, params, plugin) => {
478
489
  replPort = createREPLServer(scrypted, params, plugin);
479
490
  },
480
491
  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
@@ -317,7 +317,7 @@ export class ScryptedRuntime {
317
317
  }
318
318
 
319
319
  if (!isEngineIOEndpoint && isUpgrade) {
320
- this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, ws => {
320
+ this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, async (ws) => {
321
321
  try {
322
322
  const handler = this.getDevice<EngineIOHandler>(pluginDevice._id);
323
323
  const id = 'ws-' + this.wsAtomic++;
@@ -328,8 +328,6 @@ export class ScryptedRuntime {
328
328
  }
329
329
  pluginHost.ws[id] = ws;
330
330
 
331
- handler.onConnection(httpRequest, `ws://${id}`);
332
-
333
331
  ws.on('message', async (message) => {
334
332
  try {
335
333
  pluginHost.remote.ioEvent(id, 'message', message)
@@ -346,6 +344,8 @@ export class ScryptedRuntime {
346
344
  }
347
345
  delete pluginHost.ws[id];
348
346
  });
347
+
348
+ await handler.onConnection(httpRequest, `ws://${id}`);
349
349
  }
350
350
  catch (e) {
351
351
  console.error('websocket plugin error', e);
@@ -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) {